今天主要的任务是为敌人 LavaGolem 添加基于状态机的 AI 行为逻辑,并解决敌人动画朝向与实际移动方向不一致的问题。 之前的问题包括:
敌人的精灵图在运行动画时会朝反方向移动。
翻转精灵图后,RayCast2D 的检测方向和碰撞逻辑与精灵方向不一致。
需要保证动画、碰撞检测器和逻辑方向保持一致。
2. 状态机结构设计
定义了 State 枚举来描述 LavaGolem 的三种主要行为:
1 2 3 4 5
enum State { IDLE, # 原地站立 WALK, # 巡逻行走 RUN # 追击玩家 }
状态目标:
IDLE:站立不动,等待巡逻或发现玩家。
WALK:低速巡逻,遇到墙或悬崖掉头。
RUN:高速追击玩家,遇到墙或悬崖掉头。
3. 脚本实现细节
利用 playerchecker(RayCast2D)检测玩家是否在视线范围内:
1 2 3 4
func can_see_player() -> bool: if not playerchecker.is_colliding(): return false return playerchecker.get_collider() is Player
状态物理逻辑
tick_physics() 根据当前状态执行不同速度和移动逻辑:
1 2 3 4 5 6 7 8 9 10 11 12
func tick_physics(state: State, delta: float) -> void: match state: State.IDLE: move(0.0, delta) State.WALK: move(max_speed / 3, delta) State.RUN: if wallchecker.is_colliding() or not floorchecker.is_colliding(): direction *= -1 move(max_speed, delta) if can_see_player(): calm_down_timer.start()
状态切换规则
get_next_state() 用来判断何时切换状态:
IDLE → WALK:等待超过 2 秒。
WALK → IDLE:遇到墙或悬崖。
RUN → WALK:冷静计时结束。
玩家在视线范围内时,优先进入 RUN 状态。
动画与翻转同步
transition_state() 中播放对应动画,并在需要时调整方向:
1 2 3 4 5 6 7 8 9 10 11 12 13
func transition_state(from: State, to: State) -> void: match to: State.IDLE: animation_player.play("idle") if wallchecker.is_colliding(): direction *= -1 State.WALK: animation_player.play("walk") if not floorchecker.is_colliding(): direction *= -1 floorchecker.force_raycast_update() State.RUN: animation_player.play("run")
@onready var wallchecker: RayCast2D = $Graphics/Wallchecker @onready var playerchecker: RayCast2D = $Graphics/Playerchecker @onready var floorchecker: RayCast2D = $Graphics/Floorchecker @onready var calm_down_timer: Timer = $CalmDownTimer
func _ready() -> void: scale.x *= -1
func can_see_player() -> bool: if not playerchecker.is_colliding(): return false return playerchecker.get_collider() is Player
func tick_physics(state: State, delta: float) -> void: match state: State.IDLE: move(0.0, delta) State.WALK: move(max_speed / 3, delta) State.RUN: if wallchecker.is_colliding() or not floorchecker.is_colliding(): direction *= -1 move(max_speed, delta) if can_see_player(): calm_down_timer.start()
func get_next_state(state: State) -> State: if can_see_player(): return State.RUN match state: State.IDLE: if state_machine.state_time > 2: return State.WALK State.WALK: if wallchecker.is_colliding() or not floorchecker.is_colliding(): return State.IDLE State.RUN: if calm_down_timer.is_stopped(): return State.WALK return state
func transition_state(from: State, to: State) -> void: match to: State.IDLE: animation_player.play("idle") if wallchecker.is_colliding(): direction *= -1 State.WALK: animation_player.play("walk") if not floorchecker.is_colliding(): direction *= -1 floorchecker.force_raycast_update() State.RUN: animation_player.play("run")