今日速览
- 敌人 AI 的复杂度不在「智能」,而在状态之间的切换。用 8 个
is_chasing/is_attacking这类 bool 撑不过两周,必然出现「边攻击边巡逻」的鬼畜行为。 - 状态机的三条铁律:任意时刻只有一个状态、切换前先 exit 当前状态、未定义转移的输入一律忽略。
NavigationAgent2D从不移动父节点,它只负责算路径。移动必须自己写在_physics_process里,get_next_path_position()每个物理帧只调一次。- 在
_ready()里设置target_position拿到的常常是空路径——导航地图还没同步。要用call_deferred或等navigation_map_changed。 - 敌人「原地左右抽搐」的经典原因:每帧重设
target_position,或path_max_distance设得比移动速度还小。
核心学习内容
一、bool 标志位的崩塌
Game Programming Patterns 的 State 章节把这套失败过程讲得很透:给角色加 is_jumping,修好二段跳;再加 is_ducking,出现「跳跃中松手变站立」;再加俯冲,出现「俯冲中能再跳」。每加一个动作就要动两个方法、加一个字段,最后没人敢改。
判据很简单:当你写下一堆「同一时刻只有一个为 true」的 bool 时,你要的其实就是枚举。
二、Godot 4 最小可用状态机
extends CharacterBody2D
enum State { PATROL, ALERT, CHASE, ATTACK }
var state: State = State.PATROL
var target: Node2D
func _physics_process(delta: float) -> void:
match state:
State.PATROL: _do_patrol(delta)
State.ALERT: _do_alert(delta)
State.CHASE: _do_chase(delta)
State.ATTACK: _do_attack(delta)
move_and_slide()
# 唯一入口:所有切换都必须走这里
func change_state(next: State) -> void:
if next == state:
return
_exit_state(state)
state = next
_enter_state(next)
func _enter_state(s: State) -> void:
match s:
State.ALERT: $Anim.play("alert"); alert_timer.start()
State.CHASE: $Anim.play("run")
State.ATTACK: $Anim.play("attack")
State.PATROL: $Anim.play("walk")
func _exit_state(s: State) -> void:
match s:
State.ALERT: alert_timer.stop() # 不清理就会出现「已经追上了但警戒计时器又把状态拉回去」
State.ATTACK: $Anim.speed_scale = 1.0
关键点:_enter_state / _exit_state 成对出现,把「这个状态独占的资源」放进去——计时器、动画、音效、速度倍率。90% 的状态机 bug 都是漏了 exit。
三、接上 NavigationAgent2D 追击
func _do_chase(_delta: float) -> void:
if nav.is_navigation_finished():
change_state(State.ATTACK)
return
# 顺序很重要:先判 finished,再取下一个路点
var next := nav.get_next_path_position()
velocity = global_position.direction_to(next) * speed
func _ready() -> void:
# 延后一帧,避免导航地图未同步导致空路径
call_deferred("_acquire_target")
func _acquire_target() -> void:
nav.target_position = target.global_position
path_desired_distance 建议设成 4–8 像素(不要 1 像素),target_desired_distance 设成攻击距离。追击时不要每帧重设 target_position,改为每 0.2–0.3 秒更新一次,否则路径反复重算,敌人会在两个点之间跳舞。
四、四个高频坑清单
- 空路径:
_ready()里查路径 → 用 deferred 调用。 - 原地抽搐:每帧重设目标,或
path_max_distance太小(应大于单帧位移的 5–10 倍)。 - 贴墙抖动:
move_and_slide()后没有回写velocity,改用direction_to()每帧重算而不是累加。 - 状态回弹:警戒计时器、受击硬直这类「有时间窗的状态」必须在 exit 里停掉。
🎯 DeepSeek 提示词
提示词 1 · 生成敌人 AI 状态机骨架
你是一位有 8 年经验的动作游戏客户端程序员,精通 Godot 4.x 与 GDScript。
任务:为一个 2D 横版动作游戏的「近战敌人」生成有限状态机(FSM)骨架。
输入(我会填写):可用状态列表、每个状态的进入/退出副作用、状态间允许的转移条件。
输出要求:
1. 单个 .gd 文件,extends CharacterBody2D,不依赖任何第三方插件。
2. 用 enum 定义状态,switch/match 分发,必须提供 change_state()、_enter_state()、_exit_state() 三个函数。
3. _exit_state 必须清理该状态独占的资源(Timer、AnimationPlayer、速度倍率)。
4. 未定义的状态转移必须被忽略,不允许抛错。
5. 每个函数上方用一行中文注释写明「为什么这样写」,不要解释基础语法。
6. 不要写任何我没列出的状态,不要引入信号总线/Autoload。
约束:Godot 4.3+ 语法;变量用类型标注;不写 print 调试语句。
用法:把你的状态表贴进去,DeepSeek 出骨架,你只填 _do_xxx 的行为细节,避免它自作主张扩设计。
提示词 2 · 把 if/else 敌人逻辑重构成状态机
你是一位代码重构专家。下面是一段 Godot 4 GDScript 敌人 AI 代码,它用多个 bool 标志位管理行为,已经出现状态冲突的 bug。
请按以下步骤处理:
1. 先列出代码里所有隐含的「状态」(不是变量名,而是行为阶段),以及当前哪些 bool 组合是非法的。
2. 画出状态转移表(markdown 表格:当前状态 | 触发条件 | 目标状态)。
3. 重写为 enum + match 的状态机,保持原有对外接口和节点路径不变。
4. 单独列出「重构后行为可能发生的改变」,一条一行,供我确认。
约束:不新增任何美术/音效资源引用;不改变数值参数;若原代码逻辑本身有歧义,先用注释标注 TODO,不要自行发明规则。
待重构代码:
〔在此粘贴代码〕
提示词 3 · 排查敌人寻路抖动 / 原地打转
你是 Godot 4 导航系统(NavigationServer2D / NavigationAgent2D)的排查专家。
现象:敌人按 NavigationAgent2D 追击玩家时,会在两个点之间来回抖动,或走到墙边原地打转,偶尔完全不动。
请按此格式回答:
1. 可能原因排序(按概率从高到低,最多 6 条),每条一句话说明机理。
2. 每条原因对应的「30 秒验证方法」(具体到打印哪个属性、改哪个值、看什么现象)。
3. 给出修好后的 _physics_process 片段,要求包含 is_navigation_finished() 前置判断、get_next_path_position() 单帧只调一次、目标位置限频更新。
4. 最后给一份推荐参数表:path_desired_distance / target_desired_distance / path_max_distance 在「追击速度 120px/s、角色半径 12px」下的取值与理由。
约束:不要泛泛说「检查代码」,每一条都要可执行。我的代码:
〔在此粘贴 _physics_process 与 _ready〕
学习心得
做一款 Steam 游戏,玩家最先失去耐心的不是难度,而是敌人看起来「傻」——卡墙、抽搐、明明看见了却站着不动。而这类问题几乎都能在状态机层面消灭,不需要任何「AI 算法」。
对「用 DeepSeek 从零做游戏」这个目标来说,状态机是极好的提示词练习场:它的输入是结构化的(状态表、转移条件),输出是可以验证的(跑起来就知道对不对)。也就是说,这是我少数能完全靠「描述清楚」就让 DeepSeek 一次写对的模块——前提是我先把状态表想明白,而不是把「写个聪明的敌人」丢给它。
今天最大的收获其实是那句判据:一堆互斥的 bool = 一个 enum。这条能直接砍掉我之后一半的返工。
明日关注
- 给敌人加「视野锥 + 射线遮挡」的发现逻辑,验证从 PATROL 到 ALERT 的转移是否需要一个独立检测器脚本
- 试
NavigationObstacle2D,让敌人之间互相避让,避免一群怪叠成一坨 - 把状态机模板抽成一个可复用的
StateMachine节点,为之后的 Boss 战做准备
本文由自动化学习流水线每日生成,坚持即学即记。
评论