Defold diventa completamente 3D: Analisi Tecnica e Tutorial sull'Architettura
In breve
Defold si evolve in un engine 3D completo, offrendo agli sviluppatori un controllo totale sulla Rendering Pipeline tramite Lua e shader personalizzati. L'articolo approfondisce l'architettura tecnica necessaria per gestire camera controller, asset glTF e la State Synchronization in contesti Multiplayer. Viene inoltre evidenziato come l'integrazione con horizOn possa semplificare drasticamente la gestione dell'infrastruttura Backend per progetti 3D Cross-Platform.
Ogni indie developer conosce il momento in cui il proprio game engine lo tradisce. Inizi con un concept 2D leggero, lo scope creep introduce elementi 3D e improvvisamente la build size del tuo engine esplode, i tempi di caricamento rallentano e le tue build web vanno in crash per mancanza di memoria. Engine massicci come Unity e Unreal sono fenomenali per la fedeltà AAA, ma per i solo developers che mirano a una distribuzione cross-platform fluida — specialmente WebGL e mobile — spesso sembrano come guidare un carro armato per andare a fare la spesa.
Ecco Defold. Storicamente acclamato come un engine 2D ultra-veloce e zero-bloat, utilizzato dagli studi per raggiungere ogni piattaforma, da HTML5 a Nintendo Switch con un singolo codebase, Defold è ufficialmente cresciuto. Ora è un game engine 3D a tutti gli effetti. Sebbene tecnicamente abbia sempre renderizzato in un contesto 3D sotto il cofano (proiettando piani piatti tramite una camera ortografica), i recenti aggiornamenti hanno introdotto strumenti 3D dedicati, supporto completo per mesh glTF e un workflow ottimizzato per il vero sviluppo 3D.
Questo rappresenta un cambiamento enorme per l'ecosistema. Se cerchi un engine che compili in pochi secondi, produca binari di pochi megabyte e ti dia comunque il potere di renderizzare mondi 3D dinamici, Defold è ora un concorrente top-tier. In questo tutorial sul 3D game engine Defold e analisi dell'architettura, approfondiremo come funziona la Rendering Pipeline 3D di Defold, come scriptare una 3D camera personalizzata da zero e le implicazioni backend del networking di un gioco 3D leggero.
Sotto il cofano: La realtà della Rendering Pipeline 3D di Defold
Per capire come padroneggiare Defold in 3D, devi comprendere la sua filosofia di rendering. Defold non ti fornisce una Rendering Pipeline PBR (Physically Based Rendering) pre-configurata out of the box come fa Unreal Engine. Al contrario, fornisce uno render_script altamente ottimizzato e data-driven scritto in Lua.
Tutto ciò che viene disegnato sullo schermo in Defold è gestito da un render_script. Per impostazione predefinita, questo script è configurato per il 2D. Imposta una matrice di proiezione ortografica, ordina gli sprite in base al loro valore Z (depth) e li disegna dal fondo verso il primo piano. Per sbloccare le funzionalità 3D di Defold, dobbiamo riscrivere questo script per utilizzare una matrice di proiezione prospettica, abilitare l'hardware depth testing e definire custom render predicates per i nostri modelli 3D.
Questo accesso di basso livello è un'arma a doppio taglio. Da un lato, devi scrivere un po' di matematica delle matrici. Dall'altro, hai il controllo assoluto sulle tue draw calls, permettendoti di ottimizzare il rendering per hardware di fascia ultra-bassa in modi che gli engine monolitici rendono incredibilmente difficili.
Architettare il tuo Custom 3D Render Script
Per renderizzare veri modelli 3D senza che si sovrappongano in modo errato in base all'ordine di disegno, dobbiamo abilitare il depth buffer (Z-buffer). Ecco un render script 3D fondamentale che sostituisce la pipeline predefinita di Defold.
-- main/3d_pipeline.render_script
function init(self)
-- Definisce il colore di sfondo (RGBA)
self.clear_color = vmath.vector4(0.1, 0.1, 0.12, 1.0)
self.clear_buffers = {
[render.BUFFER_COLOR_BIT] = self.clear_color,
[render.BUFFER_DEPTH_BIT] = 1.0,
[render.BUFFER_STENCIL_BIT] = 0
}
-- Crea i render predicates. 'model' è il tag predefinito per le mesh 3D
self.predicates = {
model = render.predicate({"model"}),
gui = render.predicate({"gui"}),
text = render.predicate({"text"})
}
end
function update(self)
-- 1. Setup dello stato di rendering per il frame
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(self.clear_buffers)
-- 2. Configura la 3D Camera Projection
local window_width = render.get_window_width()
local window_height = render.get_window_height()
if window_width == 0 or window_height == 0 then return end
local aspect_ratio = window_width / window_height
local fov = math.rad(60) -- 60 gradi di Field of View
local near_z = 0.1
local far_z = 1000.0
local proj_matrix = vmath.matrix4_perspective(fov, aspect_ratio, near_z, far_z)
render.set_projection(proj_matrix)
-- 3. Disegna i modelli 3D con Depth Testing abilitato
render.set_depth_test(render.COMPARE_LEQUAL)
render.set_cull_face(render.FACE_BACK)
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
-- La view matrix viene passata tramite messaggi dal nostro script della camera
if self.view_matrix then
render.set_view(self.view_matrix)
render.draw(self.predicates.model)
end
-- 4. Disegna la GUI sopra la scena 3D (Ortografica)
render.set_depth_mask(false)
render.set_depth_test(render.COMPARE_ALWAYS)
local gui_proj = vmath.matrix4_orthographic(0, window_width, 0, window_height, -1, 1)
render.set_projection(gui_proj)
render.set_view(vmath.matrix4())
render.draw(self.predicates.gui)
render.draw(self.predicates.text)
end
function on_message(self, message_id, message)
if message_id == hash("set_view_matrix") then
self.view_matrix = message.matrix
end
end
Nota quanto esplicitamente stiamo gestendo lo stato qui. Puliamo il depth buffer, calcoliamo la matrice prospettica in base alle dimensioni correnti della finestra, forziamo il back-face culling per risparmiare cicli di rasterizzazione e, infine, riportiamo la matrice di proiezione in modalità ortografica prima di renderizzare la GUI. Questo ti offre una robusta architettura di rendering a pipeline divisa.
Costruire un First-Person 3D Camera Controller
Un render script da solo non mostrerà molto senza una camera che si muove nello spazio. Defold opera pesantemente su un'architettura di Message Passing. A differenza degli engine object-oriented dove la camera potrebbe chiamare direttamente transform.Translate(), in Defold il nostro script della camera calcolerà la sua view matrix e la invierà al render script che abbiamo appena scritto.
Costruiamo un classico First-Person camera controller che gestisce il mouse-look (pitch e yaw) e il movimento da tastiera (WASD).
-- scripts/camera_controller.script
go.property("mouse_sensitivity", 0.2)
go.property("move_speed", 10.0)
function init(self)
msg.post(".", "acquire_input_focus")
self.pitch = 0
self.yaw = 0
-- Nasconde e cattura il cursore del mouse
window.set_mouse_lock(true)
self.forward = vmath.vector3(0, 0, -1)
self.right = vmath.vector3(1, 0, 0)
self.up = vmath.vector3(0, 1, 0)
self.velocity = vmath.vector3(0)
end
function update(self, dt)
local pos = go.get_position()
-- Applica la velocità di movimento
if vmath.length_sqr(self.velocity) > 0 then
local move_dir = vmath.normalize(self.velocity)
pos = pos + move_dir * self.move_speed * dt
go.set_position(pos)
end
-- Calcola la view matrix
local rotation = go.get_rotation()
self.forward = vmath.rotate(rotation, vmath.vector3(0, 0, -1))
local target = pos + self.forward
local view_matrix = vmath.matrix4_look_at(pos, target, self.up)
-- Invia la view matrix calcolata alla rendering pipeline
msg.post("@render:", "set_view_matrix", { matrix = view_matrix })
-- Resetta la velocità per il frame successivo
self.velocity = vmath.vector3(0)
end
function on_input(self, action_id, action)
if action_id == hash("mouse_moved") then
self.yaw = self.yaw - action.dx * self.mouse_sensitivity
self.pitch = self.pitch + action.dy * self.mouse_sensitivity
-- Limita il pitch per evitare che la camera si ribalti
self.pitch = math.max(-89, math.min(89, self.pitch))
-- Converte gli angoli di Eulero in un Quaternione
local rot_y = vmath.quat_rotation_y(math.rad(self.yaw))
local rot_x = vmath.quat_rotation_x(math.rad(self.pitch))
go.set_rotation(rot_y * rot_x)
elseif action_id == hash("move_forward") then
self.velocity = self.velocity + self.forward
elseif action_id == hash("move_backward") then
self.velocity = self.velocity - self.forward
elseif action_id == hash("move_left") then
self.right = vmath.cross(self.forward, self.up)
self.velocity = self.velocity - self.right
elseif action_id == hash("move_right") then
self.right = vmath.cross(self.forward, self.up)
self.velocity = self.velocity + self.right
end
end
Questo script cattura i movimenti delta del mouse per regolare pitch e yaw, convertendo quegli angoli di Eulero in una robusta rotazione tramite Quaternione. Deriva poi i vettori forward e right direttamente da quella rotazione per garantire che premere "W" ti muova sempre nella direzione in cui stai guardando.
Integrazione di Asset 3D e Shader personalizzati
Con i recenti aggiornamenti di Defold, portare asset 3D nel tuo progetto è banale. L'engine supporta nativamente i formati .gltf e .glb, che sono diventati lo standard del settore per lo sviluppo web e di giochi leggeri.
Tuttavia, renderizzare una mesh richiede un materiale, e i materiali richiedono shader. Per impostazione predefinita, Defold include materiali di base, ma scrivere i tuoi shader GLSL ti dà la distintività visiva necessaria per distinguerti. Scriviamo un veloce shader textured unlit, perfettamente ottimizzato per target mobile o HTML5.
Il Vertex Shader (model.vp)
// model.vp
uniform highp mat4 view_proj;
uniform highp mat4 world;
attribute highp vec4 position;
attribute mediump vec2 texcoord0;
varying mediump vec2 var_texcoord0;
void main()
{
// Calcola la posizione finale screen-space del vertice
vec4 p = view_proj * world * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
gl_Position = p;
}
Il Fragment Shader (model.fp)
// model.fp
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D texture_sampler;
uniform lowp vec4 tint;
void main()
{
// Campiona la texture e moltiplica per una tinta di colore
lowp vec4 tex_color = texture2D(texture_sampler, var_texcoord0.xy);
gl_FragColor = tex_color * tint;
}
Nel tuo file materiale di Defold, mappi l'uniform view_proj alla view-projection matrix integrata nell'engine, e texture_sampler alla diffuse texture della tua mesh. Poiché questi shader non calcolano l'illuminazione dinamica o le shadow maps, girano in modo incredibilmente veloce, permettendoti di mantenere facilmente i 60 FPS su hardware di fascia bassa.
Gestione del Multiplayer 3D e della State Synchronization
Quando passi da un gioco 2D basato su griglia a un ambiente 3D completo, la complessità della tua architettura di networking aumenta esponenzialmente. Passare dal 2D al 3D significa che la tua State Synchronization deve ora tenere conto della profondità sull'asse Z, delle imprecisioni dei floating-point tra i physics engine e delle rotazioni complete tramite quaternioni. Se gestisci male questo aspetto, otterrai gravi stutter visivi — un problema comune che abbiamo esplorato analizzando How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer.
Poiché Defold è un engine leggero, non viene fornito con un pesante sistema di replicazione integrato come gli RPC di Unreal Engine. Sei responsabile dell'impacchettamento efficiente dei dati di stato.
Affidarsi alle REST API per sincronizzare le posizioni 3D causerà istantaneamente un collo di bottiglia nel tuo game loop. Al contrario, hai bisogno di connessioni persistenti e bidirezionali. Sebbene la nostra guida precedente abbia spiegato come Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends, gli stessi principi architetturali si applicano direttamente alle estensioni WebSocket basate su Lua di Defold.
Ecco un esempio di come dovresti serializzare i dati di transform 3D per minimizzare la dimensione del payload sui WebSocket:
-- scripts/network_sync.lua
local json = require "builtins.scripts.json"
function serialize_transform(go_id)
local pos = go.get_position(go_id)
local rot = go.get_rotation(go_id)
-- Comprime il payload al minimo indispensabile
-- Arrotondiamo la posizione a 2 decimali per risparmiare banda
local payload = {
id = hash_to_hex(go_id),
x = math.floor(pos.x * 100) / 100,
y = math.floor(pos.y * 100) / 100,
z = math.floor(pos.z * 100) / 100,
-- I quaternioni richiedono tutte e 4 le componenti per una ricostruzione accurata
qx = rot.x,
qy = rot.y,
qz = rot.z,
qw = rot.w
}
return json.encode(payload)
end
Arrotondando i float e impacchettando solo i dati essenziali, eviti che i tuoi network buffers vadano in overflow durante gli aggiornamenti rapidi di movimento. Gestire questo flusso è la chiave per un gioco cross-platform reattivo.
L'onere dell'infrastruttura per il 3D Cross-Platform
Scrivere un game client 3D altamente ottimizzato in Defold è immensamente soddisfacente. L'engine non intralcia il tuo cammino, compila in millisecondi e ti permette di concentrarti esclusivamente sulla logica.
Tuttavia, nel momento in cui decidi di rendere quel gioco 3D Multiplayer, aggiungere cloud saves o implementare classifiche cross-platform, il tuo focus si sposta immediatamente dallo sviluppo del gioco all'orchestrazione dei server. Ti ritrovi improvvisamente a scrivere Dockerfiles, configurare cluster Kubernetes, lottare con istanze Redis per lo stato delle sessioni e cercare di proteggere i tuoi gateway WebSocket dagli attacchi DDoS.
Costruire tutto questo da soli richiede l'impostazione di load balancers, sharding del database e gestione dei certificati SSL — facilmente 4-6 settimane di lavoro solo per avere un prototipo affidabile funzionante.
Con horizOn, questi servizi Backend sono pre-configurati, permettendoti di pubblicare il tuo gioco invece della tua infrastruttura. horizOn fornisce integrazioni native per l'autenticazione utente, la sincronizzazione del database in real-time e la logica server-authoritative, colmando perfettamente il divario per engine leggeri come Defold che non vengono distribuiti con ecosistemi backend proprietari. Riesci a mantenere la velocità di un minuscolo engine client delegando il lavoro pesante dell'architettura server.
Best Practice per lo sviluppo 3D in Defold
Se hai intenzione di costruire un progetto 3D commercialmente valido in Defold, attieniti rigorosamente a queste linee guida architetturali:
- Mantieni la geometria pesantemente ottimizzata: Defold è progettato per la velocità. Per mantenere il suo vantaggio in termini di leggerezza, mantieni la geometria dei tuoi livelli sotto i 100.000 poligoni totali per scena, specialmente se il target è HTML5/WebGL. Usa baked normal maps invece di mesh ad alta densità per simulare il dettaglio.
- Sfrutta i Render Predicates per il Frustum Culling: Defold non esegue automaticamente il culling degli oggetti che si trovano fuori dalla visuale della camera nello spazio 3D. Devi scrivere una logica di Frustum Culling personalizzata in Lua, disabilitando dinamicamente i componenti modello degli oggetti fuori dai limiti per risparmiare tempo di rasterizzazione.
- Consolida le Draw Calls tramite Atlasing: Ogni materiale e texture unico richiede una draw call separata inviata alla GPU. Combina le tue texture in grandi texture atlases. Se 10 diversi modelli 3D condividono esattamente lo stesso materiale e lo stesso atlas, Defold può raggrupparli (batch) in modo molto più efficiente sotto il cofano.
- Pre-calcola la matematica complessa: Le moltiplicazioni di matrici e le conversioni di quaternioni sono operazioni molto costose in Lua. Memorizza nella cache i tuoi vettori forward e right e ricalcolali solo quando la rotazione del giocatore cambia effettivamente, invece di eseguire calcoli pesanti incondizionatamente ad ogni singolo frame.
- Disaccoppia la logica dalla frequenza di rendering: La tua logica di gioco (
update) potrebbe girare a 60 FPS, ma i tuoi passaggi di fisica personalizzata o di networking potrebbero avere un tick a 30 FPS. Interpola le tue posizioni visive 3D in base alla velocità invece di farle scattare direttamente sull'ultimo stato per garantire un rendering fluido su monitor con diverse frequenze di aggiornamento. - Gestisci la Garbage Collection di Lua: In un ambiente 3D dinamico, crei e distruggi frequentemente oggetti vettoriali e matrici. Il Garbage Collector di Lua può causare picchi di frame evidenti se non gestito. Riutilizza le istanze di
vmath.vector3evmath.matrix4quando possibile aggiornando direttamente i loro valori interni, invece di istanziare nuove variabili locali all'interno del tuo loop diupdate. Pre-alloca memory pools per proiettili ed entità. - Esegui il Bake della tua illuminazione esternamente: Poiché l'illuminazione dinamica negli shader GLSL personalizzati consumerà rapidamente il tuo performance budget sui dispositivi mobile, esegui il bake della global illumination e dell'ambient occlusion direttamente nelle tue texture usando Blender o Maya prima di esportare i tuoi modelli glTF. Un semplice shader unlit con un'illuminazione magnificamente baked supererà sempre in prestazioni uno shader dinamico complesso sui browser web mobile.
Conclusione
L'evoluzione di Defold in un robusto game engine 3D è una vittoria enorme per gli sviluppatori indipendenti. Riesce a mantenere i suoi tempi di compilazione fulminei e i suoi binari incredibilmente piccoli offrendo al contempo le fondamenta matematiche grezze e gli strumenti necessari per costruire mondi 3D vasti e coinvolgenti. Padroneggiando i render script personalizzati, comprendendo le operazioni sulle matrici e serializzando in modo efficiente i dati di rete, puoi creare titoli cross-platform che competono tecnicamente con engine molto più grandi e pesanti.
Quando sarai pronto a portare online il tuo client 3D altamente ottimizzato e scalare il tuo Multiplayer Backend senza il mal di testa della gestione dell'infrastruttura grezza, prova horizOn gratuitamente o consulta la documentazione API per vedere quanto velocemente puoi integrare servizi real-time nel tuo prossimo progetto Defold.