ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Godot 2D敌人AI与战斗系统实战:状态机驱动的巡逻追击与受伤反馈

Godot 2D敌人AI与战斗系统实战:状态机驱动的巡逻追击与受伤反馈 这个系列写到第十三篇我猜不少读者已经跟我一样把主角的移动、跳跃、瓦片地图、摄像机这些东西折腾得差不多了。前几篇我们一直在解决“主角怎么动”但一个游戏如果只有主角在空荡荡的地图里跑来跑去那它跟一个动态壁纸没什么区别。所以这一篇该干正事了给这个世界塞几个敌人让他们会巡逻、会追你、会攻击你同时也得能被你打。这篇做完你的项目才算是迈过 demo 的门槛有了点“游戏”的意思。这篇的内容我给它定位成“2D 敌人 AI 与战斗系统”。它不需要你有多高深的设计模式基础但需要你已经掌握 Godot 的基础场景、节点和 GDScript 语法——如果你是从本系列第一篇一直跟到现在的读者那正好。全文会从状态机的设计思路讲起然后落到敌人场景的搭建、AI 脚本的编写最后解决几个我实际开发中踩过的坑。代码以 Godot 4.x 为准3.x 读者注意一下 API 差异比如move_and_slide()这些地方。1. 内容整体设计与思路拆解1.1 为什么先想状态机而不是先写代码我自己第一次给游戏做敌人 AI 的时候想法特别天真让敌人看见玩家就追追到了就打。于是我把这些逻辑全塞进一个_physics_process里结果写了两百行后完全失控一会儿敌人抽搐一会儿攻击个不停调试了一个晚上还没搞明白问题出在哪。后来我才意识到敌人 AI 这类东西行为逻辑天然是分立的站岗、巡视、发现目标、缠斗、被打、倒地每一种行为都有自己的条件、动作和收尾。如果不用状态机把这些行为隔开你本质上是在维护一个巨大的 if-else 怪物改动一处就可能崩掉另一处。状态机的思路其实不复杂你把它想象成门口保安的上班流程就懂了。保安平时在岗亭里坐着Idle每隔一段时间出来巡逻一圈Patrol听到异常动静或者看到可疑人员他就锁定目标跟上去Chase距离够了就出手阻拦Attack挨了一棍子会退两步、喊一声Hurt最后倒地上不动了Die。游戏里的敌人 AI 完全可以照搬这套模型只是在编程上我们把“保安”换成一个枚举变量用match语句根据当前状态执行不同逻辑。这样每个行为都是独立的一块代码互不干扰出问题也好排查。在这个方案里我选择用最朴素的方式做状态机一个enum枚举状态一个当前状态变量_physics_process里一个match分支。Godot 引擎本身支持动画树、行为树插件也有可视化的状态机编辑器但对大多数 2D 动作游戏来说那些东西属于杀鸡用牛刀。用enummatch写出来的状态机代码量小、执行效率高、依赖少而且完全可控。等你的 AI 复杂度到了需要叠加多个状态、处理状态打断、做复杂决策树的程度再考虑引入行为树插件也不迟。这个道理和写文档一样先做能用的再做可扩展的。1.2 感知设计Area2D 负责“看见范围”RayCast2D 负责“是否真的看得见”敌人要追你首先得“知道”你来了。很多新手在这里的第一个错误是想用距离判断写一句if player.global_position.distance_to(global_position) 300然后让敌人追过去。这个方法在小项目里能跑但很快你会发现问题距离判断是圆形的敌人隔着一堵墙也能“看到”你而且这个判断放在哪个节点上、多久算一次、要不要区分上下左右全都得自己维护。与其这样不如直接用 Godot 的 Area2D 做感知范围——你把它想象成一个看不见的雷达圈玩家一进圈body_entered信号就通知敌人“有人来了”玩家出圈body_exited通知“目标丢了”。雷达圈的大小、形状都能在编辑器里直接拖调起来非常直观。但 Area2D 只能告诉你“目标在范围内”它不能告诉你“中间有没有墙挡着”。这时候就需要 RayCast2D 出手了。RayCast2D 的思路就是仇恨系统里的“视线检测”每帧从敌人位置朝玩家方向打一条射线如果射线撞到的是墙壁或障碍物那就说明玩家虽然在感知范围内但被挡住了敌人不应该知道玩家在哪。这其实就是现实里的视觉判断逻辑——你听到走廊里有声音但你不一定能看到声音来源只有当你亲眼确认了才会真正追上去。把 Area2D 和 RayCast2D 组合起来敌人的 AI 才算是“有脑子”而不是“开透视”。这套组合还有一个额外好处感知和 AI 逻辑解耦。你可以随时调整感知半径来改变游戏难度——普通敌人视野小精英敌人视野大潜行关卡里干脆把 Area2D 的半径缩到贴身距离。视野检测的频率也不用担心性能RayCast2D 每物理帧打一次射线对现代设备和 2D 小场景来说开销可以忽略。反而要注意的是碰撞层配置RayCast 必须要排除玩家自己和敌人自身的碰撞还要排除那些不该挡视线的透明物体。具体的层和掩码设置我会在第三节实操部分给出一份可以直接抄的配置表。1.3 把受伤反馈排在攻击动画之前做一开始做这个系列教程的时候我设定了这条开发顺序先做敌人的受伤反馈再做敌人的攻击行为。很多人会觉得这个顺序反了攻击不是更核心吗我的理由是这样的受伤反馈是玩家和敌人之间的第一层交互。如果玩家打了敌人一刀敌人没有任何反应只是在原地继续追你玩家立刻就会觉得游戏“手感很飘”。反过来如果把攻击行为先做完再去补受伤你就得在已经稳定的 AI 状态机里额外插入 Hurt 状态改动面会大很多。所以我在新项目里养成了习惯任何战斗单位都先把“掉血 - 闪烁 - 击退 - 死亡”这条链路打通再回头加它的主动攻击。这不是什么玄学纯粹是踩坑踩出来的开发节奏。把受伤反馈做在前面的另一个原因是你可以用一套统一的take_damage接口来串联所有伤害来源。不管是玩家挥剑、射箭、丢炸弹还是场景里的陷阱、岩浆最终都是调用同一个接口传入伤害数值和击退方向。这个接口设计好后敌人脚本的主干就定了生命值变量、无敌帧计时器、受击动画。后面加攻击行为时只是往状态机里增加 Attack 状态不需要回头改受伤逻辑。这就像盖房子先把水电管道铺好再刷墙贴砖入住后改动才少。具体到无敌帧设计我一般用is_invincible这个布林值来控制。敌人受击后把is_invincible置为 true启动一个短计时器比如 0.3 秒计时结束后再置回 false。很多动作游戏里的闪白、闪烁动画本质上就是无敌帧的表现。如果你不做无敌帧玩家连续按攻击键时敌人就会在极短帧内被多次触发伤害判定血条瞬间清空同时击退向量反复叠加敌人会被打得原地抖成筛子。这些细节单看每个都很小但它们正是“手感”的来源。2. 核心细节解析与实操要点2.1 敌人场景的节点树布局动手之前先把场景结构想清楚。我建议的敌人场景节点树大概是这样的根节点是 CharacterBody2D命名 Enemy它负责物理移动和碰撞响应下面挂一个 Sprite2D 用来显示形象再挂一个 AnimationPlayer 播放动画然后是三个交互节点——VisionArea一个 Area2D用于感知玩家进入范围、VisionRay一个 RayCast2D用于视线阻挡检测、AttackArea一个 Area2D用于攻击判定范围最后是几个 CollisionShape2D。注意这里的 CollisionShape2D 不能只挂一个移动碰撞和攻击判定要分开移动碰撞挂在 CharacterBody2D 自带的 CollisionShape2D 上攻击判定的碰撞形状挂在 AttackArea 下面。可能有读者会问为什么攻击判定不直接放在 Sprite 的碰撞里因为移动碰撞和攻击判定是完全不同的两种东西。移动碰撞是物理性质的它决定敌人能不能穿过墙壁、能不能站在平台上攻击判定是逻辑性质的它只要负责在特定帧检测玩家是否在攻击范围内不需要参与物理移动。把两者分开后你可以单独调攻击范围的大小、开关攻击判定而不会影响敌人的物理移动。比如你想做一把长矛的攻击AttackArea 的 CollisionShape2D 可以是一个扁长的 RectangleShape2D你想做重锤攻击则可以改成圆形或扇形完全不用动移动碰撞。碰撞层规划也要提前做。我习惯的做法是玩家 CharacterBody2D 放在第 1 层敌人放在第 2 层环境墙壁、平台、障碍放在第 3 层。Area2D 的检测用 mask 来控制VisionArea 需要检测第 1 层玩家AttackArea 也需要检测第 1 层VisionRay 的碰撞掩码则要包含第 3 层墙壁同时排除第 1、2 层否则射线会一直命中自己和玩家导致敌人永远“看不见”目标。这个配置看着琐碎但非常重要很多 AI 异常问题追根到底都是 layer 和 mask 配对错了。2.2 状态机落地时的三个隐藏坑状态机的代码逻辑看着简单真正写到项目里还是有几个坑很容易踩。第一个坑match分支里没有收尾。很多新手会用“当前状态 A - 某些条件满足 - 把状态变量改成 B”的方式来切换状态但改完状态变量之后A 状态的代码在这一帧可能还会继续执行一段。比如敌人攻击动画已经播放到一半你决定让它进入 Hurt 状态但match的 Attack 分支本帧还有几行代码没跑完导致击退方向还没设置好就被覆盖了。我的习惯是每个状态的分支里尽量只做一件事所有可能触发切换的条件都放到状态判断的最前面一旦确定要切换立刻用return结束这一帧的物理处理不给后续代码执行的机会。第二个坑状态切换时没有重置状态字段。敌人从 Chase 切回 Idle 时上一个状态里的攻击冷却计时器、目标引用、动画播放进度这些东西如果不清理下一个状态可能会带着上一帧的“记忆”运行。打个比方你在办公室里正处理 Excel突然被叫去开会散会回来后如果你不开新的工作区屏幕上还留着之前的表格脑子里也还想着表格的事效率肯定低。状态切换也是这样进入新状态时要把该清零的清零、该重新初始化的初始化——比如进入 Idle 时把目标置空进入 Attack 时把冷却计时器重置为attack_cooldown的初始值。我建议把状态切换的初始化逻辑单独抽出一个_enter_state函数每次切状态时调用而不是散落在各个match分支里。第三个坑不要在一帧内反复进入同一个状态。如果状态切换发生在_physics_process里而切换后又立刻满足切回条件极端情况下会出现同一帧里状态变量被改来改去动画和计时器被反复重置表现出来就是敌人抽搐、动画闪跳。解决思路是给状态切换加一层保护新状态和当前状态相同就不做任何处理。这跟很多 UI 库里的状态去重是一个道理。这三个坑我当年都踩过每个都会浪费你至少半小时的调试时间提前把代码结构写好才是正道。2.3 朝向与动画敌人要“看着”玩家敌人追击玩家时有两个表现细节特别影响观感一个是朝向一个是动画。朝向问题很简单敌人往左移动时Sprite2D 要镜像翻转往右移动时正回来。很多新手会写成sprite.scale.x -1这在大多数情况下没问题但如果你把 CollisionShape2D 也挂在 Sprite2D 下面scale 翻转会把碰撞形状一起翻过来导致物理碰撞区域反向。我习惯用 Sprite2D 的flip_h属性这个属性只翻转贴图的绘制方向不影响碰撞形状安全得多。代码上就一句话sprite.flip_h velocity.x 0。注意velocity是向量不要拿 position 去比否则敌人在原地小范围抖动时朝向也会跟着乱闪。动画这边我建议把敌人动画命名为固定的四到五个idle、walk、attack、hurt、die。这样代码里可以统一用animation.play(state_name)来播放不用为每种敌人单独写条件判断。Godot 的 AnimationPlayer 还支持 Method Track可以在动画时间轴上的特定帧调用脚本方法。这个特性非常适合做攻击判定你在攻击动画的“挥刀”那一帧放一个方法调用敌人真正出手时才检测伤害而不是动画刚开始就判定。这样做的好处是玩家能看到攻击前摇躲起来有操作空间游戏的“公平感”会好很多。还有一个细节是动画速度。attack 动画播放期间敌人不应该继续移动否则就会出现“一边后退一边砍人”的诡异画面。我的做法是进入 Attack 状态时把velocity设置为零攻击动画播放完再根据状态是否还是 Attack 来决定切换回 Idle 或 Chase。这个逻辑放在哪里虽然看起来是小事但对战斗手感的影响很大。你如果玩过那种“敌人打你时还漂移”的独立游戏就知道这个细节做不好有多出戏。3. 实操过程与核心环节实现3.1 搭建敌人场景与碰撞分层配置接下来是动手环节。打开 Godot 4.x新建一个场景根节点选择 CharacterBody2D命名为 Enemy。先把项目设置里的物理层规划好第 1 层 player、第 2 层 enemy、第 3 层 environment并给对应的节点设置 layer 和 mask。然后给 Enemy 挂上 CollisionShape2D用 RectangleShape2D 或 CapsuleShape2D 都可以尺寸略小于贴图就行——碰撞盒稍微收紧一点玩家打中敌人边缘时会感觉更“友好”不容易出现刀锋明明碰到了模型却没判定的情况。这个“碰撞盒比视觉略小”的经验在平台跳跃和俯视角动作游戏里都适用。接着挂 Sprite2D 和 AnimationPlayer。Sprite2D 的贴图你可以临时用一个简单的占位图比如 Godot 内置的 Icon 图标或者自己在画图里画个圆形AnimationPlayer 里新建几个动画 Clip至少包含 idle、walk、attack、hurt、die 这五个暂时每段只做最简单的位移或旋转就行等美术资源到位后替换。再建两个 Area2DVisionArea 挂在 Enemy 下给它加一个 CollisionShape2D建议用 CircleShape2D半径 200 到 300 之间把它的 monitorable 关掉这个节点不需要被别的节点监测、monitoring 保持开启然后把 mask 设置成只检测第 1 层 player。另一个是 AttackArea挂在 Enemy 下CollisionShape2D 用 CircleShape2D 或 RectangleShape2D半径根据攻击距离设这个节点专门用来检测玩家是否在攻击范围内。最后加一个 RayCast2D名字叫 VisionRaytarget_position 设成 (300, 0)朝右探测具体会被代码里的方向更新覆盖enabled 默认开启collision_mask 只勾第 3 层 environment。这一整套配置做完后你可以在编辑器里拖动玩家节点到敌人周围看看 VisionArea 的圆圈是否直观显示出来。如果没显示检查 Area2D 的 debug 颜色设置如果显示但射线不工作大概率是 RayCast2D 的 target_position 方向不对——这个值表示相对于敌人的偏移而不是世界坐标。还有一个容易忽略的细节敌人或者玩家如果挂在不同子场景下面并且这些子场景是通过实例化放进主世界的物理层配置仍然管用因为物理层是基于每个节点自己的 collision_layer 和 collision_mask 设置的跟场景树层级无关。3.2 AI 脚本从巡逻到追击到攻击的完整实现敌人场景搭好后给根节点写 AI 脚本。先给出完整代码我用的版本是 Godot 4.x 的 GDScript注释写得比较详细方便你直接抄。代码里我用了一段巡逻逻辑敌人没有发现玩家时在左右两个边界点之间来回走发现玩家且视线无障碍时进入追击追击到攻击范围内就攻击受伤进入 Hurt血量为零进入 Die。以下是完整脚本extends CharacterBody2D enum State { IDLE, PATROL, CHASE, ATTACK, HURT, DIE } export var patrol_left : 100 export var patrol_right : 300 export var move_speed : 80.0 export var chase_speed : 150.0 export var attack_range : 40.0 export var attack_cooldown : 1.2 export var max_health : 3 export var invincible_time : 0.3 onready var sprite: Sprite2D $Sprite2D onready var animation_player: AnimationPlayer $AnimationPlayer onready var vision_area: Area2D $VisionArea onready var vision_ray: RayCast2D $VisionRay onready var attack_area: Area2D $AttackArea var current_state : State.IDLE var target: Node2D null var health : max_health var attack_timer : 0.0 var is_invincible : false var invincible_timer : 0.0 var patrol_direction : 1 var home_position : Vector2.ZERO func _ready(): home_position global_position func _physics_process(delta): attack_timer - delta if is_invincible: invincible_timer - delta if invincible_timer 0: is_invincible false _check_vision() match current_state: State.IDLE: _state_idle() State.PATROL: _state_patrol() State.CHASE: _state_chase() State.ATTACK: _state_attack() State.HURT: _state_hurt() State.DIE: _state_die() move_and_slide() func take_damage(amount: int, knockback_dir: Vector2 Vector2.ZERO): if is_invincible or current_state State.DIE: return health - amount velocity knockback_dir * 150.0 is_invincible true invincible_timer invincible_time _enter_state(State.HURT) if health 0: _enter_state(State.DIE) func _state_idle(): animation_player.play(idle) velocity.x move_toward(velocity.x, 0, 10.0) if target: _enter_state(State.CHASE) func _state_patrol(): animation_player.play(walk) if global_position.x home_position.x patrol_right: patrol_direction -1 elif global_position.x home_position.x - patrol_left: patrol_direction 1 velocity.x patrol_direction * move_speed if target: _enter_state(State.CHASE) func _state_chase(): animation_player.play(walk) if not target: _enter_state(State.PATROL) return var dir sign(target.global_position.x - global_position.x) velocity.x dir * chase_speed sprite.flip_h dir 0 if global_position.distance_to(target.global_position) attack_range: _enter_state(State.ATTACK) func _state_attack(): velocity.x 0 if attack_timer 0.0: return animation_player.play(attack) attack_timer attack_cooldown _apply_attack_damage() func _state_hurt(): velocity.x move_toward(velocity.x, 0, 20.0) if invincible_timer 0.0: _enter_state(State.IDLE) func _state_die(): velocity Vector2.ZERO animation_player.play(die) func _enter_state(new_state: int): if current_state new_state: return current_state new_state if current_state State.ATTACK: attack_timer 0.0 elif current_state State.HURT: invincible_timer invincible_time elif current_state State.PATROL: patrol_direction 1 func _check_vision(): if target null: return vision_ray.target_position to_local(target.global_position) vision_ray.force_raycast_update() if vision_ray.is_colliding(): target null func _apply_attack_damage(): for body in attack_area.get_overlapping_bodies(): if body.is_in_group(player) and body.has_method(take_damage): body.take_damage(1, global_position.direction_to(body.global_position))脚本的核心逻辑都在这了我挑几个重点说一下。_physics_process里我先统一处理计时器再match当前状态执行对应函数最后调用move_and_slide()这是 CharacterBody2D 真正把速度变成位移的关键一步——很多新手忘了加这行结果敌人脚本写了半天物体纹丝不动。take_damage是统一的受击入口里面有伤害计算、击退、无敌帧、状态切换任何伤害来源最后都走这个函数。_check_vision负责把感知范围和视线检测结合玩家不在范围target 自然为 null玩家在范围里但被墙挡住target 也会被清空。如果你做的是平台跳跃游戏记得给敌人加上重力模拟这段示例为了突出 AI 逻辑刻意省略了重力。你可以在_physics_process里给velocity.y累加一个gravity * delta然后调用move_and_slide()后判断是否着地。敌人站在平台边缘会被重力拉下去这和玩家角色的处理方式完全一致。3.3 玩家的攻击如何接到敌人身上玩家的攻击接入有几种方式。最简单粗暴的方式是玩家攻击时直接对全场景找敌人并调用take_damage类似get_tree().get_nodes_in_group(enemy)然后遍历调用。这个方案写起来快但有几个问题无法精确控制攻击范围敌人在屏幕外也会被打到多个敌人时逻辑混乱。所以我不推荐尤其是当你后面想做连招、多段判定、暴击之类的东西这套方案根本撑不住。我推荐的做法是给玩家做一个攻击判定区域。玩家按下攻击键时在玩家面前生成一个短暂的 Area2D或者直接启用一个平时隐藏的攻击 Area2D它的 mask 设置为只检测敌人层。这个 Area2D 的body_entered信号触发时取到 body也就是敌人调用body.take_damage(damage, knockback_dir)。这里的 knockback_dir 通常是玩家位置指向敌人位置的方向。AttackArea 只在攻击动画的特定帧启用一小会儿然后立刻关闭避免同一刀命中多次。这个设计的好处是伤害来源和伤害承受者完全解耦玩家攻击、远程子弹、陷阱只要调用敌人的take_damage接口就能统一生效敌人也不需要关心攻击从哪里来。这里还有个细节body_entered信号里的 body 参数类型。敌人在我的方案里是 CharacterBody2D玩家攻击框也是 Area2D。当 Area2D 与 CharacterBody2D 重叠时如果你把 Area2D 的body_entered信号连出来回调参数会直接给出那个 CharacterBody2D如果你把攻击框做成子弹让子弹以 Area2D 的形式飞出去同样适用于这套逻辑。如果你做的项目是俯视角动作游戏可能会想用圆形攻击范围如果是横版动作游戏矩形攻击框会更贴近直觉。总之多试多调攻击框稍微比视觉大一点玩家才会觉得“判定很宽松”这其实是动作游戏通用的人性化设计。4. 常见问题与排查技巧实录4.1 敌人 AI 行为异常的排查表教程写到这儿我猜你已经准备动手试了。以下这些问题是我反复遇到的我整理成了排查表你照着检查大概率能省掉一晚上的调试时间。第一类是“敌人完全不追玩家”先看 VisionArea 有没有检测到玩家把玩家拖进感知范围如果编辑器里没有高亮多半是 Area2D 的 mask 没勾选 player 层或者玩家节点的 collision_layer 不是第 1 层。第二类是“感知到了但就是不过来”多半是 RayCast2D 被墙挡住或者射线配置问题暂时把 RayCast2D 的 enabled 关掉如果敌人开始追人了问题就出在视野检测上。第三类是“敌人追到一半抽搐、原地抖动”通常是因为状态切换太频繁或者击退向量和移动速度互相覆盖。你可以在_physics_process里临时打印当前状态和目标距离看看它是不是在 Attack 和 Chase 之间来回横跳。第四类是“攻击判定不生效”可能是因为你用的是body_entered信号但敌人在画面内还没真正物理接触或者 attack_area 的 mask 没选玩家层。第五类是“伤害数字乱跳、被打一次掉多管血”这是无敌帧没生效检查is_invincible有没有在受击后正确置位以及攻击框有没有持续开启。我把这些问题整理成下面的表格。现象常见原因排查与解决敌人完全不追玩家VisionArea 的 mask 没勾选 player 层检查节点属性确认玩家 collision_layer 配置感知到人却不过来RayCast2D 被墙挡住或 target_position 错误暂时禁用 RayCast 测试检查射线方向敌人原地抽搐状态切换过于频繁击退与移动互相覆盖打印状态切换日志检查状态进入条件攻击判定不生效attack_area 的 mask 未选玩家层或伤害代码未调用在编辑器打开 AttackArea 调试确认伤害函数被调用打一下掉多条血无敌帧未生效攻击框持续开启检查受击后的 is_invincible 设置攻击框及时关闭敌人卡墙里、掉出地图CollisionShape2D 与子节点位置错位在编辑器打开 Shape 可视化检查节点层级4.2 两个能显著提升调 AI 效率的小技巧第一个技巧在敌人身上临时挂一个调试用的文本标签。你可以用 Label 节点在_physics_process里把current_state的数字转成字符串显示出来。比如用match把 0 切成IDLE、1 切成PATROL实时显示在敌人头顶。这样做你一眼就能看出敌人卡在哪个状态是用肉眼调试状态机的利器。等逻辑稳定后再把 Label 删掉或者放在一个 debug 开关后面。我个人的经验是状态机类 AI 的绝大多数问题都是“看起来在动其实是卡在错误状态循环”有了状态标签五分钟就能定位。第二个技巧把 AI 的关键参数全部导出到编辑器。move_speed、chase_speed、attack_range、attack_cooldown这些数值全部用export修饰这样不用改代码就能在编辑器里调整。调试 AI 时最忌讳每次改一个数字都要重新运行整个场景你应该让游戏跑起来然后直接在编辑器右侧的 Inspector 里拖参数、看效果。另外如果你在写敌人 AI 时发现自己的敌人看不到玩家可以先在调试阶段把 VisionRay 的 enabled 关掉排除视野检测干扰如果确认是物理层的问题记得把 Area2D 的 debug 颜色调得很显眼在编辑器里一眼看出感知范围。这些小工具虽然简单但能让开发过程从“盲人摸象”变成“看着仪表盘开车”。顺带说一句开发环境的题外话。如果你在打开 Godot 或运行项目时遇到界面乱码、控件显示不全之类的问题通常和项目代码无关先检查编辑器设置里的字体和主题实在不行换 Compatibility 渲染器试试——这个方法能解决很多奇怪的显示问题。别在乱码问题上死磕它只是编辑器环境的干扰项你的 AI 逻辑不会因此出错。这套敌人基类我后来在很多项目里复用扩展起来非常顺手。想加一个远程敌人就把 AttackArea 换成射子弹的实例化逻辑想做巡逻路线点把patrol_left和patrol_right改成路径点数组想让敌人有不同性格在进入 Chase 状态时随机一个速度倍率就行。作为系列教程的一环我建议你先原样抄一遍代码让敌人能跑起来打起来然后再动手改成自己游戏需要的形态。状态机这种东西光看十遍不如自己踩一遍“不追人、乱抽搐、打不死”的坑我现在写 AI 的思路很大一部分就是当年被这些问题磨出来的。
返回列表