第一部分 07:InputMap 输入系统

2026-05-19
408314 分钟
...

这一章讲 Godot 的输入系统。

先记一句话:

不要直接在代码里绑定 W/A/S/D而是先在 InputMap 里定义动作名然后代码只判断动作名

比如不要长期这样写:

if Input.is_key_pressed(KEY_W):
    move_up()

更推荐这样写:

if Input.is_action_pressed("move_up"):
    move_up()

原因很简单:

W 键只是一个具体按键
move_up 才是游戏行为

Godot 的 Input 单例负责处理键盘、鼠标、手柄和输入动作;这些动作可以在 Project > Project Settings > Input Map 里配置,也可以通过 InputMap 类用代码配置。(Godot Engine documentation)

1. InputMap 是什么

InputMap 可以理解为“输入映射表”。

它把具体输入设备上的按键,映射成游戏里的动作。

比如:

move_up      = W // 手柄左摇杆上
move_down    = S // 手柄左摇杆下
move_left    = A // 手柄左摇杆左
move_right   = D // 手柄左摇杆右
interact     = E / Enter / 手柄 A
attack       = 鼠标左键 / J / 手柄 X
open_bag     = B / I
pause        = Esc / Start

这样你的代码只关心:

Input.is_action_pressed("move_up")

而不关心玩家到底按的是 W、方向键,还是手柄摇杆。

官方文档也强调,InputMap 是处理各种输入的灵活方式:你先创建具名的 input action,然后给这个 action 分配键盘、鼠标点击等输入事件。(Godot Engine documentation)

2. 为什么不直接监听具体按键

比如你直接写:

if Input.is_key_pressed(KEY_W):
    direction.y -= 1

短期看起来没问题,但后期会很难维护。

因为你以后可能会遇到:

玩家想改键位
同时支持 WASD 和方向键
支持手柄
支持手机虚拟按键
不同平台按键不一样
UI 提示要显示当前绑定键

如果代码里到处都是 KEY_WKEY_EMOUSE_BUTTON_LEFT,后期就像在一锅热粥里找一粒米,能找,但心态会糊。

更好的方式是:

if Input.is_action_pressed("move_up"):
    direction.y -= 1

这样以后你想把 move_up 从 W 改成方向键,代码不用动,只改 InputMap。

3. 前端类比

前端里你可能会写:

window.addEventListener("keydown", (event) => {
  if (event.key === "w") {
    moveUp()
  }
})

这是直接监听具体按键。

Godot 的 InputMap 更像你先定义一层语义:

const keyMap = {
  moveUp: ["w", "ArrowUp"],
  interact: ["e", "Enter"],
}

然后游戏逻辑只判断:

if (isActionPressed("moveUp")) {
  moveUp()
}

也就是说:

具体按键是输入层
动作名是游戏逻辑层

Godot 推荐你把这两层分开。

4. 在编辑器里配置 InputMap

操作路径:

Project
Project Settings
Input Map

然后:

1. 在顶部输入动作名
2. 点击 Add 添加动作
3. 在动作右边添加按键鼠标手柄输入
4. 代码里使用这个动作名

Godot 官方教程里也说明,Input Map 窗口允许在顶部添加新的 actions;这些 action 是你的标签,底部可以把按键绑定到这些 action 上。(Godot Engine documentation)

5. 推荐的动作命名

动作名建议用英文小写 + 下划线。

推荐:

move_up
move_down
move_left
move_right
interact
attack
open_inventory
pause
confirm
cancel
dash
use_skill

不太推荐:

w
e
left_click
key_space
玩家移动上
按E交互

原因是动作名应该表达“行为”,不是表达“按键”。

move_up 是行为
W 是按键

interact 是行为
E 是按键

attack 是行为
鼠标左键是输入方式

6. 默认动作 ui_up、ui_down、ui_left、ui_right

Godot 新项目通常有一些内置 UI 动作,比如:

ui_up
ui_down
ui_left
ui_right
ui_accept
ui_cancel

这些常用于 UI 菜单导航、确认、取消。Godot 文档也提到,新项目已经包含一些默认 action,可以在 InputMap 对话框里开启显示内置动作查看。(Godot Engine documentation)

