Defold 输入系统:动作绑定、触控键盘手柄与手势识别

系统覆盖 Defold 输入处理:输入绑定文件(input_binding)与动作(action)体系、on_input 回调与分发优先级、触控多点触控、虚拟摇杆与按钮、键盘/鼠标/游戏手柄动作映射、手势识别(拖拽/捏合/滑动)、输入与 UI 节点交互、以及跨平台输入适配的工程实践。

引言

输入是玩家与游戏的「唯一接口」——一个手势延迟 50ms、一个摇杆反了、一次多点触控误触,都能毁掉整个手感。本文把 Defold 输入系统讲透:先讲输入绑定文件(input_binding)与「动作」抽象,再讲 on_input 回调与事件分发优先级,接着覆盖触控(单点/多点/坐标映射)、虚拟摇杆与按钮的实现、键盘/鼠标/游戏手柄的动作映射、手势识别(拖拽/捏合缩放/滑动判定),然后是输入与 GUI/UI 节点的交互分发,最后给跨平台输入适配(移动/桌面/主机)的工程清单。

前置:/defold-script-system-lua/(消息与生命周期)、/defold-ui-gui/(GUI 节点交互)、/defold-camera-viewport/(屏幕↔世界坐标换算)。触控平台实践见 前端专题。


目录


1. 输入绑定的核心抽象:动作

Defold 的输入不直接暴露「按键」,而是暴露「动作」:

硬件输入(触控/键鼠/手柄)
    ↓ input_binding 文件
抽象动作(jump / move_left / attack / confirm)
    ↓ on_input 回调
游戏逻辑消费「语义动作」而非「具体按键」

为什么抽象动作:

- 同一游戏逻辑跨平台:手机用触控、桌面用键盘、主机用按键
- 换键/重绑定 = 改绑定文件,不动逻辑代码
- 动作语义化:改「按键」不影响「玩家做什么」

动作的三种事件形态:

pressed    按下(边沿触发,一次)
released   抬起(边沿触发,一次)
repeated   长按重复(按住自动重复)
continuous 持续状态(按住期间每帧,值为强度)

心智:Defold 输入 = 「硬件 → 绑定文件 → 语义动作 → 逻辑」——玩家行为用动作表达,换平台只改绑定。


2. 输入绑定文件与动作映射

input_binding 文件(game.project 关联):

game.input_binding 配置:
  - trigger 动作:pressed/released/repeated
  - continuous 动作:每帧持续(摇杆/按键强度)
  - 每种动作映射多个硬件输入
// game.input_binding 片段(示意)
{
  "trigger": {
    "jump":   ["key_j", "gamepad_action_2", "touch_click"],
    "pause":  ["key_escape"]
  },
  "continuous": {
    "move_h": ["key_a", "key_d", "gamepad_leftstick"],
    "move_v": ["key_w", "key_s"]
  }
}

动作 → 代码:

function on_input(self, action_id, action)
    if action_id == hash("jump") and action.pressed then
        self.player:jump()
    end
end

坐标型动作(触控/鼠标):

-- 触控与鼠标动作带屏幕坐标
function on_input(self, action_id, action)
    if action_id == hash("click") then
        -- action.x / action.y(屏幕逻辑坐标)
        -- action.dx / action.dy(触控移动增量)
        -- action.screen_x / screen_y(物理像素)
    end
end

心智:绑定文件 = 动作↔硬件映射表——trigger 管「边沿事件」、continuous 管「持续状态」,坐标动作自带屏幕坐标。


3. on_input 回调与分发优先级

输入事件如何到达游戏逻辑:

输入系统
   → on_input 回调(根脚本 / 所有监听者按注册顺序)
   → 消息分发(msg.post 到具体对象)
   → GUI 节点事件(gui 的 on_input)

分发优先级与「消费」:

