引言
移动端适配是 Defold 项目从「能跑」到「能上架」之间最容易被低估的一段路。同一份代码要在 18:9 的挖孔屏、4:3 的平板、折叠屏上都不出问题,靠的不是一套魔法参数,而是三件事分开处理:游戏世界怎么渲染、GUI 怎么排布、安全区怎么避让。本文按这三条线展开,并给出可直接抄的代码与项目设置。
目录
- 1. 适配要解决的四个问题
- 2. 分辨率与渲染投影
- 3. GUI 缩放模式与锚点
- 4. 显示配置与布局切换
- 5. 安全区与刘海屏
- 6. 触屏输入适配
- 7. 横竖屏与高 DPI 实战
- 8. 真机调试与常见坑
- 9. 速查表
- 相关阅读
- 延伸阅读
1. 适配要解决的四个问题
1. 内容量 屏幕更宽时是看到更多场景,还是把画面拉大?
2. 宽高比 16:9 与 4:3 之间如何过渡,是黑边、裁切还是缩放?
3. 界面排布 按钮贴屏幕边缘还是贴内容,换方向后怎么重排?
4. 遮挡区域 刘海、挖孔、圆角、手势条,哪些区域不能放关键 UI?
前两个问题由渲染投影决定,第三个由 GUI 锚点与缩放模式决定,第四个由安全区决定。三者互不替代:把投影设成「显示更多内容」并不能让按钮躲开刘海,把按钮锚到屏幕边缘也不能阻止场景被裁切。
踩坑:最常见的错误是「用一套 GUI 硬扛所有机型」——节点锚点全是默认值,结果在 iPad 上按钮跑到屏幕正中、在挖孔屏上被摄像头挡住。适配必须从设计阶段就把锚点和安全区当成必填项。
2. 分辨率与渲染投影
1. 默认行为
game.project 里的 display.width / display.height 定义的是逻辑分辨率,也是脚本与属性里使用的坐标系。默认渲染脚本的行为是:无论窗口多大,始终把这块逻辑区域拉伸铺满窗口。
[display]
width = 960
height = 640
high_dpi = 0
这在窗口被拉成不同宽高比时会变形(圆变椭圆)。要改,向渲染脚本发消息切换投影模式:
msg.post("@render:", "use_stretch_projection") -- 拉伸铺满(默认,会变形)
msg.post("@render:", "use_fixed_fit_projection") -- 等比缩放,尽量填满,可能露黑边
msg.post("@render:", "use_fixed_projection", { zoom = 2 }) -- 固定缩放倍数
2. 三种策略的取舍
| 消息 | 行为 | 适合 |
|---|---|---|
use_stretch_projection | 铺满窗口,宽高比失真 | 极少用,宽高比差异小的场景 |
use_fixed_fit_projection | 等比缩放,保证逻辑区域全可见 | 现代 HD 游戏,宁可有黑边 |
use_fixed_projection | 固定倍数放大,视野随之变化 | 像素风复古游戏 |
use_fixed_fit_projection 保证「至少看到逻辑分辨率的全部内容」,多出来的部分显示更多场景,不会裁切。用 Camera 组件也能达到同样效果:勾选 Orthographic Projection 并把 Orthographic Zoom 设为目标倍数。
3. 像素风的两个必改项
[graphics]
texture_profiles = /builtins/graphics/default.texture_profiles
[display]
width = 320
height = 200
再加两处设置,缺一不可:
- Texture Profiles 里把过滤设为 nearest,否则放大后模糊
- game.project 里关掉 Subpixels,精灵永远对齐整像素
3. GUI 缩放模式与锚点
1. 三个节点属性
每个 GUI 节点有三个决定适配行为的属性:
Pivot 节点中心点,缩放与旋转围绕它进行
Anchor 场景或父节点被拉伸时,节点的位置如何跟随(Left/Right/Top/Bottom/None)
Adjust 场景或父节点被调整时,节点自身尺寸如何变化
Anchor 是位置策略,Adjust 是尺寸策略,二者独立。一个常见的错误是只设了 Anchor 忘了 Adjust,结果按钮位置对了但尺寸还是老的。
2. Adjust Mode 的三种取值
| 模式 | 行为 | 典型用途 |
|---|---|---|
gui.ADJUST_FIT | 等比缩放到能装下,保持宽高比 | 图片、图标,绝不希望变形 |
gui.ADJUST_ZOOM | 等比缩放到铺满,可能超出边界 | 全屏背景 |
gui.ADJUST_STRETCH | 直接拉伸填满,允许变形 | 纯色条、边框、进度条底 |
运行时改单个节点:
gui.set_adjust_mode(gui.get_node("bg"), gui.ADJUST_ZOOM)
gui.set_xanchor(gui.get_node("hp_bar"), gui.ANCHOR_LEFT)
gui.set_yanchor(gui.get_node("hp_bar"), gui.ANCHOR_TOP)
-- 读取
local mode = gui.get_adjust_mode(node)
local w, h = gui.get_size(node)
踩坑:Anchor 只有在场景或父节点边界变化时才起作用。如果父节点自身是
FIT且尺寸固定,子节点设 Anchor 也不会有任何变化——先确认父级的 Adjust 是否会变。
3. 尺寸模式
节点还有 Size Mode,决定尺寸来源:gui.SIZE_MODE_MANUAL 手动设,或按贴图/父节点自动。做自适应进度条时,通常让底图 STRETCH、前景条 MANUAL 并用 gui.set_size 按比例设宽:
local pct = self.hp / self.max_hp
local full_w = 200
gui.set_size(gui.get_node("hp_fill"), vmath.vector3(full_w * pct, 16, 0))
4. 显示配置与布局切换
1. Display Profiles
Display Profiles 文件描述一组目标分辨率 + 设备型号,GUI 场景可以针对每个 Profile 存一套节点属性覆盖。默认文件是 builtins/render/default.display_profiles,含 Landscape(1280×720)与 Portrait(720×1280)两个 Profile,不带设备型号,因此匹配任意机型。
[display]
display_profiles = /builtins/render/default.display_profiles
dynamic_orientation = 1
勾选 Dynamic Orientation 后,引擎在设备旋转时自动切换到匹配方向的布局。
2. 布局是属性覆盖,不是另一套节点
GUI 里的「Layout」只覆盖属性,不能增删节点。想在某布局里隐藏节点,只能移到屏幕外或用脚本逻辑处理。被覆盖的属性在编辑器里显示为蓝色,可一键重置。
3. 手动切换与消息
关掉 Auto Layout Selection 后由脚本控制:
function init(self)
local ok = gui.set_layout("Portrait")
if not ok then print("no Portrait layout") end
local layouts = gui.get_layouts()
for id, size in pairs(layouts) do print(id, size.x, size.y) end
end
function on_message(self, message_id, message, sender)
if message_id == hash("layout_changed") and message.id == hash("Portrait") then
-- 切换到竖屏布局后的额外逻辑
end
end
布局真正发生变化时才会发 layout_changed。渲染脚本还会收到 window_resized 消息,message.width / message.height 是新的窗口尺寸——这是重算相机与视口的地方。
5. 安全区与刘海屏
1. 内置安全区 API
window.get_safe_area() 返回当前窗口的安全区矩形,字段齐全:
local sa = window.get_safe_area()
-- sa.x, sa.y, sa.width, sa.height
-- sa.inset_left, sa.inset_top, sa.inset_right, sa.inset_bottom
print(sa.inset_top) -- 顶部被刘海/状态栏占用的像素数
坐标是物理像素,需要除以 window.get_display_scale() 才能换算到逻辑坐标:
local scale = window.get_display_scale()
local top_inset = window.get_safe_area().inset_top / scale
2. GUI 安全区模式
GUI 场景可以声明安全区模式,引擎自动把 insets 计入节点调整:
gui.set_safe_area_mode(gui.SAFE_AREA_BOTH) -- 四边全部避让
-- 其他取值:SAFE_AREA_NONE / SAFE_AREA_LONG / SAFE_AREA_SHORT
项目级默认值在 game.project:
gui.safe_area_mode
none 默认,忽略所有 insets
long 横屏避让左右,竖屏避让上下
short 横屏避让上下,竖屏避让左右
both 四边全避让
long 与 short 的名字来自「屏幕的长边 / 短边」:long 指避让长边方向的 insets,short 指避让短边方向的 insets。
3. 更细的控制:safearea 扩展
需要拿到圆角半径或自己画背景时,用 extension-safearea:
local insets = safearea.get_insets() -- 四个方向的 insets
local radius = safearea.get_corners_radius() -- 圆角半径
safearea.set_background_color(vmath.vector4(0, 0, 0, 1))
踩坑:
window.get_safe_area()返回的是物理像素,而 GUI 与脚本里的位置是逻辑像素。忘记除以window.get_display_scale()会导致在 high_dpi 机型上 inset 被高估一倍。
6. 触屏输入适配
1. 触摸事件
触屏与鼠标在 Defold 里走同一套 on_input,用 action_id 区分。多点触控用 action.id(触点编号)跟踪:
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
self.touch_id = action.id
self.start = vmath.vector2(action.x, action.y)
elseif action_id == hash("touch") and action.released then
if action.id == self.touch_id then
local dx = action.x - self.start.x
if math.abs(dx) > 40 then self:swipe(dx) end
end
end
end
action.x / action.y 已经是逻辑坐标,无需再换算,这一点和 window.get_safe_area() 不同,容易搞混。
2. 按钮的触摸区域
GUI 脚本里用 gui.pick_node 判断触点是否落在节点上,注意节点可能被安全区推走:
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
if gui.pick_node(gui.get_node("btn_play"), action.x, action.y) then
msg.post("#", "play")
end
end
end
小屏幕上手指比像素粗,交互目标最小边建议不小于 44 逻辑像素;用一张透明贴图把点击区域撑大,比把按钮画大更好。
7. 横竖屏与高 DPI 实战
1. 高 DPI 与视网膜屏
[display]
high_dpi = 1
开启后,支持的屏幕上会创建双倍分辨率的后备缓冲:脚本与属性里的坐标仍是逻辑分辨率,渲染分辨率翻倍。1 倍尺寸的素材看起来不变,而把高清素材缩到 0.5 使用就会在视网膜屏上更清晰。
2. 横竖屏切换的完整流程
1. game.project 勾选 Dynamic Orientation
2. 在 GUI 场景里建 Landscape / Portrait 两个 Layout
3. 监听 layout_changed 更新与方向相关的逻辑
4. 在渲染脚本里监听 window_resized 重算相机与视口
5. 场景本身(非 GUI)的投影在切换后要重新下发 use_fixed_fit_projection
第 5 步最容易被漏:GUI 布局管理器会自动重排节点,但游戏世界内容默认仍以 stretch-fit 投影渲染。方向切换后要么重新发投影消息,要么换用 Camera 组件由引擎按相机参数处理。
3. 折叠屏与平板
local info = sys.get_sys_info()
print(info.device_model, info.system_name, info.system_version)
local w, h = window.get_size()
local aspect = w / h
if aspect > 2.0 then
-- 超宽屏,可能需要额外留白或调整视野
end
Display Profiles 的 Device Models 字段可以按机型前缀匹配(iPhone10 会匹配 iPhone10,*),但只有 Android 与 iOS 会返回设备型号,其他平台返回空串。
8. 真机调试与常见坑
1. 编辑器内模拟分辨率
Debug 菜单里的 Simulate Resolution 可以直接把运行中的窗口改成某个机型的尺寸,实时看效果。这是成本最低的验证方式,但它不模拟安全区与圆角。
2. 常见坑清单
- 只设 Anchor 不设 Adjust,位置对了尺寸还是错的
- 忘除 display_scale,inset 在 high_dpi 机型上翻倍
- 布局切换后没重发投影消息,场景内容仍是旧投影
- 用 STRETCH 处理图标,在宽屏上被拉扁
- 交互热区小于 44 逻辑像素,真机上点不中
- 把关键按钮放在安全区外,被刘海或手势条遮住
- 用固定像素写死 UI 位置,换分辨率就错位
3. 一套可复用的检查顺序
先看游戏世界:投影消息是否按宽高比切换?
再看 GUI:每个节点的 Anchor / Adjust 是否明确?
再看安全区:safe_area_mode 是否设置,关键 UI 是否在内?
最后看输入:热区尺寸与坐标是否用逻辑像素?
按这个顺序过一遍,绝大多数「在我手机上没问题」的适配 bug 都能提前暴露。
9. 速查表
| 需求 | 做法 | 备注 |
|---|---|---|
| 等比缩放不变形 | msg.post("@render:", "use_fixed_fit_projection") | 可能露黑边 |
| 固定倍数放大 | use_fixed_projection, { zoom = 4 } | 像素风 |
| 像素清晰 | Texture Profiles 设 nearest + 关 Subpixels | 两者都要 |
| 节点保持宽高比 | gui.set_adjust_mode(n, gui.ADJUST_FIT) | 图标、图片 |
| 节点铺满 | gui.set_adjust_mode(n, gui.ADJUST_ZOOM) | 背景 |
| 节点贴边 | gui.set_xanchor(n, gui.ANCHOR_LEFT) | 与 yanchor 独立 |
| 取安全区 | window.get_safe_area() | 返回物理像素 |
| 物理转逻辑 | 除以 window.get_display_scale() | 常被遗漏 |
| GUI 避让刘海 | gui.set_safe_area_mode(gui.SAFE_AREA_BOTH) | 项目级 gui.safe_area_mode |
| 圆角半径 | safearea.get_corners_radius() | 需 extension-safearea |
| 自动横竖屏 | 勾 Dynamic Orientation + 建双 Layout | 监听 layout_changed |
| 窗口变化 | 渲染脚本收 window_resized | message.width/height |
| 高清屏 | high_dpi = 1 | 逻辑分辨率不变 |
| 机型信息 | sys.get_sys_info().device_model | 仅 Android / iOS 有值 |
一句话记忆:渲染投影管「看到多少」,GUI 锚点与缩放管「排在哪、多大」,安全区管「别被挡住」——三条线分开配,再按「世界 → GUI → 安全区 → 输入」的顺序逐个验证。
相关阅读
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。