刚入门时,用这些也可以:

if Input.is_action_pressed("ui_right"):
    direction.x += 1

但我更建议你做正式游戏时,自己建一套:

move_up
move_down
move_left
move_right
interact
attack
open_inventory
pause

原因是:

ui_up 更偏 UI 导航
move_up 更偏角色移动

这俩语义分开,后期做菜单、对话框、背包时更清楚。

7. RPG 项目推荐 InputMap

你的 2D RPG 可以先配置这些:

move_up
move_down
move_left
move_right
interact
attack
open_inventory
pause
confirm
cancel

推荐绑定:

move_upW、↑
move_downS、↓
move_leftA、←
move_rightD、→

interactEEnter
attack鼠标左键J
open_inventoryBI
pauseEsc
confirmEnterSpace
cancelEscBackspace

后面加技能系统时再加:

skill_1
skill_2
skill_3
dash
lock_target

8. Input.is_action_pressed

它是什么

Input.is_action_pressed("move_right")

意思是:

move_right 这个动作现在是否正在被按住

它适合持续行为。

比如:

角色移动
按住奔跑
按住蓄力
按住瞄准
持续拖拽

玩家移动例子

extends CharacterBody2D

@export var speed: float = 120.0

func _physics_process(delta: float) -> void:
    var direction := Vector2.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_down"):
        direction.y += 1
    if Input.is_action_pressed("move_up"):
        direction.y -= 1

    velocity = direction.normalized() * speed
    move_and_slide()

这就是最基础的 RPG 角色移动。

9. Input.is_action_just_pressed

它是什么

Input.is_action_just_pressed("interact")

意思是:

interact 这个动作是否刚刚按下

重点是“刚刚”。

它只会在按下的那一瞬间返回 true。

适合一次性行为。

比如:

交互
攻击
跳跃
打开背包
暂停游戏
确认对话
拾取物品

打开背包例子

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("open_inventory"):
        toggle_inventory()

也可以写在 _process 里:

func _process(delta: float) -> void:
    if Input.is_action_just_pressed("open_inventory"):
        toggle_inventory()

但对于一次性输入,我更推荐用 _unhandled_input(event),后面会讲原因。

为什么不能用 is_action_pressed 打开背包

错误写法:

func _process(delta: float) -> void:
    if Input.is_action_pressed("open_inventory"):
        inventory.visible = !inventory.visible

这会导致你按住按键的一小段时间里,它每帧都切换一次:

肉眼看就是疯狂闪烁。

正确写法:

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("open_inventory"):
        inventory.visible = !inventory.visible

或者:

func _process(delta: float) -> void:
    if Input.is_action_just_pressed("open_inventory"):
        inventory.visible = !inventory.visible

10. Input.is_action_just_released

它是什么

Input.is_action_just_released("attack")

意思是:

attack 这个动作是否刚刚松开

适合:

松开鼠标发射弓箭
松开按键释放蓄力技能
松开拖拽物品
松开按钮结束瞄准

蓄力攻击例子

var charging: bool = false
var charge_time: float = 0.0

func _process(delta: float) -> void:
    if Input.is_action_just_pressed("attack"):
        charging = true
        charge_time = 0.0

    if Input.is_action_pressed("attack") and charging:
        charge_time += delta

    if Input.is_action_just_released("attack") and charging:
        charging = false
        release_attack(charge_time)

func release_attack(time: float) -> void:
    print("释放攻击,蓄力时间:", time)

11. event.is_action_pressed

这个和 Input.is_action_pressed 很像,但用法不一样。

Input.is_action_pressed

主动查询当前输入状态:

if Input.is_action_pressed("move_right"):
    direction.x += 1

适合持续行为。

event.is_action_pressed

判断“当前这个输入事件”是不是某个动作:

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("interact"):
        interact()

适合输入回调函数。

官方 InputEvent 文档提到,事件对象有 is_action()is_pressed()is_echo() 等方法;动作可以在 Project Settings 的 Input Map 标签中创建并分配输入事件。(Godot Engine documentation)

