Godot 4 可交互机关门:用 Timer 与自定义信号解耦游戏逻辑

Godot 4 可交互机关门:用 Timer 与自定义信号解耦游戏逻辑

OK,OK,大家好,欢迎大家来到大鹏 AI 教育,我是张大鹏。

机关门是游戏里很常见的小功能:玩家进入感应区,门开始打开;几秒后自动关闭;UI、音效和任务系统还要同时收到状态变化。功能看起来不大,但如果所有逻辑都塞进门的脚本,项目很快就会变成节点之间互相查找、互相调用的"引用蜘蛛网"。

本文用 Godot 4 的 Timer 和自定义信号完成一个可复用机关门。重点不是做一段门动画,而是建立"事件发生后再通知其他系统"的解耦结构。

来源与证据

本文的技术主线来自《大鹏 AI GDScript 游戏脚本入门》中"使用 Godot 信号解耦游戏事件"一节,重点采用 Timer 超时、幂等信号连接、自定义信号和发送---连接---接收排错方法。机关门是对这些已验证机制的独立实战应用。

先把交互拆成三个角色

一个稳定的机关门至少包含三个职责:

  • Area3D 只负责感知玩家进入或离开;
  • DoorController 只负责门的状态与动画;
  • UI、音效和任务节点通过信号响应结果,不直接控制门。

门可以定义四个状态:

gdscript 复制代码
enum DoorState {
	CLOSED,
	OPENING,
	OPEN,
	CLOSING,
}

状态是当前事实,信号则用来表达"某个事实刚刚发生"。例如 door_openedopen_door 更适合作为信号名:前者是在广播结果,后者听起来像要求接收者执行命令。

配置输入动作,避免把按键写死

如果机关门还支持玩家按键开启,应先在 Project Settings 的 Input Map 中创建 interact,再绑定键盘、手柄或其他输入设备。脚本只读取动作名:

gdscript 复制代码
func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("interact") and not event.is_echo():
		request_open()
		get_viewport().set_input_as_handled()

这样以后把 E 改为空格键或手柄按钮,不需要修改脚本。event.is_echo() 可以避免长按键产生连续重复触发。

用 Timer 管理自动关闭

DoorController 下添加一个 Timer,命名为 AutoCloseTimer

  • One Shot:开启;
  • Autostart:关闭;
  • Wait Time:例如 2.5 秒。

门完全打开后启动计时器,超时后请求关闭。不要在 _process() 中手写倒计时,因为 Timer 的意图更清楚,也能直接通过信号连接。

gdscript 复制代码
@onready var auto_close_timer: Timer = $AutoCloseTimer

func _ready() -> void:
	if not auto_close_timer.timeout.is_connected(_on_auto_close_timeout):
		auto_close_timer.timeout.connect(_on_auto_close_timeout)

func _on_auto_close_timeout() -> void:
	request_close()

is_connected() 很重要。场景重新初始化或连接代码被重复调用时,它能避免同一个回调叠加多次。

定义门自己的领域信号

门不需要知道谁在显示提示、谁在播放音效。它只发布状态变化:

gdscript 复制代码
signal door_state_changed(previous: DoorState, current: DoorState)
signal door_opened
signal door_closed

var state: DoorState = DoorState.CLOSED

func _set_state(next_state: DoorState) -> void:
	if state == next_state:
		return

	var previous := state
	state = next_state
	door_state_changed.emit(previous, state)

状态切换函数先判断新旧状态是否相同,避免重复点击让"正在打开"的门再次进入打开流程。

完成打开和关闭流程

下面使用 AnimationPlayer 播放 openclose 动画:

gdscript 复制代码
@onready var animation_player: AnimationPlayer = $AnimationPlayer

func request_open() -> void:
	if state in [DoorState.OPEN, DoorState.OPENING]:
		return

	auto_close_timer.stop()
	_set_state(DoorState.OPENING)
	animation_player.play("open")

