Defold 相机与视口:投影矩阵、平滑跟随与屏幕震动

系统覆盖 Defold 相机系统:相机组件与投影、正交/透视投影矩阵、视口与屏幕坐标、平滑跟随(lerp/追尾相机)、屏幕震动(shake)与击退、多相机分层与切换、渲染脚本与相机耦合、以及横版/俯视/缩放等游戏相机模式的实战实现。

引言

游戏的世界坐标与屏幕坐标之间隔着「相机」——相机的投影矩阵决定玩家能看到世界的哪一块、以什么视角看。本文把 Defold 相机讲透:先讲相机组件与两种投影(正交/透视)的差别,再深入投影矩阵与视口/屏幕坐标换算,接着讲「平滑跟随」(追尾相机)的 lerp 实现与防抖,再讲屏幕震动与击退这些「手感」效果,然后是多相机分层(背景层/主世界/UI 层)与切换,最后讲渲染脚本如何与相机耦合、以及横版/俯视/缩放三类典型相机模式。

前置:/defold-3d-games/(投影与 3D 基础)、/defold-script-system-lua/(消息与协程)、/defold-shader-postprocessing/(渲染管线)。矩阵基础见 [[cs-fundamentals]]。


目录


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/ — 屏幕坐标输入与相机互转
  • 游戏开发专题 — 相机与运镜的通用实践
  • 图形学专题 — 投影矩阵与渲染管线

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章

  1. Defold Tilemap 碰撞与关卡设计
  2. Defold 性能分析与调试工具链
  3. Defold Collections、Factories 与动态实例化