12. _input 和 _unhandled_input 怎么选

你前面已经见过这两个:

func _input(event: InputEvent) -> void:
    pass

func _unhandled_input(event: InputEvent) -> void:
    pass

简单记:

_input
比较早收到输入适合全局输入底层输入需要优先处理的输入

_unhandled_input
只有输入还没被 UI 或其他逻辑处理时才执行适合游戏角色输入

对于 RPG,我建议:

玩家交互攻击打开背包
优先用 _unhandled_input

UI 按钮
优先用 Button pressed 信号

角色持续移动
 _physics_process + Input.is_action_pressed

13. 为什么推荐 _unhandled_input

假设你打开了背包 UI。

背包里有按钮、物品格子、关闭按钮。

这时候你按鼠标左键,应该是点击 UI,而不是让角色攻击。

如果你把攻击逻辑写在 _input 里,它可能会比 UI 更早拿到输入,容易出现:

点背包物品时角色也攻击了
点菜单按钮时角色也交互了
输入穿透到游戏世界

_unhandled_input 的优势是:

UI 先处理
没人处理时游戏世界再处理

Godot 的 Input 文档提醒,Input 单例的方法反映的是全局输入状态,不受 Control.accept_event()Viewport.set_input_as_handled() 影响;这些处理只影响输入在场景树中的传播。也就是说,做 UI 和游戏输入隔离时,要注意全局查询和事件传播是两套东西。(Godot Engine documentation)

14. 玩家移动为什么不用 _unhandled_input

因为移动是持续行为。

玩家按住 W 时,角色应该每个物理帧都向上移动。

如果只在输入事件发生时处理,就不稳定。

推荐:

func _physics_process(delta: float) -> void:
    var direction := Vector2.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_down"):
        direction.y += 1
    if Input.is_action_pressed("move_up"):
        direction.y -= 1

    velocity = direction.normalized() * speed
    move_and_slide()

也就是说:

持续移动
_physics_process + Input.is_action_pressed

一次性交互
_unhandled_input + event.is_action_pressed

这个分工非常重要。

15. Input.get_vector:更简洁的移动输入

Godot 有一个很适合移动的写法:

var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")

它会直接返回一个 Vector2

完整移动代码可以写成:

extends CharacterBody2D

@export var speed: float = 120.0

func _physics_process(delta: float) -> void:
    var direction := Input.get_vector(
        "move_left",
        "move_right",
        "move_up",
        "move_down"
    )

    velocity = direction * speed
    move_and_slide()

这比手动写四个 if 更简洁。

手动写法

var direction := Vector2.ZERO

if Input.is_action_pressed("move_right"):
    direction.x += 1
if Input.is_action_pressed("move_left"):
    direction.x -= 1
if Input.is_action_pressed("move_down"):
    direction.y += 1
if Input.is_action_pressed("move_up"):
    direction.y -= 1

velocity = direction.normalized() * speed

get_vector 写法

var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed

入门阶段先理解手动写法,熟了之后用 get_vector 很舒服。

16. get_vector 参数顺序

这个顺序要记住:

Input.get_vector(negative_x, positive_x, negative_y, positive_y)

也就是:

Input.get_vector("move_left", "move_right", "move_up", "move_down")

对应:

move_left   = x - 1
move_right  = x + 1
move_up     = y - 1
move_down   = y + 1

注意 Godot 2D 坐标里:

向右 x 增加
向下 y 增加
向上 y 减少

所以:

move_up negative_y
move_down positive_y

17. 一个更完整的玩家输入脚本

class_name Player
extends CharacterBody2D

@export var speed: float = 120.0

@onready var animation_player: AnimationPlayer = $AnimationPlayer

var last_direction: Vector2 = Vector2.DOWN

func _ready() -> void:
    animation_player.play("idle_down")

func _physics_process(delta: float) -> void:
    var direction := Input.get_vector(
        "move_left",
        "move_right",
        "move_up",
        "move_down"
    )

    if direction != Vector2.ZERO:
        last_direction = direction
        velocity = direction * speed
        play_walk_animation(direction)
    else:
        velocity = Vector2.ZERO
        play_idle_animation(last_direction)

    move_and_slide()

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("interact"):
        try_interact()

    if event.is_action_pressed("attack"):
        attack()

    if event.is_action_pressed("open_inventory"):
        open_inventory()

