1. 从"会动的角色"到"会思考的敌人":第7篇要解决的核心问题
如果你跟我一样是照着2Dadventure这个系列一路做过来的,那前面的第1篇到第6篇大概已经把角色的移动、跳跃、动画切换、Tilemap关卡绘制、摄像机跟随、基础UI这一整套"能跑起来的冒险Demo"搭完了。但肯定有个很别扭的地方:场景里的敌人全是傻站着的,或者只会沿着一个方向来回走,玩家走过去碰一下掉血,然后对方就被撞死了——这哪叫冒险游戏,这叫碰碰车。
这个系列做到第7篇,我认为真正意义上的"冒险感"是从这一篇开始的。敌人得会巡逻、会追你、会攻击、会被打退、会掉东西,你得有一整套可复用的战斗交互框架。第7篇的内容不是零碎地加一两个脚本,而是要把"敌人AI"和"战斗反馈"这两个系统接进你已有的角色控制器和UI框架里。全文会围绕一套我实际在项目里验证过的方案来讲:有限状态机驱动的敌人基类、基于触发器加物理检测的感知系统、带无敌帧的战斗判定、对象池化的伤害飘字和掉落物。适合正在做横版2D冒险、对这种"敌人像活的一样"的效果有需求的开发者参考。
先给结论:如果你把敌人的行为决策、感知输入、行动输出三者强行塞进Update里写if-else,前两个敌人没问题,第五个开始失控。所以这篇的重点不是教你写某个具体敌人,而是教你搭一个"敌人的骨架",以后你加任何小怪、精英怪、Boss,都是在填充这个骨架。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 敌人状态机:别再堆if-else了,把行为拆成状态
2.1 状态划分的粒度:五个状态起步
拿最典型的近战小兵举例,我常用的状态粒度是这五个:
- Patrol(巡逻):在指定范围内来回走,或沿路点移动
- Chase(追击):发现玩家后追上去,同时判断距离决定下一步
- Attack(攻击):进入攻击范围后播放前摇、出手、后摇
- Hurt(受击):被玩家打中后播放硬直动画,短暂不进行攻击
- Death(死亡):播放死亡动画、关闭碰撞、生成掉落物
为什么是这个粒度而不是更细?因为状态机最怕的是"状态爆炸"。粒度越细,状态切换的逻辑越绕,调试的时候越容易在多个状态之间来回横跳。上面五个状态基本覆盖了横版2D冒险里八成敌人的行为需求,远程法师、飞行敌人、Boss战都能在这个骨架上扩展。
2.2 状态切换的数据流与触发条件
状态机本质上是一个"当前状态 + 切换条件"的模型。每次Tick只处理当前状态的逻辑,然后检查是否满足切到其他状态的条件。关键是这部分逻辑如果是散落的,时间越长越难维护,所以我把切换逻辑收敛在Update里集中管理:
csharp复制public abstract class EnemyBase : MonoBehaviour
{
public enum EnemyState { Patrol, Chase, Attack, Hurt, Death }
protected EnemyState currentState;
protected Rigidbody2D rb;
protected Animator animator;
protected Transform playerTransform;
[Header("状态切换参数")]
public float detectionRange = 5f; // 发现玩家的范围
public float attackRange = 1.5f; // 攻击范围
public float loseTargetRange = 8f; // 丢失玩家的范围
protected virtual void Update()
{
if (currentState == EnemyState.Death) return;
if (currentState == EnemyState.Hurt) return; // 受击时硬直,不切换
if (playerTransform == null) return;
float distanceToPlayer = Vector2.Distance(transform.position, playerTransform.position);
EvaluateStateTransition(distanceToPlayer);
ExecuteCurrentState(distanceToPlayer);
}
protected virtual void EvaluateStateTransition(float distance)
{
if (currentState == EnemyState.Attack) return; // 攻击过程中不切状态
if (distance <= detectionRange && distance > attackRange)
{
SetState(EnemyState.Chase);
}
else if (distance <= attackRange)
{
SetState(EnemyState.Attack);
}
else if (distance > loseTargetRange)
{
SetState(EnemyState.Patrol);
}
}
}
有一处需要特别说明:Hurt状态下我直接return了,因为受击硬直应该拥有最高优先级,玩家打中敌人后如果敌人还能继续攻击,整个战斗反馈的手感会非常差。Attack状态内部也要有"前摇-伤害判定-后摇"的子流程,这要在动画事件里配合完成,后面专门讲。
