引言
游戏的世界坐标与屏幕坐标之间隔着「相机」——相机的投影矩阵决定玩家能看到世界的哪一块、以什么视角看。本文把 Defold 相机讲透:先讲相机组件与两种投影(正交/透视)的差别,再深入投影矩阵与视口/屏幕坐标换算,接着讲「平滑跟随」(追尾相机)的 lerp 实现与防抖,再讲屏幕震动与击退这些「手感」效果,然后是多相机分层(背景层/主世界/UI 层)与切换,最后讲渲染脚本如何与相机耦合、以及横版/俯视/缩放三类典型相机模式。
前置:/defold-3d-games/(投影与 3D 基础)、/defold-script-system-lua/(消息与协程)、/defold-shader-postprocessing/(渲染管线)。矩阵基础见 [[cs-fundamentals]]。
目录
- 1. 相机是什么:世界到屏幕的映射
- 2. 相机组件与投影:正交 vs 透视
- 3. 投影矩阵与视口
- 4. 屏幕坐标与世界坐标互转
- 5. 平滑跟随:追尾相机的 lerp 实现
- 6. 屏幕震动与击退:手感效果
- 7. 多相机分层与切换
- 8. 渲染脚本与相机耦合
- 9. 典型相机模式:横版俯视缩放
- 10. 速查表
- 延伸阅读
1. 相机是什么:世界到屏幕的映射
相机 = 一个「看世界的窗口」,它决定三件事:
看哪里 → 相机位置 + 朝向(world 变换)
看多大 → 投影(正交:视野矩形;透视:FOV)
怎么看 → 视口(屏幕上的显示区域)
游戏里相机的三大职责:
① 跟随玩家:让角色保持在画面中合理位置
② 表达叙事:运镜(缩放、震动、切镜)传达情绪
③ 划分层级:背景层/主世界/UI 分属不同相机
Defold 的相机模型:
Defold 本身没有「相机组件」(默认摄像机 = 无投影变换的全屏视口)
→ 需要自己实现:camera 组件 + 设置投影矩阵(go.set / 渲染脚本)
→ 自由度大:正交、透视、混合都可行
心智:相机决定「看哪里、看多大、怎么看」——Defold 不内置相机,投影矩阵自己算,自由度即是责任。
2. 相机组件与投影:正交 vs 透视
Defold 中的相机 = 一个 camera 组件 + 你在代码里设置的投影:
-- 创建相机组件
local camera_id = factory.create("main/camera") -- 含 camera 组件
-- 设置正交投影(2D 游戏标配)
local w, h = 960, 540 -- 视口逻辑尺寸
local near, far = -1, 1
go.set("#camera", "orthographic_zoom", 1) -- 缩放
msg.post("#camera", "set_orthographic_projection", {zoom = 1})
两种投影的选择:
| 维度 | 正交投影 | 透视投影 |
|---|---|---|
| 效果 | 平行投影,无近大远小 | 有近大远小,纵深真实 |
| 适用 | 2D 游戏、UI、策略 | 3D 场景、俯视 3D |
| 数学 | 矩形裁剪体 | 视锥体(FOV + 近远平面) |
Defold 内置两种投影消息:
-- 正交投影(2D)
msg.post("#camera", "set_orthographic_projection", {zoom = 1})
-- 透视投影(3D)
msg.post("#camera", "set_perspective_projection", {
fov = math.rad(45), near = 0.1, far = 1000 })
心智:2D 用正交(矩形裁剪体)、3D 用透视(视锥体)——Defold 通过投影消息切换,记住「2D 默认正交」。
3. 投影矩阵与视口
相机投影的本质是一系列矩阵变换:
世界坐标 → 视图矩阵(相机位置/朝向) → 视图空间
视图空间 → 投影矩阵(正交/透视) → 裁剪空间
裁剪空间 → 视口映射 → 屏幕坐标
正交投影矩阵(手动构造):
设可见矩形 [left, right] × [bottom, top]:
缩放 X:2 / (right - left)
缩放 Y:2 / (top - bottom)
平移:- (right + left) / (right - left),同理 Y
-- Defold 渲染脚本里构造正交投影(示意)
function build_ortho(left, right, bottom, top, near, far)
local zs = 2 / (right - left), zt = 2 / (top - bottom), zf = 2 / (far - near)
return vmath.matrix4(
zs, 0, 0, -(right + left) / (right - left),
0, zt, 0, -(top + bottom) / (top - bottom),
0, 0, zf, -(far + near) / (far - near),
0, 0, 0, 1)
end
视口 = 投影在屏幕上的「画布」:
- 视口可小于全屏(双人分屏、画中画)
- 视口改变 → 投影矩阵重设(宽高比变化影响可见范围)
- 宽高比适配:正交矩形按屏幕宽高比缩放,避免拉伸
心智:投影矩阵 = 缩放 + 平移把「可见矩形」映射到裁剪空间;视口是「画布」,宽高比变了要重算可见范围防拉伸。
4. 屏幕坐标与世界坐标互转
触摸/点击的屏幕坐标 → 世界坐标是每款游戏都要做的换算:
-- Defold:屏幕坐标 → 世界坐标
local screen_to_world = function(screen_x, screen_y)
-- 视口逻辑尺寸与相机矩形
local w, h = 960, 540
local cam = go.get("#camera", "position")
local left, bottom = cam.x - w/2, cam.y - h/2 -- 正交相机居中
return left + screen_x, bottom + screen_y
end
反向换算(世界 → 屏幕,用于 UI 指示/箭头):
local world_to_screen = function(wx, wy)
local w, h = 960, 540
local cam = go.get("#camera", "position")
return wx - cam.x + w/2, wy - cam.y + h/2
end
缩放对换算的影响:
- 相机 zoom ≠ 1 时,换算要除以 zoom
- 逻辑分辨率 vs 物理像素(DPR)→ 统一用逻辑坐标
- 旋转相机时换算要经过旋转矩阵(一般避免旋转相机)
实战:点击放置 + 相机跟随:
function on_input(self, action_id, action)
if action.pressed and action_id == hash("touch") then
local wx, wy = screen_to_world(action.x, action.y)
-- 放置/移动物体到 (wx, wy)
end
end
心智:屏幕↔世界互转 = 相机位置 + 半视口偏移 ± zoom 缩放——触摸/放置/箭头都靠它,别让 UI 层和世界层混用坐标。
5. 平滑跟随:追尾相机的 lerp 实现
相机直接「瞬移」到目标会抖——平滑跟随用插值:
-- 追尾相机:每帧向目标位置插值
function update(self, dt)
local target = go.get("#player", "position")
local cam = go.get("#camera", "position")
-- lerp 系数(帧率无关:1 - exp(-speed*dt))
local t = 1 - math.exp(-self.smooth_speed * dt) -- speed≈6~10
local nx = cam.x + (target.x - cam.x) * t
local ny = cam.y + (target.y - cam.y) * t
go.set("#camera", "position", vmath.vector3(nx, ny, cam.z))
end
防抖与手感要点:
- 帧率无关插值:用 1 - exp(-speed·dt),不用固定 t=0.1
- 速度调参:speed 低 → 拖尾感;高 → 贴脸
- 半程跟随时要「先慢后快」的缓动(ease-in-out)
- 边界钳制:不超出关卡边界(只跟到地图边)
带偏移的跟随(主角偏置):
-- 玩家偏置:让相机看「前方」多一些
local lead = vmath.vector3(60, 40, 0) -- 向右/向上偏置
go.set("#camera", "position", target + lead)
预测跟随(看向目标速度方向):
相机位置 = 目标位置 + 目标速度 × 预测时间
→ 高速移动时画面「跟上」而非「被甩在身后」
心智:平滑跟随 = 帧率无关插值 + 边界钳制 + 偏置/预测——相机贴太近会晕、太远会散,速度就是「手感」旋钮。
6. 屏幕震动与击退:手感效果
相机震动(shake)是「打击感」的免费放大器——伤害、爆炸、命中反馈都靠它:
-- 相机震动:衰减正弦
function update(self, dt)
if self.shake_time > 0 then
self.shake_time = self.shake_time - dt
local amp = self.shake_amp * (self.shake_time / self.shake_dur)
local sx = math.sin(self.shake_time * 60) * amp -- 高频抖动
local sy = math.cos(self.shake_time * 47) * amp -- 双轴错频更自然
-- 叠加到相机位置(基础跟随 + 震动偏移)
end
end
-- 触发震动
function add_shake(self, amp, dur)
self.shake_amp, self.shake_time, self.shake_dur = amp, dur, dur
end
震动参数的三要素:
幅度 amp:越小越「质感」,越大越「爆炸」
时长 dur:0.1~0.4s 短促、0.5s+ 强震
频率 freq:高频(>50Hz)细抖、低频(20~30Hz)重晃
击退(recoil)/ 缩放脉冲(zoom punch):
- 击退:命中瞬间相机向「受力反方向」快速偏移再回弹
- zoom punch:命中瞬间相机 zoom 突然放大 5%~10% 再回弹
- 技巧:都用一个「快速起、指数衰减」的冲击响应
震动叠加进跟随:
local cam = follow_position() + shake_offset() -- 跟随 + 震动叠加
go.set("#camera", "position", cam)
心智:屏幕震动 = 衰减正弦叠加 + 双轴错频——「快起慢衰减」的冲击响应是手感核心,别让它盖过跟随逻辑。
7. 多相机分层与切换
一个相机不够用——分层是 2D 游戏的标配:
三层相机方案:
背景层相机:远景、视差背景(固定)
主世界相机:跟随玩家的场景
UI 相机:HUD、菜单(不跟随,固定全屏)
为什么分层:不同层「跟随方式」不同,混在一个相机里做不到
视差背景(Parallax)的实现:
-- 背景层相机按「玩家速度的比例」移动
-- 背景 = 主相机位移 × parallax_factor(如 0.2/0.5/1.0)
function update(self, dt)
local player = go.get("#player", "position")
local bg = vmath.vector3(player.x * 0.3, player.y * 0.3, 0)
go.set("#bg_camera", "position", bg) -- 远景跟得慢
end
相机切换(过场/切镜):
- 切换 = 改变「活跃相机」的投影与跟随目标
- 过场动画:相机沿路径移动(缓动到固定机位)
- 安全做法:所有相机常驻,用「enable/disable」控制,避免重建
多相机的渲染注意:
- 每层相机对应一个渲染「pass」,用 render 脚本排序
- 层之间要处理遮挡:UI 层最后画
- 背景层不响应输入(输入分发要按层过滤)
心智:多相机分层 = 背景视差 + 主世界跟随 + UI 固定,过场靠切换活跃相机——常驻相机用 enable 控制,别反复重建。
8. 渲染脚本与相机耦合
Defold 里相机的真正「生效」发生在渲染脚本(render.render):
-- render/render.render 片段
function render(self)
-- 主世界 pass:设置相机投影 + 世界变换
render.set_camera(render_camera) -- 绑定的相机组件
render.draw_world(...)
-- UI pass:切到全屏视口投影
render.set_camera(nil) -- 无相机 = 默认全屏
render.draw_ui(...)
end
相机数据如何流到渲染:
相机组件(go 位置/朝向/投影消息)
↓ render.set_camera 绑定
渲染脚本取出「视图 × 投影」矩阵
↓ 每帧传给 shader 的 matrix
GPU 用矩阵把顶点变换到屏幕
渲染脚本里动态改投影(视口/缩放):
-- 运行时改视口(画中画、分屏)
render.set_viewport(0, 0, w/2, h) -- 左半屏
render.set_viewport(w/2, 0, w/2, h) -- 右半屏
相机与 render 的协调铁律:
- 相机位置每帧 update 里设,渲染脚本每帧取用
- 改投影的消息要在「渲染帧前」发出(msg.post 到 camera)
- 分屏 = 同一个世界投影两次不同视口,别复制相机
心智:相机的「生效点」在渲染脚本——render.set_camera 绑定组件、set_viewport 切视口,update 设位置、渲染前发投影消息,两个世界靠矩阵耦合。
9. 典型相机模式:横版俯视缩放
三类游戏各有一种「默认相机手法」:
横版(Platformer):
- 正交投影,视野宽度固定(如 16:9 的 960×540)
- 相机只跟随 X(+ 少量 Y,保证跳跃可见)
- 边界钳制:左右不超过关卡长度
- 触发式镜头:走到检查点 → 镜头锁定一段(保持关卡构图)
俯视(Top-down):
- 相机跟随 X、Y,Z 固定
- 视野矩形大于「操作舒适区」,留出四周空间
- 避免镜头频繁转向(大幅转向让玩家眩晕)
- 或正交透视混合(俯视 45° 用透视投影更立体)
缩放(Zoom):
- zoom 值 = 正交矩形宽高同时缩放
- 缩放范围钳制(最小/最大 zoom)
- 平滑缩放用「对数插值」(1 -> 2 与 2 -> 4 视觉均匀)
- 应用:BOSS 战拉近、大地图拉远、狙击瞄准拉近
-- 对数缩放(视觉均匀)
local t = 1 - math.exp(-zoom_speed * dt)
self.zoom = self.zoom + (math.log(target_zoom / self.zoom)) * self.zoom * t
心智:横版只管 X、俯视管 X/Y、缩放用对数插值——相机的「模式」就是「跟随哪些轴 + 怎么钳制 + 怎么缩放」的组合。
10. 速查表
全篇速查:
| 主题 | 结论 |
|---|---|
| 职责 | 看哪里/看多大/怎么看 |
| 投影 | 2D 正交矩形、3D 透视视锥 |
| 矩阵 | 缩放+平移映射可见矩形 |
| 换算 | 相机位置+半视口±zoom |
| 跟随 | 1-exp(-speed·dt) 帧率无关插值 |
| 震动 | 衰减正弦双轴错频 |
| 分层 | 背景视差/主世界/UI 三层 |
| 渲染 | set_camera 绑定、set_viewport 切视口 |
| 模式 | 横版管 X、俯视管 XY、缩放对数 |
| 手感 | speed 是跟随旋钮、amp/freq 是震动旋钮 |
一句话记忆:相机是「世界到屏幕的映射」——2D 正交、3D 透视,投影矩阵算好宽高比防拉伸;触摸互转靠「相机位置 + 半视口 ± zoom」;跟随用 1-exp(-speed·dt) 帧率无关插值加边界钳制,震动用衰减正弦双轴错频,分层用背景视差/主世界/UI 三层,生效点在渲染脚本 set_camera + set_viewport——横版管 X、俯视管 XY、缩放走对数,三个旋钮(speed/amp/freq)调出全部手感。
延伸阅读
- /defold-3d-games/ — 3D 相机与投影深入
- /defold-script-system-lua/ — 消息传递与协程
- /defold-shader-postprocessing/ — 渲染脚本与后处理
- /defold-input-system/ — 屏幕坐标输入与相机互转
- 游戏开发专题 — 相机与运镜的通用实践
- 图形学专题 — 投影矩阵与渲染管线
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。