func try_interact() -> void:
    print("尝试交互")

func attack() -> void:
    print("攻击")

func open_inventory() -> void:
    print("打开背包")

func play_walk_animation(direction: Vector2) -> void:
    if abs(direction.x) > abs(direction.y):
        if direction.x > 0:
            animation_player.play("walk_right")
        else:
            animation_player.play("walk_left")
    else:
        if direction.y > 0:
            animation_player.play("walk_down")
        else:
            animation_player.play("walk_up")

func play_idle_animation(direction: Vector2) -> void:
    if abs(direction.x) > abs(direction.y):
        if direction.x > 0:
            animation_player.play("idle_right")
        else:
            animation_player.play("idle_left")
    else:
        if direction.y > 0:
            animation_player.play("idle_down")
        else:
            animation_player.play("idle_up")

这个脚本的结构很清楚:

_physics_process
负责持续移动

_unhandled_input
负责一次性动作

get_vector
负责把方向输入变成 Vector2

animation_player
根据方向播放动画

18. 交互键 interact 的常见写法

RPG 里最常用的输入之一就是交互键。

比如:

靠近 NPC E 对话
靠近宝箱 E 打开
靠近门 E 进入
靠近采集物 E 采集

常见写法是:

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("interact"):
        try_interact()

然后:

func try_interact() -> void:
    if current_interactable == null:
        return

    current_interactable.interact()

完整一点:

extends CharacterBody2D

var current_interactable: Node = null

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("interact"):
        try_interact()

func try_interact() -> void:
    if current_interactable == null:
        return

    if current_interactable.has_method("interact"):
        current_interactable.interact()

这里的思路是:

玩家不关心对方是 NPC宝箱还是门
玩家只关心它有没有 interact 方法

这个思路很适合做 RPG。

19. Area2D + interact 输入

玩家可以用 Area2D 检测附近可交互对象。

结构:

Player CharacterBody2D
├── BodySprite Sprite2D
├── CollisionShape2D
└── InteractArea Area2D
    └── CollisionShape2D

脚本:

extends CharacterBody2D

var current_interactable: Node = null

func _ready() -> void:
    $InteractArea.body_entered.connect(_on_interact_area_body_entered)
    $InteractArea.body_exited.connect(_on_interact_area_body_exited)
    $InteractArea.area_entered.connect(_on_interact_area_area_entered)
    $InteractArea.area_exited.connect(_on_interact_area_area_exited)

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("interact"):
        try_interact()

func try_interact() -> void:
    if current_interactable == null:
        return

    if current_interactable.has_method("interact"):
        current_interactable.interact()

func _on_interact_area_body_entered(body: Node2D) -> void:
    if body.has_method("interact"):
        current_interactable = body

func _on_interact_area_body_exited(body: Node2D) -> void:
    if body == current_interactable:
        current_interactable = null

func _on_interact_area_area_entered(area: Area2D) -> void:
    if area.has_method("interact"):
        current_interactable = area

func _on_interact_area_area_exited(area: Area2D) -> void:
    if area == current_interactable:
        current_interactable = null

这个版本能检测:

StaticBody2D 类型的宝箱
Area2D 类型的触发点
CharacterBody2D 类型的 NPC

后面项目复杂后,还可以用 group 或接口式写法优化。

20. 宝箱的 interact 方法

宝箱脚本:

class_name Chest
extends StaticBody2D

@export var reward_gold: int = 100

@onready var animation_player: AnimationPlayer = $AnimationPlayer

var opened: bool = false

func interact() -> void:
    if opened:
        return

    opened = true
    animation_player.play("open")
    print("打开宝箱,获得金币:", reward_gold)

玩家只要调用:

current_interactable.interact()

宝箱就会自己处理打开逻辑。

这比让 Player 直接写:

chest.open()
player.gold += chest.reward_gold

要清爽很多。

