Defold staje się w pełni 3D: Analiza techniczna i Tutorial architektury
W skrócie
Defold ewoluował z silnika 2D w potężne rozwiązanie 3D, oferując deweloperom ekstremalnie mały rozmiar binarek i błyskawiczną kompilację. Tutorial zgłębia tajniki własnych render_scripts, implementację kamery FPP oraz optymalizację shaderów GLSL pod kątem urządzeń mobilnych i WebGL. Dowiesz się również, jak skutecznie zarządzać architekturą Multiplayer i synchronizacją stanu przy użyciu platformy horizOn.
Każdy indie deweloper zna ten moment, w którym silnik gry go zawodzi. Zaczynasz od lekkiego konceptu 2D, gdy nagle rozrastający się zakres projektu wprowadza elementy 3D, a rozmiar buildu Twojego silnika eksploduje, czasy ładowania drastycznie się wydłużają, a buildy WebGL wyłączają się z powodu braku pamięci. Potężne silniki takie jak Unity czy Unreal są fenomenalne do osiągania wierności graficznej klasy AAA, ale dla deweloperów solo stawiających na płynną dystrybucję cross-platformową — zwłaszcza WebGL i mobile — często wydają się jak jazda czołgiem do sklepu spożywczego.
Czas na Defold. Historycznie kojarzony jako ultra-szybki, pozbawiony zbędnego balastu silnik 2D używany przez studia do trafiania na każdą platformę, od HTML5 po Nintendo Switch za pomocą jednego codebase, Defold oficjalnie wydoroślał. Jest teraz w pełni funkcjonalnym silnikiem gier 3D. Choć technicznie zawsze renderował w kontekście 3D „pod maską” (rzutując płaskie powierzchnie przez ortograficzną kamerę), ostatnie aktualizacje wprowadziły dedykowane narzędzia 3D, pełne wsparcie dla meshy glTF oraz zoptymalizowany workflow dla prawdziwego rozwoju 3D.
To ogromna zmiana dla ekosystemu. Jeśli szukasz silnika, który kompiluje się w sekundy, produkuje jednocyfrowe (w megabajtach) binarki, a jednocześnie daje Ci moc renderowania dynamicznych światów 3D, Defold jest obecnie topowym pretendentem. W tym tutorialu Defold 3D game engine i analizie architektury zagłębimy się w działanie Defold Rendering Pipeline, nauczymy się skryptować własną kamerę 3D od zera i omówimy konsekwencje sieciowania lekkiej gry 3D w warstwie Backend.
Pod maską: Rzeczywistość Defold 3D Pipeline
Aby zrozumieć, jak opanować Defold w 3D, musisz pojąć jego filozofię renderowania. Defold nie daje Ci gotowego, prekonfigurowanego PBR (Physically Based Rendering) pipeline prosto z pudełka, jak robi to Unreal Engine. Zamiast tego oferuje wysoko zoptymalizowany, oparty na danych render script napisany w Lua.
Wszystko, co jest rysowane na ekranie w Defold, jest obsługiwane przez render_script. Domyślnie skrypt ten jest skonfigurowany pod 2D. Ustawia ortograficzną macierz projekcji, sortuje sprite'y według ich wartości Z (depth) i rysuje je od tyłu do przodu. Aby odblokować możliwości 3D w Defold, musimy przepisać ten skrypt, wykorzystując perspektywiczną macierz projekcji, włączyć sprzętowy depth testing i zdefiniować własne render predicates dla naszych modeli 3D.
Ten niskopoziomowy dostęp to miecz obosieczny. Z jednej strony musisz napisać trochę matematyki macierzowej. Z drugiej strony masz absolutną kontrolę nad swoimi draw calls, co pozwala na optymalizację renderowania dla ultra-słabego sprzętu w sposób, który w monolitycznych silnikach jest niezwykle trudny.
Architektura Twojego własnego 3D Render Script
Aby renderować prawdziwe modele 3D bez ich nieprawidłowego nakładania się na podstawie kolejności rysowania, musimy włączyć bufor głębi (Z-buffer). Oto fundament 3D render script, który zastępuje domyślny pipeline Defold.
-- main/3d_pipeline.render_script
function init(self)
-- Define the background clear color (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
}
-- Create render predicates. 'model' is the default tag for 3D meshes
self.predicates = {
model = render.predicate({"model"}),
gui = render.predicate({"gui"}),
text = render.predicate({"text"})
}
end
function update(self)
-- 1. Setup the rendering state for the frame
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(self.clear_buffers)
-- 2. Configure the 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 degree 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. Draw 3D Models with Depth Testing enabled
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)
-- The view matrix is passed via messages from our camera script
if self.view_matrix then
render.set_view(self.view_matrix)
render.draw(self.predicates.model)
end
-- 4. Draw GUI over the 3D scene (Orthographic)
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
Zauważ, jak jawnie zarządzamy tutaj stanem. Czyścimy bufor głębi, obliczamy macierz perspektywy na podstawie aktualnych wymiarów okna, wymuszamy back-face culling, aby oszczędzać cykle rasteryzacji, i wreszcie przełączamy macierz projekcji z powrotem na ortograficzną przed renderowaniem GUI. Daje to solidną architekturę typu split-pipeline.
Budowa kontrolera kamery First-Person 3D
Sam render script niewiele pokaże bez kamery poruszającej się w przestrzeni. Defold opiera się w dużej mierze na architekturze Message Passing. W przeciwieństwie do silników obiektowych, gdzie kamera mogłaby bezpośrednio wywołać transform.Translate(), w Defold nasz skrypt kamery obliczy swoją macierz widoku (view matrix) i wyśle ją do napisanego właśnie render script.
Zbudujmy standardową kamerę First-Person, która obsługuje mouse-look (pitch i yaw) oraz poruszanie się za pomocą klawiatury (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
-- Hide and capture the mouse cursor
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()
-- Apply movement velocity
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
-- Calculate the 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)
-- Send the calculated view matrix to the render pipeline
msg.post("@render:", "set_view_matrix", { matrix = view_matrix })
-- Reset velocity for the next frame
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
-- Clamp pitch to prevent the camera from flipping over
self.pitch = math.max(-89, math.min(89, self.pitch))
-- Convert Euler angles to a Quaternion
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
Ten skrypt przechwytuje ruchy myszy (mouse delta), aby dostosować pitch i yaw, konwertując te Euler angles na solidną rotację typu Quaternion. Następnie wyprowadza wektory forward i right bezpośrednio z tej rotacji, zapewniając, że naciśnięcie „W” zawsze porusza Cię w kierunku, w którym aktualnie patrzysz.
Integracja zasobów 3D i własne Shadery
Dzięki ostatnim aktualizacjom Defold, wprowadzanie zasobów 3D do Twojego projektu jest banalne. Silnik natywnie wspiera formaty .gltf i .glb, które stały się standardem branżowym w tworzeniu gier webowych i lekkich projektów.
Jednak renderowanie mesha wymaga materiału, a materiały wymagają Shaderów. Domyślnie Defold zawiera podstawowe materiały, ale pisanie własnych Shaderów GLSL daje Ci wizualną oryginalność niezbędną do wyróżnienia się. Napiszmy szybki, unlit teksturowany Shader, który jest idealnie zoptymalizowany pod urządzenia mobilne lub HTML5.
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()
{
// Calculate the final screen-space position of the vertex
vec4 p = view_proj * world * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
gl_Position = p;
}
Fragment Shader (model.fp)
// model.fp
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D texture_sampler;
uniform lowp vec4 tint;
void main()
{
// Sample the texture and multiply by a color tint
lowp vec4 tex_color = texture2D(texture_sampler, var_texcoord0.xy);
gl_FragColor = tex_color * tint;
}
W pliku materiału Defold mapujesz uniform view_proj na wbudowaną macierz widoku-projekcji silnika, a texture_sampler na teksturę diffuse Twojego mesha. Ponieważ te Shadery nie obliczają dynamicznego oświetlenia ani shadow maps, działają niesamowicie szybko, pozwalając na łatwe utrzymanie 60 FPS na słabym sprzęcie.
Obsługa 3D Multiplayer i synchronizacja stanu
Kiedy przechodzisz z gry 2D opartej na siatce do pełnego środowiska 3D, złożoność Twojej architektury sieciowej rośnie wykładniczo. Przejście z 2D do 3D oznacza, że synchronizacja stanu musi teraz uwzględniać oś Z, niedokładności liczb zmiennoprzecinkowych w silnikach fizycznych oraz pełne rotacje Quaternion. Jeśli zrobisz to źle, skończysz z poważnym visual stutteringiem — powszechnym problemem, który badaliśmy analizując How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer.
Ponieważ Defold jest lekkim silnikiem, nie posiada ciężkiego, wbudowanego systemu replikacji, jak RPCs w Unreal Engine. Jesteś odpowiedzialny za efektywne pakowanie danych stanu.
Poleganie na REST APIs do synchronizacji pozycji 3D błyskawicznie stworzy wąskie gardło w Twojej pętli gry. Zamiast tego potrzebujesz trwałych, dwukierunkowych połączeń. Chociaż nasz poprzedni przewodnik opisywał, jak Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends, dokładnie te same zasady architektoniczne stosują się bezpośrednio do rozszerzeń WebSocket w Lua dla Defold.
Oto przykład, jak powinieneś serializować dane 3D transform, aby zminimalizować rozmiar payloadu przez WebSockets:
-- 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)
-- Compress the payload to the absolute minimum required data
-- We round position to 2 decimal places to save bandwidth
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,
-- Quaternions require all 4 components for accurate reconstruction
qx = rot.x,
qy = rot.y,
qz = rot.z,
qw = rot.w
}
return json.encode(payload)
end
Zaokrąglając liczby zmiennoprzecinkowe i pakując tylko niezbędne dane, zapobiegasz przepełnieniu buforów sieciowych podczas gwałtownych aktualizacji ruchu. Zarządzanie tym przepływem jest kluczem do responsywnej rozgrywki cross-platformowej.
Ciężar infrastruktury w Cross-Platform 3D
Pisanie wysoce zoptymalizowanego klienta gry 3D w Defold daje ogromną satysfakcję. Silnik nie wchodzi Ci w drogę, kompiluje się w milisekundach i pozwala skupić się wyłącznie na logice.
Jednak w momencie, gdy zdecydujesz się uczynić tę grę 3D produkcją typu Multiplayer, dodać zapisy w chmurze lub zaimplementować cross-platformowe rankingi, Twoja uwaga natychmiast przesuwa się z tworzenia gry na orkiestrację serwerów. Nagle piszesz Dockerfiles, konfigurujesz klastry Kubernetes, walczysz z instancjami Redis dla sesji i próbujesz zabezpieczyć bramy WebSocket przed atakami DDoS.
Samodzielne zbudowanie tego wymaga ustawienia Load Balancerów, shardingu baz danych i zarządzania certyfikatami SSL — to łatwo 4-6 tygodni pracy tylko po to, by uruchomić wiarygodny prototyp.
Dzięki horizOn, te usługi Backend są prekonfigurowane, pozwalając Ci dostarczyć grę zamiast infrastruktury. horizOn zapewnia natywne integracje dla uwierzytelniania użytkowników, synchronizacji bazy danych w czasie rzeczywistym i logiki server-authoritative, idealnie wypełniając lukę dla lekkich silników takich jak Defold, które nie są dostarczane z własnymi ekosystemami serwerowymi. Zachowujesz szybkość małego silnika po stronie klienta, zlecając najtrudniejsze zadania architektury serwerowej na zewnątrz.
Dobre praktyki dla Defold 3D Development
Jeśli planujesz budowę komercyjnego projektu 3D w Defold, trzymaj się ściśle tych wytycznych architektonicznych:
- Utrzymuj geometrię silnie zoptymalizowaną: Defold jest zaprojektowany pod kątem szybkości. Aby zachować tę przewagę, utrzymuj geometrię poziomów poniżej 100 000 poligonów na scenę, zwłaszcza jeśli celujesz w HTML5/WebGL. Używaj baked normal maps zamiast gęstych meshy do symulacji szczegółów.
- Wykorzystuj Render Predicates do Frustum Culling: Defold nie odcina automatycznie obiektów znajdujących się poza widokiem kamery w przestrzeni 3D. Musisz napisać własną logikę Frustum Culling w Lua, dynamicznie wyłączając komponenty modeli obiektów będących poza zasięgiem, aby oszczędzić czas rasteryzacji.
- Konsoliduj Draw Calls poprzez Atlasing: Każdy unikalny materiał i tekstura wymaga oddzielnego draw call wysyłanego do GPU. Łącz swoje tekstury w duże atlasy tekstur. Jeśli 10 różnych modeli 3D dzieli ten sam materiał i atlas, Defold może je znacznie wydajniej batchować.
- Wstępne obliczanie złożonej matematyki: Mnożenie macierzy i konwersje Quaternion są kosztownymi operacjami w Lua. Cache’uj swoje wektory forward i right i przeliczaj je tylko wtedy, gdy rotacja gracza faktycznie się zmienia, zamiast wykonywać ciężką matematykę bezwarunkowo w każdej klatce.
- Oddziel logikę od częstotliwości renderowania: Twoja logika gry (
update) może działać w 60 FPS, ale Twoja fizyka lub kroki sieciowe mogą tykać w 30 FPS. Interpoluj wizualne pozycje 3D na podstawie prędkości (velocity), zamiast przeskakiwać bezpośrednio do najnowszego stanu, aby zapewnić maślany smooth rendering na monitorach o różnej częstotliwości odświeżania. - Zarządzaj Lua Garbage Collection: W dynamicznym środowisku 3D często tworzysz i niszczysz obiekty wektorów i macierzy. Garbage Collection w Lua może powodować zauważalne spadki klatek, jeśli pozostanie bez nadzoru. Reużywaj instancje
vmath.vector3ivmath.matrix4, aktualizując ich wewnętrzne wartości bezpośrednio, zamiast instancjonować nowe zmienne lokalne wewnątrz pętliupdate. Pre-alokuj memory pools dla pocisków i encji. - Bake’uj oświetlenie zewnętrznie: Ponieważ dynamiczne oświetlenie we własnych Shaderach GLSL szybko zje Twój budżet wydajności na urządzeniach mobilnych, bake’uj global illumination i ambient occlusion bezpośrednio w teksturach za pomocą Blendera lub Maya przed eksportem modeli glTF. Prosty unlit Shader z pięknie wypieczonym oświetleniem zawsze wygra z kompleksem dynamicznych Shaderów w mobilnych przeglądarkach.
Podsumowanie
Ewolucja Defold w solidny silnik gier 3D to ogromna wygrana dla niezależnych deweloperów. Skutecznie zachowuje on swoje błyskawiczne czasy kompilacji i niesamowicie mały rozmiar binarek, oferując jednocześnie fundamenty matematyczne i narzędzia wymagane do budowania rozległych, angażujących światów 3D. Opanowując własne render scripts, rozumiejąc operacje na macierzach i wydajnie serializując dane sieciowe, możesz budować tytuły cross-platformowe, które technicznie konkurują z znacznie większymi, przeładowanymi silnikami.
Kiedy będziesz gotowy, by wyprowadzić swojego zoptymalizowanego klienta 3D do sieci i skalować Multiplayer Backend bez bólu głowy związanego z zarządzaniem surową infrastrukturą, wypróbuj horizOn za darmo lub sprawdź dokumentację API, aby zobaczyć, jak szybko możesz zintegrować usługi czasu rzeczywistego w swoim następnym projekcie Defold.