亲手做一个交互闭环:靠近、按键、反馈、离开
搭建单目标 Area2D 练习,并亲自检查长按重复和远距离误触发。
01前置与节点树:先把依赖摆到明处
本课在独立练习项目中完成,只有一个玩家和一个调查点,不是可直接扩展到多个重叠目标的交互框架。新建 Node2D 根节点 InteractionLab,子节点为 Player (CharacterBody2D) 和 Clue (Area2D)。Player 下放 CollisionShape2D 与 Sprite2D;Clue 下放 CollisionShape2D 与 Label,Label 写“纸条”。Sprite2D 可直接使用练习项目自带图标并缩小。
两处碰撞形状都在 Shape 属性中新建 RectangleShape2D,尺寸分别约 24×24 与 96×96。Player 加入 player 分组,碰撞层勾第 1 层;Clue 的碰撞掩码只勾第 1 层并保持 Monitoring 开启。把两者放在窗口内,起点不要重叠。
02第一步:单独验证移动,不把问题混在一起
在“项目设置 → 输入映射”添加 move_left、move_right、move_up、move_down,分别绑定方向键,再添加 interact 绑定 E。Player 使用下面完整脚本;先确认能移到纸条附近,再继续。速度交给 CharacterBody2D,move_and_slide() 不额外传入 delta。
extends CharacterBody2D
@export var speed: float = 160.0
func _physics_process(_delta: float) -> void:
velocity = Input.get_vector("move_left", "move_right",
"move_up", "move_down") * speed
move_and_slide()本课全部代码已核对 Godot 4.7 关键 API,未完成实际场景交互验证。若图标不见,先检查 Sprite2D 的 Texture 与位置,不要因此修改移动计算。
把整个 InteractionLab 保存后运行,不要单独运行只有玩家的子场景,否则根本没有调查目标。测试开始时先点一下游戏窗口取得键盘焦点,再按方向键;在脚本编辑器里按方向键只会移动光标。确认移动成功后停止运行,再修改调查脚本,避免把运行中临时变化当成已经保存的场景修改。
03第二步:连接进入和离开信号
给 Clue 挂上下面脚本。这里统一在代码中连接信号,不要再到编辑器重复连接。进入时只记录范围资格,按键时才调查,离开时取消资格。纸条节点不会因为检测到玩家就自动替玩家作决定。Label 的相对节点路径必须保持为 Clue 的直接子节点。
extends Area2D
var in_range: bool = false
var examined: bool = false
@onready var hint: Label = $Label
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
in_range = true
hint.text = "E:调查纸条"
func _on_body_exited(body: Node2D) -> void:
if body.is_in_group("player"):
in_range = false
hint.text = "纸条"
func _unhandled_input(event: InputEvent) -> void:
if not in_range or not event.is_action_pressed("interact"):
return
get_viewport().set_input_as_handled()
if examined:
hint.text = "你已经读过这张纸条"
return
examined = true
hint.text = "纸上写着:练习完成"
print("首次调查成功")04第三步:理解三道不同的边界
in_range 回答“当前够不够近”,examined 回答“首次调查是否已经发生”,二者不能合并成一个变量。is_action_pressed() 默认不接受键盘长按产生的回显事件,减少一次长按重复触发;已调查状态则防止玩家多次重新按键重复领取首次结果。
_unhandled_input 允许界面先处理输入,标记事件已处理可阻止后续事件接收者继续响应,但它不会阻止另一个脚本用 Input 单例轮询按键。本例没有对白框;以后加对白时还需显式的交互忙碌状态和统一输入归属,不能仅凭这行方法调用就声称“输入绝不会穿透”。
05第四步:用现象缩小排错范围
完全没有靠近提示时,打开调试菜单的可见碰撞形状,确认两块实际相交,再检查 Clue 的 Mask 是否包含 Player 所在 Layer,以及玩家分组拼写。不要把“自己属于哪层”和“我要检测哪层”反着理解。信号只检测物理碰撞对象,单独 Sprite2D 不是玩家物理体。
一次按键打印两次时,检查场景里是否实例化了两个 Clue、是否既用编辑器又用代码连接、是否其他脚本也在调查。若离开后还能触发,优先观察退出回调而非加计时器强行清除。范围列表按物理步更新,不要在移动后的同一行期待立即查询到所有新重叠对象。
06验收:四条路径都成立才算闭环
按顺序实测:远处按 E,没有调查;靠近出现提示;按住 E 三秒,首次成功只打印一次;离开再按 E,不应改变状态;重新靠近并按 E,只提示已读。把这五条预期和实际结果记录下来。保存场景并重新启动,当前例子的已读状态会重置,这是没有实现持久化的正常边界。
追加练习只做一件事:把首次反馈换成一条你自己的练习文本,不添加正式剧情。多个调查范围重叠时,本课方案没有“选择最近目标”的规则;以后应由单一交互控制器维护候选并明确选择,而不是复制十个自行抢按键的节点后再猜谁先执行。
动手后,再勾选
不是“我看懂了”,而是你在独立练习里做出并检查过。
换一个情景,你会怎么判断?
调用 set_input_as_handled() 后,其他脚本仍用 Input.is_action_pressed() 轮询同一个键,能保证它不响应吗?
用自己的话,留一条笔记
可以写“我之前以为……,现在发现……”,或者记录还没解决的问题。
↗回到资料核对
本课是解释与练习,不替代对应版本的官方手册。网页实验不运行 Godot;未标明实机验证的代码,仍需在独立工程中检查。
标记已读不会自动勾选实作,也不代表游戏功能已经完成。