21. NPC 的 interact 方法

NPC 脚本:

class_name NPC
extends CharacterBody2D

@export var npc_name: String = "村民"
@export var dialog_text: String = "你好,旅行者。"

func interact() -> void:
    print(npc_name + ":" + dialog_text)

玩家靠近 NPC,按 interact,就调用 NPC 自己的 interact()

22. 门的 interact 方法

门脚本:

class_name Door
extends Area2D

@export var target_scene_path: String = "res://scenes/world/house_inside.tscn"

func interact() -> void:
    get_tree().change_scene_to_file(target_scene_path)

这样:

宝箱NPC门都有 interact 方法
玩家只负责调用 interact
具体怎么交互由对象自己决定

这个就是很干净的架构味儿了,香,真的香。

23. 攻击输入 attack

攻击通常也是一次性输入。

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("attack"):
        attack()

攻击方法:

func attack() -> void:
    animation_player.play("attack")
    print("攻击")

如果有攻击冷却:

@export var attack_cooldown: float = 0.4

var can_attack: bool = true

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("attack"):
        attack()

func attack() -> void:
    if not can_attack:
        return

    can_attack = false
    animation_player.play("attack")

    await get_tree().create_timer(attack_cooldown).timeout
    can_attack = true

24. 打开背包 open_inventory

推荐动作名:

open_inventory

代码:

@onready var inventory_panel: Control = $UI/InventoryPanel

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("open_inventory"):
        toggle_inventory()

func toggle_inventory() -> void:
    inventory_panel.visible = !inventory_panel.visible

如果背包打开时不让玩家移动:

var input_locked: bool = false

func toggle_inventory() -> void:
    inventory_panel.visible = !inventory_panel.visible
    input_locked = inventory_panel.visible

func _physics_process(delta: float) -> void:
    if input_locked:
        velocity = Vector2.ZERO
        move_and_slide()
        return

    var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

更完整的做法是做一个 GameStateUIManager,后面讲架构时再展开。

25. pause 暂停输入

推荐动作名:

pause

绑定:

Esc
Start

代码:

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("pause"):
        toggle_pause()

func toggle_pause() -> void:
    get_tree().paused = !get_tree().paused

不过暂停系统有个坑:

游戏暂停后很多节点不再处理输入
但暂停菜单需要继续处理输入

所以暂停菜单通常要设置自己的 process mode,让它在暂停时仍然能运行。

这个后面讲“暂停系统”时单独开一章,不然这里会岔太远。

26. 鼠标输入

Godot 也可以把鼠标绑定到动作。

比如:

attack = 鼠标左键

然后代码仍然是:

if event.is_action_pressed("attack"):
    attack()

也可以直接判断鼠标事件:

func _input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
            print("鼠标左键按下")

不过角色攻击这类游戏行为,还是推荐用 InputMap:

attack

这样玩家以后可以改成:

鼠标左键
J
手柄 X

代码完全不用改。

27. 鼠标位置

获取鼠标在当前视口的位置:

var mouse_pos := get_viewport().get_mouse_position()

获取鼠标在 2D 世界中的位置,常用于瞄准、点击地图:

var world_mouse_pos := get_global_mouse_position()

比如让角色朝向鼠标:

func _process(delta: float) -> void:
    look_at(get_global_mouse_position())

俯视角 RPG 如果有鼠标瞄准、法术落点、点击移动,会经常用这个。

28. 手柄输入

InputMap 的优势之一就是天然适合支持手柄。

比如:

move_up     = W // 左摇杆上
move_down   = S // 左摇杆下
interact    = E / 手柄 A
attack      = 鼠标左键 / 手柄 X
pause       = Esc / Start

代码仍然是:

Input.is_action_pressed("move_up")

或者:

Input.get_vector("move_left", "move_right", "move_up", "move_down")

这就是动作抽象的价值。

29. 输入和 UI 的关系

UI 节点,比如 Button,通常不需要你自己写输入判断。

按钮用信号:

func _ready() -> void:
    $StartButton.pressed.connect(_on_start_button_pressed)

func _on_start_button_pressed() -> void:
    print("开始游戏")