func request_close() -> void:
	if state in [DoorState.CLOSED, DoorState.CLOSING]:
		return

	_set_state(DoorState.CLOSING)
	animation_player.play("close")

func _on_animation_player_animation_finished(animation_name: StringName) -> void:
	if animation_name == &"open":
		_set_state(DoorState.OPEN)
		door_opened.emit()
		auto_close_timer.start()
	elif animation_name == &"close":
		_set_state(DoorState.CLOSED)
		door_closed.emit()

关键点是:不要在调用 play("open") 后立刻宣称门已经打开。动画开始只表示进入 OPENING;只有收到 animation_finished,才能把状态改为 OPEN 并发出 door_opened

让感应区只发出请求

Area3D 只判断进入的对象是否属于玩家组:

gdscript 复制代码
func _on_detection_area_body_entered(body: Node3D) -> void:
	if body.is_in_group("player"):
		request_open()

使用分组比写死某个玩家节点路径更稳。以后玩家场景换层级,只要仍属于 player 组,机关门就不需要修改。

如果多人停留在门口,不能在任意一个对象离开时直接关门。可以维护区域内玩家数量,只有计数归零后才允许启动关闭逻辑。

UI 和音效如何接入

外部节点只订阅门的信号:

gdscript 复制代码
func bind_door(door: Node) -> void:
	door.door_opened.connect(_on_door_opened)
	door.door_closed.connect(_on_door_closed)

func _on_door_opened() -> void:
	status_label.text = "机关门已打开"
	open_audio.play()

门脚本没有 StatusLabelAudioStreamPlayer 的路径,因此 UI 改版、音效节点移动,都不会破坏门的核心逻辑。

常见问题排查

回调执行两次

检查编辑器信号面板和代码连接是否同时存在。固定场景结构可以用编辑器连接,运行时对象适合代码连接,但同一信号不要两边都连。

门还没打开就自动关闭

确认计时器是在 open 动画结束后启动,而不是在 request_open() 中立即启动。

玩家离开后门反复开关

检查 Area3D 的碰撞层、遮罩和玩家分组,并确认场景中没有重复的感应区域实例。

删除回调后运行时报错

删除函数前先断开场景文件中保存的信号连接。函数不存在但连接仍存在时,运行时会找不到接收方法。

最终验收清单

运行场景后至少验证:

  1. 玩家第一次进入区域,门只打开一次;
  2. 动画结束后才发出 door_opened
  3. Timer 超时后只触发一次关闭;
  4. 连续按键不会重复连接或重复播放动画;
  5. UI、音效节点移除后,门仍能独立工作;
  6. 日志能看出 CLOSED → OPENING → OPEN → CLOSING → CLOSED 的完整状态变化。

总结

一个可交互机关门真正值得复用的,不是模型或动画,而是清晰的事件边界。Timer 管时间,自定义信号发布事实,状态机拒绝重复请求。三者组合后,机关门可以自然扩展到钥匙门、压力板、任务门和多人副本门,而不必把整个场景树绑在一起。

相关推荐
Python私教1 天前
Godot 4 CharacterBody2D 角色移动:输入、碰撞与八方向控制实战
人工智能·游戏引擎·godot·gdscript·游戏开发
Python私教1 天前
用 Godot 4 拆解 MOBA 技能:前摇、判定、后摇与冷却如何做成可靠状态机
godot
Python私教1 天前
Blender + Godot 4 制作 MOBA 防御塔:从低模、GLB 到碰撞与攻击范围
godot
Python私教4 天前
Godot 4 角色移动为什么要用 CharacterBody2D?从 Input Map 到碰撞滑动一次讲透
godot
Python私教4 天前
Godot 4 TileMapLayer 入门:从图集、碰撞到可玩的 2D 关卡
godot
Python私教4 天前
Godot图片怎么导入?PNG、压缩、Mipmap与Reimport一次讲清
游戏引擎·godot
Python私教4 天前
Blender模型导入Godot完整教程:GLB、BLEND与继承场景怎么选
运维·godot·blender