- 多个对象监听同一个动作 → 都收到(Defold 不自动「消费」)
- 手动处理「焦点」:UI 打开时「吞掉」世界输入
- 常见模式:输入管理器脚本集中处理 → 转发给当前状态
-- 输入管理器:按游戏状态分发
function on_input(self, action_id, action)
    if self.ui_open then return end          -- UI 打开吞输入
    if self.state == "playing" then
        handle_gameplay(action_id, action)
    elseif self.state == "dialogue" then
        handle_dialogue(action_id, action)
    end
end

关键:不要每个对象都监听全局输入:

- 多对象各自 on_input → 顺序不确定、互相打架
- 正解:单一输入管理器分发,或用 msg 定向发送
- 需要「当前唯一接受者」时 → 设焦点对象

心智:输入事件「广播」给所有监听者——用单一输入管理器集中处理、按状态分发、UI 打开时吞输入,别让多个对象抢同一个动作。


4. 触控:单点多点与坐标映射

触控是移动端的默认输入,要点是坐标与多点:

坐标映射:

function on_input(self, action_id, action)
    if action.touch then
        -- action.x/y:游戏逻辑坐标(已映射到渲染视口)
        -- action.screen_x/y:物理像素
        -- 分辨率适配:逻辑坐标已处理 DPR,直接用于世界互转
    end
end

多点触控:

-- Defold 触控事件携带触控点 id(action.touch_id)
function on_input(self, action_id, action)
    if action.touch then
        self.touches[action.touch_id] = {x = action.x, y = action.y}
    end
end

-- 双指捏合:两指各自跟踪,算距离变化
function pinch_update(self)
    local a, b = touches[1], touches[2]
    local dist = vmath.length(a - b)
    local scale_delta = dist / self.last_dist
    self.camera_zoom = self.camera_zoom * scale_delta
end

触控判定要点:

- 区分「点击 vs 滑动」:按下后移动超过阈值 = 滑动,否则抬起算点击
- 多点触控要按 touch_id 区分手指,别混
- 触控区域的命中:用「逻辑坐标 + 命中矩形」
- 预防误触:点击容差(按下到抬起 < 时间 & < 距离才算点击)

心智:触控 = 逻辑坐标 + touch_id 区分手指 + 点击/滑动阈值判定——双指捏合就是「跟踪两指 + 算距离变化缩放」。


5. 虚拟摇杆与按钮

移动端没有实体键盘,虚拟控件是必备——虚拟摇杆是移动端「移动手感」的命门:

摇杆实现(跟随式 vs 固定式):

-- 跟随式摇杆:按下位置成为摇杆圆心
function on_input(self, action_id, action)
    if action_id == hash("stick") then
        if action.pressed then
            self.stick_center = {x = action.x, y = action.y}
        end
        -- 向量 = 当前触摸 - 圆心(钳制在半径内)
        local dx = clamp(action.x - self.stick_center.x, -R, R)
        local dy = clamp(action.y - self.stick_center.y, -R, R)
        local nx, ny = dx / R, dy / R        -- 归一化 -1~1
        self.player:set_move(nx, ny)
    end
end

摇杆设计要点:

- 死区:输入 < 半径 15% 视为 0(防漂移)
- 半径钳制:超出半径按最大方向输出(别让拇指「滑出」)
- 归一化输出 -1~1 → 供移动/瞄准复用
- 跟随式适合拇指随意按压;固定式适合竞技(位置固定好肌肉记忆)

虚拟按钮:

-- 按钮 = 命中矩形 + 按下/抬起状态
local btn_rect = {x=0, y=0, w=96, h=96}
function on_input(self, action_id, action)
    if action.touch then
        local hit = point_in_rect(action.x, action.y, btn_rect)
        self.jump_pressed = hit and action.pressed
    end
end

输入缓冲(手感玄学):

- 玩家在角色「硬直帧」按下跳跃 → 记录「跳跃意图」
- 硬直结束后 100~150ms 内缓冲执行 → 手感「跟手」
- 现代动作游戏手感一半靠「输入缓冲 + 提前量」

心智:虚拟摇杆 = 按下定圆心 + 半径钳制 + 死区归一化;虚拟按钮 = 命中矩形 + 状态;再加「输入缓冲」兑现意图——移动端手感三分之二在这里。


6. 键盘鼠标与游戏手柄