不要这样写按钮:

func _input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_accept"):
        start_game()

除非你确实在做键盘导航或自定义 UI 控件。

普通按钮点击:

Button.pressed 信号

自定义 UI 点击:

_gui_input(event)

游戏世界输入:

_unhandled_input(event)

持续移动:

_physics_process + Input.is_action_pressed / Input.get_vector

30. _gui_input:UI 自己处理输入

比如背包格子:

InventorySlot TextureRect

脚本:

extends TextureRect

func _gui_input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
            print("点击背包格子")

适合:

背包格子
技能栏
拖拽物品
自定义按钮
鼠标悬浮提示

普通 Button 节点不需要这么写,用 pressed 就行。

31. 输入锁 input_locked

很多 RPG 都需要输入锁。

比如:

对话中不能移动
背包打开时不能移动
过场动画时不能操作
玩家死亡时不能操作
场景切换时不能操作

简单写法:

var input_locked: bool = false

func _physics_process(delta: float) -> void:
    if input_locked:
        velocity = Vector2.ZERO
        move_and_slide()
        return

    var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

func _unhandled_input(event: InputEvent) -> void:
    if input_locked:
        return

    if event.is_action_pressed("interact"):
        try_interact()

这样只要:

input_locked = true

玩家就不能移动和交互。

32. 对话时锁输入

比如:

func start_dialog() -> void:
    input_locked = true
    dialog_panel.visible = true

func end_dialog() -> void:
    dialog_panel.visible = false
    input_locked = false

如果对话框自己需要按确认键推进文本,可以让对话框处理输入,而不是玩家处理。

extends Control

signal dialog_finished

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("confirm"):
        show_next_line()

这里要注意:对话框作为 UI 层最好有自己的输入管理,不要所有输入都塞到 Player。

33. 输入动作和游戏状态

项目稍微复杂后,输入会和状态有关。

比如:

正常状态
moveattackinteract 生效

对话状态
confirmcancel 生效
moveattack 不生效

背包状态
UI 点击confirmcancel 生效
moveattack 不生效

暂停状态
pausemenu navigation 生效
游戏世界停止

可以先用简单枚举:

enum GameMode {
    PLAYING,
    DIALOG,
    INVENTORY,
    PAUSED
}

var game_mode: GameMode = GameMode.PLAYING

玩家输入:

func _unhandled_input(event: InputEvent) -> void:
    if game_mode != GameMode.PLAYING:
        return

    if event.is_action_pressed("interact"):
        try_interact()

    if event.is_action_pressed("attack"):
        attack()

移动:

func _physics_process(delta: float) -> void:
    if game_mode != GameMode.PLAYING:
        velocity = Vector2.ZERO
        move_and_slide()
        return

    var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

这是比到处写 input_locked 更清晰的做法。

34. 运行时改键位

Godot 支持通过 InputMap 在代码里添加、修改动作和输入事件。InputMap 是一个管理 InputEventAction 的单例,动作可以从 Project Settings 的 Input Map 创建,也可以在代码中通过 add_action()action_add_event() 等方法管理。(Godot Engine documentation)

简单示意:

func add_custom_jump_key() -> void:
    if not InputMap.has_action("jump"):
        InputMap.add_action("jump")

    var event := InputEventKey.new()
    event.keycode = KEY_SPACE

    InputMap.action_add_event("jump", event)

不过,运行时改键位涉及保存玩家设置。

官方文档也提醒,运行时修改 InputMap 的状态不会自动保存;动态输入配置需要开发者自己决定如何保存设置。(Godot Engine documentation)

入门阶段先用编辑器配置 InputMap 就够了。

35. 输入动作配置建议

移动相关

move_up
move_down
move_left
move_right

不要用:

w
s
a
d

战斗相关

attack
dash
block
skill_1
skill_2
skill_3

交互相关

interact
pickup
talk
confirm
cancel

实际上 interact 可以覆盖很多事情:

对话
开门
开宝箱
采集
调查
拾取

刚开始不需要拆太细。

UI 相关

open_inventory
open_map
pause
confirm
cancel

