Manipolatori di voxel Lua
Introduzione
Le funzioni introdotte nel capitolo Mappa: operazioni base sono comode e facili da usare, ma per le grandi aree non sono efficienti. Ogni volta che set_node
e get_node
vengono chiamati da una mod, la mod deve comunicare con il motore di gioco. Ciò risulta in una costante copia individuale dei singoli nodi, che è lenta e abbasserà notevolmente le performance del gioco. Usare un Manipolatore di Voxel Lua (Lua Voxel Manipulator, da qui LVM) può essere un’alternativa migliore.
Concetti
Un LVM permette di caricare grandi pezzi di mappa nella memoria della mod che ne ha bisogno. Da lì si possono leggere e modificare i dati immagazzinati senza dover interagire ulteriormente col motore di gioco, e senza eseguire callback; in altre parole, l’operazione risulta molto più veloce. Una volta fatto ciò, si può passare l’area modificata al motore di gioco ed eseguire eventuali calcoli riguardo la luce.
Lettura negli LVM
Si possono caricare solamente aree cubiche negli LVM, quindi devi capire da te quali sono le posizioni minime e massime che ti servono per l’area da modificare. Fatto ciò, puoi creare l’LVM:
local vm = core.get_voxel_manip()
local emin, emax = vm:read_from_map(pos1, pos2)
Per questioni di performance, un LVM non leggerà quasi mai l’area esatta che gli è stata passata. Al contrario, è molto probabile che ne leggerà una maggiore. Quest’ultima è data da emin
ed emax
, che stanno per posizione minima/massima emersa (emerged min/max pos). Inoltre, un LVM caricherà in automatico l’area passatagli - che sia da memoria, da disco o dal generatore di mappa.
LVM e generatore mappa
Non usare core.get_voxel_manip()
con il generatore mappa, in quanto può causare glitch. Usa invece core.get_mapgen_object("voxelmanip")
.
Lettura dei nodi
Per leggere il tipo dei nodi in posizioni specifiche, avrai bisogno di usare get_data()
. Questo metodo ritorna un array monodimensionale dove ogni voce rappresenta il tipo.
local data = vm:get_data()
Si possono ottenere param2 e i dati della luce usando i metodi get_light_data()
e get_param2_data()
.
Avrai bisogno di usare emin
e emax
per capire dove si trova un nodo nei metodi sopraelencati. C’è una classe di supporto per queste cose chiamate VoxelArea
che gestisce i calcoli al posto tuo.
local a = VoxelArea:new{
MinEdge = emin,
MaxEdge = emax
}
-- Ottiene l'indice del nodo
local idx = a:index(x, y, z)
-- Legge il nodo
print(data[idx])
All’eseguire ciò, si noterà che data[idx]
è un intero. Questo perché il motore di gioco non salva i nodi come stringhe per motivi di performance; al contrario, usa un intero chiamato “ID di contenuto” (content ID). Per scoprire qual è l’ID assegnato a un tipo di nodo, si usa get_content_id()
. Per esempio:
local c_pietra = core.get_content_id("default:stone")
Si può ora controllare se un nodo è effettivamente di pietra:
local idx = a:index(x, y, z)
if data[idx] == c_pietra then
print("è pietra!")
end
Gli ID di contenuto di un nodo potrebbero cambiare durante la fase di caricamento, quindi è consigliato non tentare di ottenerli durante tale fase.
Le coordinate dei nodi nell’array di un LVM sono salvate in ordine inverso (z, y, x
), quindi se le si vuole iterare, si tenga presente che si inizierà dalla Z:
for z = min.z, max.z do
for y = min.y, max.y do
for x = min.x, max.x do
local idx = a:index(x, y, z)
if data[idx] == c_pietra then
print("è pietra!")
end
end
end
end
Per capire la ragione di tale iterazione, bisogna parlare un attimo di architettura dei computer: leggere dalla RAM - la memoria principale - è alquanto dispendioso, quindi i processori hanno molteplici livelli di memoria a breve termine (la cache). Se i dati richiesti da un processo sono in quest’ultima memoria, si possono ottenere velocemente. Al contrario, se i dati lì non ci sono, verranno pescati dalla RAM e inseriti in quella a breve termine, nel caso dovessero servire di nuovo. Questo significa che una buona regola per l’ottimizzazione è quella di iterare in modo che i dati vengano letti in sequenza, evitando di arrivare fino alla RAM ogni volta (cache thrashing).
Scrittura dei nodi
Prima di tutto, bisogna impostare il nuovo ID nell’array:
for z = min.z, max.z do
for y = min.y, max.y do
for x = min.x, max.x do
local idx = a:index(x, y, z)
if data[idx] == c_pietra then
data[idx] = c_aria
end
end
end
end
Una volta finito con le operazioni nell’LVM, bisogna passare l’array al motore di gioco:
vm:set_data(data)
vm:write_to_map(true)
Per la luce e param2, invece si usano set_light_data()
e set_param2_data()
.
write_to_map()
richiede un booleano che è true
se si vuole che venga calcolata anche la luce. Se si passa false
invece, ci sarà bisogno di ricalcolarla in un secondo tempo usando core.fix_light
.
Esempio
local function da_erba_a_terra(pos1, pos2)
local c_terra = core.get_content_id("default:dirt")
local c_erba = core.get_content_id("default:dirt_with_grass")
-- legge i dati nella LVM
local vm = core.get_voxel_manip()
local emin, emax = vm:read_from_map(pos1, pos2)
local a = VoxelArea:new{
MinEdge = emin,
MaxEdge = emax
}
local data = vm:get_data()
-- modifica i dati
for z = pos1.z, pos2.z do
for y = pos1.y, pos2.y do
for x = pos1.x, pos2.x do
local idx = a:index(x, y, z)
if data[idx] == c_erba then
data[idx] = c_terra
end
end
end
end
-- scrive i dati
vm:set_data(data)
vm:write_to_map(true)
end
Il tuo turno
- Crea una funzione
rimpiazza_in_area(da, a, pos1, pos2)
, che sostituisce tutte le istanze dida
cona
nell’area data, doveda
ea
sono i nomi dei nodi; - Crea una funzione che ruota tutte le casse di 90°;
- Crea una funzione che usa un LVM per far espandere i nodi di muschio sui nodi di pietra e pietrisco confinanti. La tua implementazione fa espandere il muschio di più di un blocco alla volta? Se sì, come puoi prevenire ciò?