Defold 全面迈向 3D:技术解析与架构教程
概要
本文深入解析了 Defold 引擎向全功能 3D 开发的转型,探讨了其基于 Lua 的数据驱动 Rendering Pipeline 核心原理。通过实战代码展示了如何构建 3D Render Script、第一人称相机控制器及 GLSL Shader,并针对跨平台 Multiplayer 开发提出了性能优化与同步策略。文章还强调了结合 horizOn 等 Backend-as-a-Service 方案来降低 3D 游戏基础设施负担的重要性。
每个独立开发者都经历过被游戏引擎“背叛”的时刻。你从一个轻量级的 2D 概念开始,随着需求蔓延引入了 3D 元素,突然间,你的引擎构建体积爆炸、加载时间变长,WebGL 构建甚至因内存溢出而崩溃。像 Unity 和 Unreal 这样庞大的引擎在 AAA 级画质上表现惊人,但对于追求无缝跨平台分发(尤其是 WebGL 和移动端)的个人开发者来说,使用它们往往感觉像是开着坦克去超市购物。
于是 Defold 登场了。长期以来,Defold 被誉为极致快速、零冗余的 2D 引擎,工作室只需一套代码库即可发布到从 HTML5 到 Nintendo Switch 的所有平台。而现在,Defold 正式成熟,成为了一个完全胜任的 3D 游戏引擎。虽然它在底层技术上一直是在 3D 环境中渲染(通过正交相机投影平面),但最近的更新引入了专门的 3D 工具、完整的 glTF mesh 支持,以及针对纯 3D 开发优化的流程。
这是生态系统的一次重大转变。如果你在寻找一个能在几秒钟内完成编译、产出个位数 MB 级别的二进制文件,同时又能提供动态 3D 世界渲染能力的引擎,Defold 现在是顶尖的选择。在这篇 Defold 3D 游戏引擎教程和架构解析中,我们将深入探讨 Defold 的 3D Rendering Pipeline 如何工作,如何从零开始编写自定义 3D 相机脚本,以及开发轻量级 3D 游戏在 Networking 方面的 Backend 影响。
底层揭秘:Defold 3D Pipeline 的真相
要精通 Defold 的 3D 开发,你必须理解其渲染哲学。Defold 并不会像 Unreal Engine 那样为你提供预配置好的 PBR (Physically Based Rendering) Pipeline。相反,它提供了一个使用 Lua 编写的、高度优化且数据驱动的 render_script。
Defold 中屏幕上绘制的所有内容都由 render_script 处理。默认情况下,该脚本针对 2D 进行配置:它设置正交投影矩阵,根据 Z 值(深度)对 sprite 进行排序,并按从后到前的顺序进行绘制。要开启 Defold 的 3D 能力,我们必须重写此脚本,使用透视投影矩阵,启用硬件深度测试,并为 3D 模型定义自定义的 render predicates。
这种底层访问权是一把双刃剑。一方面,你必须写一些矩阵数学运算;另一方面,你拥有对 Draw Call 的绝对控制权,这让你能够针对极低端硬件优化渲染,而这在那些大而全的引擎中通常极其困难。
构建自定义 3D Render Script 架构
为了渲染真实的 3D 模型而不出现基于绘制顺序的错误重叠,我们需要启用深度缓冲区(Z-buffer)。以下是一个基础的 3D render script,用于替换 Defold 的默认 Pipeline。
-- main/3d_pipeline.render_script
function init(self)
-- 定义背景清除颜色 (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
}
-- 创建渲染谓词 (predicates)。'model' 是 3D mesh 的默认标签
self.predicates = {
model = render.predicate({"model"}),
gui = render.predicate({"gui"}),
text = render.predicate({"text"})
}
end
function update(self)
-- 1. 设置当前帧的渲染状态
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(self.clear_buffers)
-- 2. 配置 3D 相机投影
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 度视野 (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. 启用深度测试并绘制 3D 模型
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)
-- view matrix 通过相机脚本发送的消息传递
if self.view_matrix then
render.set_view(self.view_matrix)
render.draw(self.predicates.model)
end
-- 4. 在 3D 场景之上绘制 GUI (正交投影)
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
请注意我们在这里管理状态的显式程度:清除深度缓冲区,根据当前窗口尺寸计算透视矩阵,强制执行背面剔除以节省光栅化周期,最后在渲染 GUI 之前将投影矩阵切换回正交。这为你提供了一个稳健的分层渲染架构。
构建第一人称 3D 相机控制器
如果没有在空间中移动的相机,仅靠 render_script 无法展示太多内容。Defold 的运作高度依赖消息传递 (Message Passing) 架构。不同于相机可能直接调用 transform.Translate() 的面向对象引擎,在 Defold 中,我们的相机脚本将计算其 view matrix 并将其分派给刚才编写的 render_script。
让我们构建一个处理鼠标视角(Pitch 和 Yaw)以及键盘移动(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
-- 隐藏并锁定鼠标光标
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()
-- 应用移动速度
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
-- 计算 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)
-- 将计算好的 view matrix 发送给渲染管线
msg.post("@render:", "set_view_matrix", { matrix = view_matrix })
-- 为下一帧重置速度
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
-- 限制 pitch 以防止相机翻转
self.pitch = math.max(-89, math.min(89, self.pitch))
-- 将欧拉角转换为四元数 (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
该脚本捕捉鼠标 delta 移动来调整 Pitch 和 Yaw,并将这些欧拉角转换为稳健的四元数旋转。随后,它直接从该旋转中推导出前向和右向向量,确保按下 “W” 键时始终朝着当前观察的方向移动。
3D 资源集成与自定义 Shader
随着 Defold 最近的更新,将 3D 资源引入项目变得非常简单。引擎原生支持 .gltf 和 .glb 格式,这已成为 Web 和轻量级游戏开发的行业标准。
然而,渲染 mesh 需要材质,而材质需要 Shader。默认情况下,Defold 包含基础材质,但编写自己的 GLSL Shader 可以赋予你脱颖而出所需的视觉独特性。让我们编写一个快速的、无光照的贴图 Shader,它针对移动端或 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()
{
// 计算顶点的最终屏幕空间位置
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()
{
// 采样纹理并乘以颜色 tint
lowp vec4 tex_color = texture2D(texture_sampler, var_texcoord0.xy);
gl_FragColor = tex_color * tint;
}
在你的 Defold 材质文件中,你将 view_proj uniform 映射到引擎内置的 view-projection 矩阵,并将 texture_sampler 映射到 mesh 的漫反射纹理。由于这些 Shader 不计算动态光照或阴影贴图,它们的运行速度极快,让你能轻松在低端硬件上维持 60 FPS。
处理 3D Multiplayer 与状态同步
当你从基于网格的 2D 游戏过渡到全 3D 环境时,联网架构的复杂性呈指数级增长。从 2D 转向 3D 意味着你的状态同步现在必须考虑 Z 轴深度、跨物理引擎的浮点数误差以及完整的四元数旋转。如果处理不当,你会遇到严重的视觉抖动——这是我们在分析 如何解决 UEFN 和 Unreal Engine 多人游戏中的玩家位置同步问题 时探讨过的常见问题。
由于 Defold 是轻量级引擎,它没有像 Unreal Engine 的 RPC 那样笨重的内置同步系统。你需要负责高效地打包状态数据。
依靠 REST API 同步 3D 位置会瞬间造成游戏循环的瓶颈。相反,你需要持久的、双向的连接。虽然我们之前的指南涵盖了如何 告别 HTTP 轮询:用于实时后端的 Unreal Engine WebSocket 教程,但同样的架构原则也直接适用于 Defold 的基于 Lua 的 WebSocket 扩展。
以下是一个如何序列化 3D 变换数据以最小化 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)
-- 将负载压缩至绝对最小所需数据
-- 我们将坐标舍入到小数点后两位以节省带宽
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,
-- 四元数需要所有 4 个分量以实现精确重建
qx = rot.x,
qy = rot.y,
qz = rot.z,
qw = rot.w
}
return json.encode(payload)
end
通过对浮点数进行舍入并仅打包核心数据,你可以防止网络缓冲区在快速移动更新期间溢出。管理这种数据流是实现流畅跨平台竞技的关键。
跨平台 3D 的基础设施负担
在 Defold 中编写一个高度优化的 3D 游戏客户端是非常令人满足的。引擎不会干扰你的思路,编译只需几毫秒,让你能专注于逻辑。
然而,一旦你决定让这款 3D 游戏支持 Multiplayer、加入云存档或实现跨平台排行榜,你的重点会立即从游戏开发转向服务器编排。你突然发现自己开始编写 Dockerfile,配置 Kubernetes 集群,为了 Session 状态与 Redis 实例纠缠,并试图保护你的 WebSocket 网关免受 DDoS 攻击。
靠自己构建这些需要设置负载均衡器、数据库分片和 SSL 证书管理——光是运行一个可靠的原型就可能需要 4 到 6 周的工作量。
通过 horizOn,这些 Backend 服务已经预先配置完成,让你只需交付游戏而非基础设施。horizOn 为用户身份验证、实时数据库同步和服务器权威逻辑提供原生集成,完美弥补了像 Defold 这种不带专用 Backend 生态系统的轻量级引擎的短板。你可以保持轻量客户端引擎的速度,同时将繁重的服务器架构工作外包出去。
Defold 3D 开发最佳实践
如果你计划在 Defold 中构建一个具有商业潜力的 3D 项目,请严格遵守以下架构准则:
- 保持几何体高度优化: Defold 专为速度而设计。为了保持其轻量优势,请将每个场景的层级几何体控制在 100,000 个多边形以内,尤其是针对 HTML5/WebGL 时。使用烘焙法线贴图而非高密度 mesh 来模拟细节。
- 利用渲染谓词实现 Frustum Culling: Defold 不会自动剔除 3D 空间中相机视野外的物体。你必须在 Lua 中编写自定义的 Frustum Culling 逻辑,动态禁用越界物体的模型组件以节省光栅化时间。
- 通过合图 (Atlasing) 合并 Draw Call: 每个独特的材质和纹理都需要向 GPU 发送单独的 Draw Call。将纹理合并到大型纹理合图中。如果 10 个不同的 3D 模型共享相同的材质和合图,Defold 在底层可以更高效地批量处理它们。
- 预计算复杂数学: 矩阵乘法和四元数转换在 Lua 中是开销很高的操作。缓存你的前向和右向向量,仅在玩家旋转真正改变时才重新计算,而不是在每一帧无条件进行繁重的数学运算。
- 将逻辑与渲染频率解耦: 你的游戏逻辑 (
update) 可能运行在 60 FPS,但自定义物理或联网步骤可能以 30 FPS 的频率 tick。根据速度插值 (Interpolate) 你的 3D 视觉位置,而不是直接对齐到最新状态,以确保在不同显示器刷新率上渲染流畅。 - 管理 Lua Garbage Collection: 在动态 3D 环境中,你会频繁创建和销毁向量对象及矩阵。如果管理不当,Lua 的 Garbage Collection 可能会导致明显的掉帧。尽量通过直接更新内部值来复用
vmath.vector3和vmath.matrix4实例,而不是在update循环内部实例化局部变量。为子弹和实体预分配内存池。 - 在外部烘焙光照: 考虑到自定义 GLSL Shader 中的动态光照在移动端会迅速消耗性能预算,请在导出 glTF 模型之前,使用 Blender 或 Maya 直接将全局照明和环境光遮蔽 (AO) 烘焙到纹理中。一个带有精美烘焙光照的简单无光照 Shader 在移动端 Web 浏览器上的表现永远优于复杂的动态 Shader。
总结
Defold 向功能完备的 3D 游戏引擎进化是独立开发者的重大胜利。它成功保留了闪电般的编译速度和极小的二进制占用,同时提供了构建宏大且迷人的 3D 世界所需的数学基础和工具。通过掌握自定义渲染脚本、理解矩阵运算以及高效序列化网络数据,你可以构建在技术层面足以与笨重引擎竞争的跨平台作品。
当你准备好将高度优化的 3D 客户端上线,并在不被基础设施管理困扰的情况下扩展多人游戏 Backend 时,可以免费试用 horizOn 或查阅 API 文档,了解如何快速将实时服务集成到你的下一个 Defold 项目中。