36. 一个比较完整的 RPG InputMap 表

move_up
W、↑

move_down
S、↓

move_left
A、←

move_right
D、→

interact
EEnter

attack
Mouse LeftJ

dash
ShiftK

open_inventory
BI

open_map
M

pause
Esc

confirm
EnterSpace

cancel
EscBackspace

37. 输入代码推荐结构

Player 只管玩家输入:

class_name Player
extends CharacterBody2D

@export var speed: float = 120.0

var input_locked: bool = false

func _physics_process(delta: float) -> void:
    handle_movement()

func _unhandled_input(event: InputEvent) -> void:
    handle_actions(event)

func handle_movement() -> void:
    if input_locked:
        velocity = Vector2.ZERO
        move_and_slide()
        return

    var direction := Input.get_vector(
        "move_left",
        "move_right",
        "move_up",
        "move_down"
    )

    velocity = direction * speed
    move_and_slide()

func handle_actions(event: InputEvent) -> void:
    if input_locked:
        return

    if event.is_action_pressed("interact"):
        try_interact()

    if event.is_action_pressed("attack"):
        attack()

func try_interact() -> void:
    print("交互")

func attack() -> void:
    print("攻击")

UI 自己管 UI 输入:

class_name InventoryPanel
extends Control

func _ready() -> void:
    visible = false

func open() -> void:
    visible = true

func close() -> void:
    visible = false

func _unhandled_input(event: InputEvent) -> void:
    if visible and event.is_action_pressed("cancel"):
        close()

GameManager 管全局输入:

class_name GameManager
extends Node

@onready var inventory_panel: InventoryPanel = $"../UI/InventoryPanel"

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("open_inventory"):
        inventory_panel.visible = !inventory_panel.visible

    if event.is_action_pressed("pause"):
        get_tree().paused = !get_tree().paused

实际项目里,职责可以继续优化,但先这样分已经比全塞 Player 好很多。

38. 常见错误

错误 1:直接写死按键

不推荐:

if Input.is_key_pressed(KEY_W):
    direction.y -= 1

推荐:

if Input.is_action_pressed("move_up"):
    direction.y -= 1

错误 2:用 pressed 做一次性开关

不推荐:

func _process(delta: float) -> void:
    if Input.is_action_pressed("open_inventory"):
        inventory.visible = !inventory.visible

推荐:

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("open_inventory"):
        inventory.visible = !inventory.visible

或者:

func _process(delta: float) -> void:
    if Input.is_action_just_pressed("open_inventory"):
        inventory.visible = !inventory.visible

错误 3:移动放 _input

不推荐:

func _input(event: InputEvent) -> void:
    if event.is_action_pressed("move_right"):
        position.x += 10

推荐:

func _physics_process(delta: float) -> void:
    var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

错误 4:UI 打开后角色还能动

需要加状态判断:

if input_locked:
    velocity = Vector2.ZERO
    move_and_slide()
    return

或者用全局游戏状态:

if game_mode != GameMode.PLAYING:
    return

错误 5:点击 UI 同时触发游戏攻击

攻击逻辑别随便写在 _input

更推荐:

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("attack"):
        attack()

同时 UI 控件自己处理 UI 输入。

39. 这一章你先记住这些

InputMap
输入动作映射表

action
游戏行为名比如 move_upinteractattack

Input.is_action_pressed
动作正在按住适合持续行为

Input.is_action_just_pressed
动作刚刚按下适合一次性行为

Input.is_action_just_released
动作刚刚松开适合蓄力释放拖拽结束

Input.get_vector
快速获取四方向移动向量

_input
较早处理输入

_unhandled_input
输入没被 UI 等处理后再处理适合游戏操作

_gui_input
UI 控件自己的输入处理

40. 最重要的一句话

InputMap 让代码关心玩家想做什么”,而不是玩家按了哪个键”。

再压缩一下:

移动_physics_process + Input.get_vector
交互_unhandled_input + event.is_action_pressed("interact")
攻击_unhandled_input + event.is_action_pressed("attack")
UI 按钮pressed 信号
UI 自定义点击_gui_input

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录