桌面与主机的输入映射:

-- 键盘:action 用 key_xxx 绑定
-- 连续动作读强度
function on_input(self, action_id, action)
    if action_id == hash("move_h") then
        self.axis_h = action.value or 0   -- continuous 动作有值
    end
end

手柄(gamepad)动作:

Defold gamepad:
  按键 → trigger 动作(gamepad_action_1 ~ 16)
  左摇杆/右摇杆 → continuous(gamepad_leftstick/rightstick,-1~1)
  扳机(LT/RT)→ continuous 值 0~1
  十字键 → gamepad_dpad_up/down/left/right
-- 摇杆与按键统一成「轴向输入」再进逻辑
function input_axis(self, action_id, action)
    if action_id == hash("move_h") then
        -- 键盘 a/d 或手柄摇杆都在这一条 continuous 动作里
        self.axis_h = clamp(action.value, -1, 1)
    end
end

鼠标:

- 鼠标移动 → action_id hash("mouse"),action.dx/dy 增量
- 鼠标按钮 → trigger 动作(mouse_button_left 等)
- 鼠标坐标 → action.x/y(适合指向/瞄准类游戏)
- 滚轮 → 缩放/切枪常用(action.value 增量)

统一「轴向」的工程实践:

- 键盘 WASD 与手柄摇杆映射到同一 continuous 动作
- 逻辑只消费「-1~1 轴向」,不关心来源
- 键位自定义 = 改绑定文件或运行时重绑

心智:键鼠/手柄都收敛成「trigger 事件 + continuous 轴向」——逻辑吃 -1~1 轴向,来源交给绑定文件,换设备零改动。


7. 手势识别:拖拽捏合滑动

手势 = 触控序列的「语义化」:

拖拽(Drag):

-- 拖拽:按下 → 持续跟踪移动 → 抬起结束
function on_input(self, action_id, action)
    if action.touch then
        if action.pressed then
            self.drag = {id = action.touch_id, sx = action.x, sy = action.y}
        elseif self.drag and action.touch_id == self.drag.id then
            self.drag.cx, self.drag.cy = action.x, action.y
        end
    end
end
-- 消费:把拖拽增量应用到「被拖对象」(世界坐标互转)

捏合缩放(Pinch):

-- 两指距离变化 → 缩放
function pinch(self, a, b)
    local dist = vmath.length(vmath.vector2(b.x - a.x, b.y - a.y))
    if self.last_dist then
        self.zoom = self.zoom * (dist / self.last_dist)   -- 钳制范围
    end
    self.last_dist = dist
end

滑动(Swipe)判定:

- 记录按下点与抬起点
- 距离 > 阈值(如 80px)且速度 > 阈值 → 判定为滑动
- 方向 = 抬起点 - 按下点的主导轴(上下左右)
- 与「点击」区分:滑动距离大 / 时长长 → 不算点击
-- 滑动判定
local dx, dy = up.x - down.x, up.y - down.y
if math.abs(dx) > 80 and math.abs(dx) > math.abs(dy) * 2 then
    swipe_dir = (dx > 0) and "right" or "left"
end

手势的工程规范:

- 每个手势要「多指 id 隔离」(第二指按下不影响第一指拖拽)
- 手势之间判定优先级(捏合优先于拖拽,当两指活跃时)
- 手势结果做「去抖动」(阈值 + 时间窗)

心智:手势 = 触控序列的语义化——拖拽跟踪单指、捏合算两指距离、滑动按阈值判定方向;多指隔离 + 优先级 + 阈值去抖是工程三件套。


8. 输入与 GUI 的交互分发

屏幕上的 UI 节点也是输入消费者——输入要在「UI 与 3D 世界」之间正确分发:

Defold GUI 输入:

-- gui 的 on_input:节点已按「层叠顺序」命中判定
function gui.on_input(self, action_id, action)
    local node = gui.pick_node(self.ui, action.x, action.y)  -- 命中节点
    if node and action.pressed then
        handle_button(node, action)
    end
end

输入分发原则:

