引言
同一套 UI,在 16:9 的手机、19.5:9 的全面屏、4:3 的平板和 21:9 的显示器上都要「看起来对」。这件事在 Defold 里不是靠手算像素,而是靠三层机制:节点的锚点(Anchor)决定「以哪条边为基准」,节点的 Adjust Mode 决定「窗口变了怎么伸缩」,节点之间的相对布局决定「父子怎么联动」。三者配合,一套 UI 才能自适应。
本文从 Defold GUI 的节点树讲起,依次覆盖锚点与 pivot 的布局语义、Adjust Mode 三种模式的取舍、百分比与对齐构成的相对布局、safe area 与刘海屏的工程处理、断点式动态重排脚本,最后给出一套可复用的多分辨率适配清单。
前置:GUI 节点体系与交互 、移动端适配与打包 。声明式布局的对照实现见 ArkUI 布局与自定义组件 、前端专题 。
1. GUI 布局的三层模型
Defold GUI 的一切都是节点(Node),节点挂在 .gui 场景的树里,布局就是给每个节点定「位置 + 尺寸 + 伸缩规则」:
box(父节点)
├── panel(背景板)
│ ├── title(标题文本)
│ └── list(列表容器)
│ ├── item_1
│ └── item_2
└── btn_close(关闭按钮)
每个节点的布局属性:
| 属性 | 含义 | 影响 |
|---|---|---|
| position | 位置(相对父节点) | 摆放 |
| size | 尺寸 | 宽高 |
| anchor | 锚点(9 宫格之一) | 以哪条边对齐 |
| pivot | 自身旋转/缩放中心 | 变换基准 |
| adjust_mode | 窗口变化时的伸缩方式 | 响应式核心 |
| inherit_alpha | 是否继承父节点透明度 | 淡入淡出 |
坐标系统:GUI 用的是设计分辨率坐标系,不是物理像素:
game.project 里设定 Display 的 Width/Height(如 960×640)
→ 这是「逻辑设计尺寸」
→ 引擎按 Adjust Mode 把设计尺寸映射到实际窗口
→ 脚本里的 gui.set_position 用的都是这套逻辑坐标
-- 逻辑坐标下设置位置(不用关心实际屏幕多大)
gui.set_position(node, vmath.vector3(480, 320, 0)) -- 设计尺寸 960×640 的中心
心智:GUI 布局是「设计分辨率坐标 + 锚点 + 伸缩规则」——先在设计尺寸里把 UI 摆好,再让 Adjust Mode 和锚点去适配真实窗口。
2. Adjust Mode:Fit / Zoom / Stretch
Adjust Mode 决定「窗口比例和设计比例不一致时怎么办」,这是响应式适配的第一个开关:
| 模式 | 行为 | 结果 | 适合 |
|---|---|---|---|
FIT | 按较短边缩放,留黑边 | 内容完整但可能出黑边 | 背景图、全屏底图 |
ZOOM | 按较长边缩放,裁切溢出 | 铺满但可能裁边 | 主界面、HUD |
STRETCH | 两轴独立拉伸 | 铺满但会变形 | 纯色背景、渐变 |
举例:设计 960×640(3:2),实际窗口 1920×1080(16:9,更宽)
FIT → 高度对齐,左右留黑边
ZOOM → 宽度对齐,上下裁掉一部分
STRETCH → 横向拉宽,圆形变椭圆
混合策略是常态——背景用 FIT、HUD 用 ZOOM:
节点级 adjust_mode 覆盖场景级:
- 根节点:默认 ZOOM(保证铺满)
- 背景节点:FIT(保证图完整,允许黑边被前景盖住)
- 按钮/文本:不伸缩(保持可读尺寸)
-- 运行时改某个节点的伸缩方式
gui.set_adjust_mode(node, gui.ADJUST_STRETCH)
三种模式的可视化判断:
FIT ┌───────────┐ 内容完整
│ █████████ │ 可能黑边
│ █████████ │
└───────────┘
ZOOM ┌───────────┐ 铺满
│███████████│ 可能裁边
│███████████│
STRETCH ┌───────────┐ 铺满
│███████████│ 可能变形
心智:FIT 保内容、ZOOM 保铺满、STRETCH 保尺寸但会变形——实际项目永远混用,背景 FIT、HUD ZOOM、装饰 STRETCH。
在 game.project 里定基调:Display 的 Width/Height 是设计尺寸,Display. Dynamic Orientation 决定是否允许横竖屏切换:
game.project → Display
Width = 960 # 设计宽(逻辑坐标)
Height = 640 # 设计高
High Dpi = 1 # 启用后按物理像素渲染
Dynamic Orientation = 0 # 0=固定 Portrait 1=固定 Landscape 2=两者皆可
-- 设计尺寸在运行时是只读常量,用它做百分比基准
local DW, DH = gui.get_width(), gui.get_height() -- 即 game.project 的设计尺寸
local half = DW * 0.5
一个常被忽略的坑:gui.get_width() 返回的是设计尺寸(缩放前),不是当前窗口尺寸。要拿真实窗口大小必须走 window.get_size()。混用这两者会导致「在高分屏上位置偏移」这类难查的问题。
3. 锚点与 pivot 的布局语义
锚点(Anchor)回答「节点以哪条边为基准摆放」,9 宫格共 9 种:
NW N NE
W C E
SW S SE
锚点的实际作用:当父节点尺寸变化时,锚在哪个边的节点就跟着那条边移动:
-- 关闭按钮锚在右上角 → 无论窗口多宽,它都贴着右上
gui.set_anchor("#btn_close", gui.ANCHOR_NE)
-- 血条锚在左上 → 贴左上
gui.set_anchor("#hp_bar", gui.ANCHOR_NW)
-- 居中标题锚在中心 → 永远居中
gui.set_anchor("#title", gui.ANCHOR_C)
锚点 + 位置的分工:
锚点决定「基准边」,position 决定「相对基准边的偏移」
ANCHOR_NE + position(-40, -40) → 距右上角各 40 像素
ANCHOR_C + position(0, 0) → 正中
pivot 与 anchor 的区别(容易混):
| 概念 | 作用 | 影响什么 |
|---|---|---|
| anchor | 摆放基准 | 窗口变化时节点跟哪条边 |
| pivot | 变换中心 | 旋转/缩放绕哪个点 |
-- pivot 设在中心 → 缩放时向四周扩散
gui.set_pivot("#icon", vmath.vector3(0.5, 0.5, 0))
-- pivot 设在底部中心 → 缩放时底边不动,向上长
gui.set_pivot("#bar", vmath.vector3(0.5, 0, 0))
心智:anchor 管「跟着哪条边」、pivot 管「绕哪个点转」——锚点定位置语义,pivot 定变换语义,两个概念别混用。
4. 相对布局:百分比与对齐
硬编码像素在多分辨率下必炸,Defold 提供百分比与对齐来写「相对布局」:
-- 用 gui.set_size 的百分比形式:宽度 = 父节点的 80%
gui.set_size(node, gui.get_size(parent).x * 0.8, 60)
-- 用对齐把节点贴到父节点某边
gui.set_position(node, vmath.vector3(0, -20, 0))
gui.set_anchor(node, gui.ANCHOR_N)
常见相对布局模式:
1. 顶部工具栏:anchor=NW,宽 = 屏幕宽,高 = 固定 64
2. 底部按钮:anchor=SW,宽 = 屏幕宽,高 = 固定 96
3. 居中弹窗:anchor=C,宽 = 屏幕宽 × 0.8,高 = 自适应
4. 列表:父容器 anchor=C,子项按索引算 y 偏移
-- 列表项动态排布(不依赖 GUI 编辑器里的手摆)
local function layout_list(self, items)
local container = gui.get_node("list")
local cw = gui.get_size(container).x
local item_h = 72
for i, item in ipairs(items) do
local node = gui.get_node("item_" .. i)
gui.set_size(node, vmath.vector3(cw - 24, item_h))
gui.set_position(node, vmath.vector3(cw * 0.5, -item_h * (i - 0.5)))
gui.set_anchor(node, gui.ANCHOR_N)
end
end
父节点尺寸变化的联动:gui.set_size 改父节点后,锚在对应边的子节点会自动跟随:
-- 面板变宽,锚在 E(右边)的子节点自动右移
gui.set_size("#panel", vmath.vector3(new_w, new_h))
-- 锚点为 NE/E/SE 的子节点无需手动调整
对齐枚举速查:
| 枚举 | 值 | 含义 |
|---|---|---|
gui.ANCHOR_NW | 左上 | 工具栏、血条 |
gui.ANCHOR_N | 上中 | 顶部标题 |
gui.ANCHOR_NE | 右上 | 关闭按钮、设置 |
gui.ANCHOR_C | 中心 | 弹窗、加载圈 |
gui.ANCHOR_SW | 左下 | 摇杆、版本号 |
gui.ANCHOR_S | 下中 | 主菜单按钮 |
gui.ANCHOR_SE | 右下 | 小地图、聊天按钮 |
心智:相对布局 = 锚点定基准 + 百分比算尺寸 + 子节点自动跟随——列表和网格一律用脚本算,别在编辑器里手摆像素。
5. Safe Area 与刘海屏
全面屏的「挖孔/刘海/圆角」会盖住 UI,必须留出安全区(Safe Area):
-- Defold 提供安全区查询
local safe = sys.get_safe_area()
-- safe.x / safe.y / safe.width / safe.height(物理像素)
把安全区换算到 GUI 逻辑坐标:
local function get_safe_insets(self)
local safe = sys.get_safe_area()
local w, h = window.get_size() -- 物理像素窗口尺寸
local scale_x = gui.get_width() / w -- 逻辑/物理 比例
local scale_y = gui.get_height() / h
return {
left = safe.x * scale_x,
right = (w - safe.x - safe.width) * scale_x,
top = safe.y * scale_y,
bottom = (h - safe.y - safe.height) * scale_y,
}
end
用安全区约束关键 UI:
-- 顶部 HUD 下移,避开刘海
local insets = get_safe_insets(self)
gui.set_position("#top_hud", vmath.vector3(0, -insets.top - 8, 0))
gui.set_anchor("#top_hud", gui.ANCHOR_N)
-- 底部按钮上移,避开手势条
gui.set_position("#bottom_bar", vmath.vector3(0, insets.bottom + 8, 0))
gui.set_anchor("#bottom_bar", gui.ANCHOR_S)
平台差异与常见陷阱:
- iOS:safe area 含状态栏 + Home Indicator,顶部通常 44~59pt
- Android:异形屏由厂商决定,部分机型 safe area 为 0(需兜底)
- 桌面/Web:safe area 恒等于全屏,代码路径要能跑通
- 横屏/竖屏切换:window.set_listener 监听 resize 后重新布局
-- 监听窗口尺寸变化,重新布局
function init(self)
window.set_listener(function(self, event, data)
if event == window.WINDOW_EVENT_RESIZED then
self:relayout()
end
end)
end
心智:安全区是「物理像素」而 GUI 是「逻辑坐标」——先查
sys.get_safe_area(),换算比例后约束顶部/底部关键 UI,并给 safe area 为 0 的机型兜底。
6. 断点式动态重排
不同宽高比需要不同布局(手机单列、平板双列),用「断点(Breakpoint)」思路做动态重排:
local BREAKPOINTS = {
{ min_ratio = 1.8, layout = "wide" }, -- 21:9 / 19.5:9
{ min_ratio = 1.3, layout = "normal" }, -- 16:9 / 3:2
{ min_ratio = 0, layout = "tall" }, -- 4:3 / 竖屏
}
local function pick_layout(w, h)
local ratio = w / h
for _, bp in ipairs(BREAKPOINTS) do
if ratio >= bp.min_ratio then return bp.layout end
end
return "normal"
end
按布局重排:
local LAYOUTS = {
wide = { cols = 4, item_w = 0.22, gap = 0.02 },
normal = { cols = 3, item_w = 0.30, gap = 0.03 },
tall = { cols = 2, item_w = 0.45, gap = 0.05 },
}
function relayout(self)
local w, h = gui.get_width(), gui.get_height()
local cfg = LAYOUTS[pick_layout(w, h)]
for i, node in ipairs(self.item_nodes) do
local col = (i - 1) % cfg.cols
local row = math.floor((i - 1) / cfg.cols)
local x = (col + 0.5) * (cfg.item_w + cfg.gap) * w
local y = -40 - row * (cfg.item_w * w + 8)
gui.set_position(node, vmath.vector3(x, y, 0))
gui.set_size(node, vmath.vector3(cfg.item_w * w, cfg.item_w * w))
end
end
字体与文本的响应式:
-- 小屏缩小字号,大屏放大(相对设计宽度)
local scale = gui.get_width() / 960
local size = math.max(14, math.floor(20 * scale)) -- 设下限,防过小
gui.set_font(node, "fonts/main", size)
文本溢出的处理:动态文案(玩家名、道具描述)长度不可控,必须显式约束:
-- 方案一:截断 + 省略号
local function truncate(text, max_chars)
if #text <= max_chars then return text end
return string.sub(text, 1, max_chars - 1) .. "…"
end
gui.set_text(node, truncate(self.player_name, 12))
-- 方案二:用带 line break 的字体,配合 gui.set_line_break
gui.set_text(node, self.desc)
gui.set_line_break(node, true)
文本三策:
固定槽位(如名称栏)→ 截断 + 省略号
弹性区域(如描述)→ 自动换行 + 容器高度自适应
极端长文本 → 滚动容器(scroll view 思路:裁剪 + 偏移)
适配自检清单:
✓ 最窄(4:3)与最宽(21:9)下 UI 都不重叠、不越界
✓ 安全区为 0 的机型跑一遍,关键 UI 不被状态栏遮挡
✓ 字体不小于 14pt(可读性下限)
✓ 触摸目标不小于 44×44 逻辑像素
✓ 横竖屏切换后重新布局(监听 WINDOW_EVENT_RESIZED)
✓ 超长文本有省略或换行策略,不撑破容器
心智:断点 = 按宽高比选布局配置(列数/尺寸/间距),再统一重排——把「布局」变成数据表,加一种屏幕形态只加一行配置。
速查表
| 需求 | 做法 |
|---|---|
| 铺满不变形 | 背景 FIT、HUD ZOOM |
| 贴右上角 | gui.set_anchor(node, gui.ANCHOR_NE) |
| 永远居中 | gui.set_anchor(node, gui.ANCHOR_C) |
| 绕底边缩放 | gui.set_pivot(node, vmath.vector3(0.5, 0, 0)) |
| 宽度占屏 80% | gui.set_size(node, w * 0.8, h) |
| 避开刘海 | sys.get_safe_area() + 比例换算 |
| 监听窗口变化 | window.set_listener(WINDOW_EVENT_RESIZED) |
| 动态改伸缩 | gui.set_adjust_mode(node, gui.ADJUST_STRETCH) |
| 列表重排 | 脚本按索引算 position,别手摆 |
| 多形态适配 | 断点表(宽高比 → 列数/尺寸) |
一句话记忆:Defold GUI 适配 = 设计分辨率坐标 + 锚点定基准 + Adjust Mode 定伸缩 + 脚本算相对布局——背景 FIT 保完整、HUD ZOOM 保铺满;anchor 管跟随、pivot 管变换;安全区用 sys.get_safe_area 换算后约束顶底;多形态靠断点表选配置再统一重排——把布局写成数据,加屏幕形态只加一行。
小结
响应式布局的难点不在 API,而在决策顺序:先确定设计分辨率与 Adjust Mode 的全局策略(背景保完整、HUD 保铺满),再用锚点把每个节点「挂」到正确的基准边,然后用百分比与脚本重排代替硬编码像素,最后用安全区与断点处理极端形态。工程上有三条硬纪律:一是任何位置尺寸都不得写死像素(除图标与间距这类装饰量);二是列表、网格一律脚本重排,编辑器里只摆结构不摆位置;三是必须在最窄与最宽两种比例下各跑一遍,并覆盖 safe area 为 0 的机型。做到这三点,一套 UI 就能从手机铺到桌面。
延伸阅读
- GUI 节点体系与交互
- 移动端适配与打包
- ArkUI 布局与自定义组件 — 声明式布局的另一种解法
- 前端专题 — 响应式与弹性布局的通用实践
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。