第一部分 07:InputMap 输入系统
这一章讲 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_W、KEY_E、MOUSE_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_up:W、↑
move_down:S、↓
move_left:A、←
move_right:D、→
interact:E、Enter
attack:鼠标左键、J
open_inventory:B、I
pause:Esc
confirm:Enter、Space
cancel:Esc、Backspace
后面加技能系统时再加:
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()
更完整的做法是做一个 GameState 或 UIManager,后面讲架构时再展开。
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. 输入动作和游戏状态
项目稍微复杂后,输入会和状态有关。
比如:
正常状态:
move、attack、interact 生效
对话状态:
confirm、cancel 生效
move、attack 不生效
背包状态:
UI 点击、confirm、cancel 生效
move、attack 不生效
暂停状态:
pause、menu 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:
E、Enter
attack:
Mouse Left、J
dash:
Shift、K
open_inventory:
B、I
open_map:
M
pause:
Esc
confirm:
Enter、Space
cancel:
Esc、Backspace
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_up、interact、attack
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如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录