- GUI 优先:UI 打开/按钮命中时,世界输入被「吞掉」
- 命中优先级:最上层节点先判定(gui 的 z 顺序)
- 按钮的「点击」判定要等 released(按下在按钮上、抬起也在 → 算点击)
-- 按钮正确判定:按下记起点,抬起检查「仍在同一按钮」才算点击
function on_input(self, action_id, action)
    if action.touch then
        if action.pressed then
            self.down_node = gui.pick_node(self.ui, action.x, action.y)
        elseif action.released then
            local up_node = gui.pick_node(self.ui, action.x, action.y)
            if up_node == self.down_node then on_click(up_node) end
        end
    end
end

GUI vs 世界输入的「焦点切换」:

- 打开菜单 → 世界输入关闭(输入管理器设 ui_open=true)
- 拖动 UI 滑块 → 世界不响应(GUI 消费)
- 对话框/文本输入 → 独占输入焦点

心智:GUI 输入优先、按钮点击要「按下+抬起在同一节点」判定——UI 打开就吞世界输入,别让菜单后面的角色还在跑。


9. 跨平台输入适配

同一套游戏逻辑 + 三套输入来源的工程清单:

平台输入来源适配要点
iOS/Android触控虚拟摇杆/按钮、多点、点击滑动阈值
桌面键鼠WASD+鼠标瞄准、快捷键
主机/掌机手柄摇杆/按键/扳机连续值

统一抽象层:

- 语义动作:move_x / aim_x / jump / attack / confirm / cancel
- 每平台一套绑定映射 → 动作层不变
- 逻辑层消费「动作 + 轴向值」,绝不消费「具体按键」

运行时检测平台:

-- Defold 平台检测
local IS_MOBILE = (sys.get_config("os") == "ios") or (sys.get_config("os") == "android")

跨平台 UI 差异:

- 移动端显示虚拟控件、桌面端不显示(或显示键位提示)
- 鼠标可悬停 → hover 状态只有桌面才有
- 触控无右键/无滚轮 → 功能别依赖右键

适配检查清单:
  ✓ 无鼠标也能完成全部核心操作
  ✓ 触控手势都有「非手势等价路径」
  ✓ 键位可重绑定
  ✓ 不同分辨率下命中区域不变形

心智:跨平台输入 = 语义动作层统一 + 平台绑定映射分设——逻辑消费动作与轴向,控件按平台显隐,核心操作别依赖鼠标/手势独占。


10. 速查表

全篇速查:

主题结论
抽象动作是硬件到逻辑的「语义层」
绑定trigger 边沿 / continuous 轴向
分发单一管理器、UI 打开吞输入
触控逻辑坐标 + touch_id 多点
摇杆定圆心 + 半径钳制 + 死区
键鼠/手柄收敛成 -1~1 轴向
手势拖拽跟踪、捏合距离、滑动阈值
GUIpick_node 命中、按下+抬起判定
跨平台动作统一 + 绑定分设
手感输入缓冲 + 提前量兑现意图

一句话记忆:Defold 输入 = 「硬件 → 绑定文件 → 语义动作 → 逻辑」——trigger 管边沿、continuous 管轴向;触控用逻辑坐标 + touch_id 区分多点,摇杆定圆心 + 死区 + 半径钳制,键鼠手柄收敛成 -1~1 轴向;手势靠阈值判定、GUI 用 pick_node 命中、UI 打开吞世界输入;跨平台 = 动作层统一 + 绑定分设——手感一半是输入缓冲与提前量,别让玩家「按了没反应」毁掉一款游戏。


延伸阅读

  • /defold-script-system-lua/ — 消息、生命周期与输入分发
  • /defold-ui-gui/ — GUI 节点体系与 pick_node 命中
  • /defold-camera-viewport/ — 屏幕↔世界坐标换算
  • /defold-multiplayer-sync/ — 输入驱动与状态同步
  • 游戏开发专题 — 输入驱动的通用实践
  • 前端专题 — 触控与手势交互的移动端实践

继续阅读

探索更多技术文章

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

全部文章 返回首页

「defold」更多文章

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