敌人AI状态机:
0. 写在前面
本文章的方法来源于视频教程:(视频教程),本人只是总结归纳,算是学习笔记,可用于查漏补缺,省去看视频的时间,希望能帮到你。不懂的可以评论区留言,看到了我就会回答。
注意:代码无法直接粘贴使用,只是解释核心代码的原理和机制。
本人只是小菜鸡,轻喷。
1. 为什么要使用状态机?
在游戏中每个角色通常存在大量动作,如站立、跑动、跳跃、蹲下,有自己写过游戏的朋友肯定知道,当切换人物的状态时,通常需要无数个bool判断,在Update里写无数个if判断条件嵌套,耦合度很高,后期成为“屎山”。当不断增加新功能时,不仅难以修改,还容易出错。
这主要是因为所有条件在一个类中判断,当然容易出错,也不够优雅。这时,就需要状态机了!
如上图,状态机的基本思想是使角色在某一给定时刻进行一个特定的动作,也就是状态。角色从一个状态立即切换到另一个状态是需要一定的限制条件的。比如角色只能从跑步或者站立切换到蹲下,而不能直接从跳跃切换到蹲下。角色从当前状态进入下一个状态的选项被称为状态过渡条件。状态集合、状态过渡条件以及记录当前状态的变量放在一起,形成了一个状态机。
2. 为什么写敌人状态机而不写玩家的?
因为玩家的我之前写完了,现在边学边写敌人的,也算复习了。大体是差不多的,可以触类旁通。
如果有人看的话,之后可能会出玩家的。
1. 搭建结构框架
框架脚本具体有三:
- EnemyEntity:所有敌人都将继承该实体类,该类用于存放每个敌人都拥有的特性。
- State:拥有所有敌人状态的基本功能,如Enter(), Exit(), LogicUpdate(), FixedUpdate();
- FiniteStateMachine:用于追踪敌人当前位于什么状态以及在状态内做正确的事。
具体如下:
public class EnemyEntity : MonoBehaviour
{
public FiniteStateMachine stateMachine;
public Rigidbody2D RB { get; private set; }
public Animator Anim { get; private set; }
public virtual void Start()
{
RB = GetComponent<Rigidbody2D>();
Anim = GetComponent<Animator>();
stateMachine = new FiniteStateMachine(); //状态机不挂载在任何物体上 所以直接生成即可
}
public virtual void Update()
{
stateMachine.CurrentState.LogicUpdate(); //实时调用状态的更新函数
}
public virtual void FixedUpdate()
{
stateMachine.CurrentState.PhysicUpdate(); //实时调用状态的物理更新函数
}
}
public class EnemyState
{
protected FiniteStateMachine stateMachine;
protected EnemyEntity entity;
protected float startTime; //记录状态的开始时间
protected string animBoolName; //用于控制动画的启动和关闭 无需在Unity的动画器中创建和连线
//构造函数初始化状态机和父类实体
public EnemyState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName)
{
this.entity = entity;
this.stateMachine = stateMachine;
this.animBoolName = animBoolName;
}
//virtual可被子类继承与重写
public virtual void Enter()
{
startTime = Time.time; //记录状态开始的时间
entity.Anim.SetBool(animBoolName, true); //启动状态动画
}
public virtual void Exit()
{
entity.Anim.SetBool(animBoolName, false); //关闭状态动画
}
public virtual void LogicUpdate()
{
}
public virtual void PhysicUpdate()
{
}
}
public class FiniteStateMachine
{
public EnemyState CurrentState { get; private set; } //记录当前状态
//初始化状态
public void Initialize(EnemyState startingState)
{
CurrentState = startingState;
CurrentState.Enter();
}
public void ChangState(EnemyState state)
{
CurrentState.Exit(); //先退出当前状态
CurrentState = state;
CurrentState.Enter();
}
}
virtual
关键字用于修改方法、属性、索引器或事件声明,并使它们可以在子类中被重写(override)。
2. 移动和站立状态
状态参考图:看不懂不用担心,具体写到什么状态时再回来看相应的部分就好。
两个基础状态,地面敌人都有的状态。继承于EnemyState,这两个状态类也并不会直接挂载到敌人身上,而是具体敌人再定义具体的类继承这两个基础类。因为每个敌人的特性会有所不同,学过继承多态的朋友应该懂的。
脚本逻辑:
- EnemyMoveState:向前移动直到碰到墙或者平台边缘。
- EnemyIdleState:休息一段时间后继续移动。
public class EnemyMoveState : EnemyState
{
//记录是否检测到平台边缘或者墙
protected bool isDetecLegde;
protected bool isDetecWall;
public EnemyMoveState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName) : base(entity, stateMachine, animBoolName)
{
}
public override void Enter()
{
base.Enter();
isDetecLegde = entity.Core.CollisionSenses.Ledge_Down;
isDetecWall = entity.Core.CollisionSenses.WallFront;
}
public override void Exit()
{
base.Exit();
}
public override void LogicUpdate()
{
base.LogicUpdate();
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
isDetecLegde = entity.Core.CollisionSenses.Ledge_Down;
isDetecWall = entity.Core.CollisionSenses.WallFront;
}
}
public class EnemyIdleState : EnemyState
{
protected bool filpAfterIdle; //站立时是否要掉头 用于判断是由于走了一段时间而停下还是因为检测到平台边缘
protected bool isIdleTimeOver; //是否超过站立时间
protected float idleTime; //站立时间
public EnemyIdleState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName) : base(entity, stateMachine, animBoolName)
{
}
public override void Enter()
{
base.Enter();
entity.Core.Movement.SetVelocityZero(); //速度清空
SetRamdomIdleTime();
isIdleTimeOver = false;
}
public override void Exit()
{
base.Exit();
if (filpAfterIdle)
{
entity.Core.Movement.Flip(); //反转朝向
}
}
public override void LogicUpdate()
{
base.LogicUpdate();
if(Time.time > startTime + idleTime)
{
isIdleTimeOver = true;
}
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
}
public void SetFlipAfterIdle(bool flip)
{
filpAfterIdle = flip;
}
//设置随机站立时间
protected void SetRamdomIdleTime()
{
idleTime = Random.Range(entity.data.minIdleTime, entity.data.maxIdleTime);
}
}
2.5核心代码模式
细心的朋友肯定发现了,
isDetecLegde = entity.Core.CollisionSenses.Ledge_Down;
isDetecWall = entity.Core.CollisionSenses.WallFront;
entity.Core.Movement.Flip();
这些玩意是什么?
因为我用到了教程后面的核心模式,也就是用Core管理一些会反复使用到的代码。比如设置速度,检查是否接触地面、墙体、平台边缘等等函数,在敌人和玩家中都可以反复使用,避免重复写。
如果要用Core的话,记得在EnemyEntity中声明哦。不想写这个的朋友也可以直接复制相应的代码到具体的状态类中。
具体这些脚本该怎么挂载到Unity,我会在下一节定义具体敌人的时候再说。
using UnityEngine;
public class Core : MonoBehaviour
{
public Movement Movement { get; private set; }
public CollisionSenses CollisionSenses { get; private set; }
private void Awake()
{
Movement = GetComponentInChildren<Movement>();
CollisionSenses = GetComponentInChildren<CollisionSenses>();
if (!Movement || !CollisionSenses)
{
Debug.LogError("核心组件缺失");
}
}
public void LogicUpdate()
{
Movement.LogicUpdate();
}
}
public class CoreComponent : MonoBehaviour
{
protected Core core;
protected virtual void Awake()
{
core = GetComponentInParent<Core>();
if(core == null)
{
Debug.LogError("父类没有核心脚本");
}
}
}
public class Movement : CoreComponent
{
private Vector2 workSpace;
public Rigidbody2D RB { get; private set; }
public Vector2 CurrentVelocity { get; private set; }
public int FacingDirection { get; private set; }
protected override void Awake()
{
base.Awake();
RB = GetComponentInParent<Rigidbody2D>();
FacingDirection = 1;
}
public void LogicUpdate()
{
CurrentVelocity = RB.velocity;
}
#region Set Functions
public void SetVelocity(float velocity, Vector2 angle, int direction)
{
angle.Normalize();
workSpace.Set(angle.x * velocity * direction, angle.y * velocity);
RB.velocity = workSpace;
CurrentVelocity = workSpace;
}
public void SetVelocity(float velocity, Vector2 direction)
{
workSpace = direction * velocity;
RB.velocity = workSpace;
CurrentVelocity = workSpace;
}
public void SetVelocityX(float velocityX)
{
workSpace.Set(velocityX, CurrentVelocity.y);
RB.velocity = workSpace;
CurrentVelocity = workSpace;
}
public void SetVelocityY(float velocityY)
{
workSpace.Set(CurrentVelocity.x, velocityY);
RB.velocity = workSpace;
CurrentVelocity = workSpace;
}
public void SetVelocityZero()
{
RB.velocity = Vector2.zero;
CurrentVelocity = Vector2.zero;
}
public void CheckIfShouldFlip(int xInput)
{
if (xInput != 0 && xInput != FacingDirection) //有输入且与当前方向相反
{
Flip();
}
}
public void Flip()
{
FacingDirection *= -1;
RB.transform.Rotate(0.0f, 180f, 0.0f);
}
#endregion
}
/// <summary>
/// 用于检测碰撞关系
/// </summary>
public class CollisionSenses : CoreComponent
{
#region Check Transform
public Transform GroundCheckTrans { get => groundCheckTrans; set => groundCheckTrans = value; }
public Transform WallCheckTrans { get => wallCheckTrans; set => wallCheckTrans = value; }
public Transform LedgeCheckTrans { get => ledgeCheckTrans; set => ledgeCheckTrans = value; }
public float GroundCheckRidius { get => groundCheckRidius; set => groundCheckRidius = value; }
public float WallCheckDistence { get => wallCheckDistence; set => wallCheckDistence = value; }
public float LedgeCheckDistence { get => ledgeCheckDistence; set => ledgeCheckDistence = value; }
public LayerMask WhatIsGround { get => whatIsGround; set => whatIsGround = value; }
[SerializeField] private Transform groundCheckTrans;
[SerializeField] private Transform wallCheckTrans;
[SerializeField] private Transform ledgeCheckTrans;
[SerializeField] private float groundCheckRidius;
[SerializeField] private float wallCheckDistence;
[SerializeField] private float ledgeCheckDistence;
[SerializeField] private LayerMask whatIsGround;
#endregion
#region Check Functions
public bool Ground
{
get => Physics2D.OverlapCircle(groundCheckTrans.position,
groundCheckRidius, whatIsGround);
}
public bool WallFront
{
get => Physics2D.Raycast(wallCheckTrans.position,
Vector2.right * core.Movement.FacingDirection, wallCheckDistence, whatIsGround);
}
public bool WallBack
{
get => Physics2D.Raycast(wallCheckTrans.position,
Vector2.right * -core.Movement.FacingDirection, wallCheckDistence, whatIsGround);
}
public bool Ledge
{
get => Physics2D.Raycast(ledgeCheckTrans.position,
Vector2.right * core.Movement.FacingDirection, wallCheckDistence, whatIsGround);
}
public bool Ledge_Down
{
get => Physics2D.Raycast(ledgeCheckTrans.position,
Vector2.down, ledgeCheckDistence, whatIsGround);
}
#endregion
}
3. 具体敌人:野猪
创建具体敌人的步骤:

第一个敌人:能站立、跑动和攻击玩家。我把他命名为Boar,也就是野猪。我不是很清楚它是不是野猪哈哈哈哈,也可能是牛。
本节先实现其移动和站立的状态。(图片可以直接复制到Unity使用)
代码:
/// <summary>
/// 野猪:继承基础敌人类 实例化野猪的各种状态类
/// </summary>
public class Boar : EnemyEntity
{
public Boar_IdleState idleState;
public Boar_MoveState moveState;
public override void FixedUpdate()
{
base.FixedUpdate();
}
public override void Start()
{
base.Start();
//第一个this是抽象的enemyEntity 第二个this是野猪 在具体类中 第二个覆盖了第一个
moveState = new Boar_MoveState(this, stateMachine, "move", this);
idleState = new Boar_IdleState(this, stateMachine, "idle", this);
stateMachine.Initialize(moveState); //初始为移动状态
}
public override void Update()
{
base.Update();
}
}
public class Boar_IdleState : EnemyIdleState
{
Boar boar;
public Boar_IdleState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Boar boar) : base(entity, stateMachine, animBoolName)
{
this.boar = boar;
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (isIdleTimeOver)
{
stateMachine.ChangState(boar.moveState);
}
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
}
}
public class Boar_MoveState : EnemyMoveState
{
private Boar boar;
public Boar_MoveState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Boar boar) : base(entity, stateMachine, animBoolName)
{
this.boar = boar;
}
public override void Enter()
{
base.Enter();
entity.Core.Movement.SetVelocityX(entity.data.movementSpeed_Boar * entity.Core.Movement.FacingDirection);
}
public override void Exit()
{
base.Exit();
}
public override void LogicUpdate()
{
base.LogicUpdate();
//当检测到墙或者没检测到平台
if(entity.Core.CollisionSenses.WallFront || !entity.Core.CollisionSenses.Ledge_Down)
{
stateMachine.ChangState(boar.idleState);
boar.idleState.SetFlipAfterIdle(true); //翻转朝向
}
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
}
}
在抽象状态类(比如移动、站立状态)里完成一些是否应该转换状态的bool值判断,然后在具体的敌人状态类里切换到其他状态。
还有脚本Data_EnemyState,用来存储敌人的一些数据具体大小,继承ScriptableObject,也就是独立于脚本类存在的用于储存脚本数据的容器,可以直接在Unity中赋值。
[CreateAssetMenu(fileName = "newEnemyData", menuName = "Data/EnemyData")] //建立菜单
public class Data_EnemyState : ScriptableObject //ScriptableObject独立于脚本类存在的用于储存脚本数据的容器
{
public float movementSpeed_Boar = 5f;
public float minIdleTime = 1f;
public float maxIdleTime = 2f;
}
ScriptableObject:
- ScriptableObject 是 Unity 提供的一个数据配置存储基类,它是一个可以用来保存大量数据的数据容器,我们可以将它保存为自定义的数据资源文件。
- ScriptableObject 是一个类似 MonoBehaviour 的基类,继承自 UnityEngine.Object 。要想使用它,需要我们写个脚本去继承 ScriptableObject 。需要注意的是,继承自 SctiptableObject 的脚本无法挂载到游戏物体上,毕竟它不是继承自 MonoBehaviour。
- ScriptableObject 类的实例会被保存成资源文件(.asset文件),和预制体,材质球,音频文件等类似,都是一种资源文件,存放在 Assets 文件夹下,创建出来的实例也是唯一存在的。
详细可查看:https://blog.youkuaiyun.com/qq_46044366/article/details/124310241
具体Unity挂载:
- Core、Movement、CollisionSenses挂载对应的同名脚本。
- Ground Detector放在野猪脚下,其他两个放在野猪的前面。
- 具体参数大小:
小技巧:
- 在代码中写下
//TODO:待完成的事
之后,可以在Visual Studio中的任务列表里看见它,方便后期的查找,防止疏漏。
- 当你要想要知道物理射线的长度具体在游戏中是怎么个事时,可以在EnemyEntity中添加OnDrawGizmos:
//绘制在Unity scence中可视的线 便于查看碰撞检测是否正常:要进行非空检测 因为在编译阶段游戏没开始则未实例化Core
public virtual void OnDrawGizmos()
{
if (Core == null || Core.CollisionSenses == null || Core.Movement == null)
return;
if (Core.CollisionSenses.WallCheckTrans != null)
{
Gizmos.DrawLine(Core.CollisionSenses.WallCheckTrans.position, Core.CollisionSenses.WallCheckTrans.position +
(Vector3)(Vector2.right * Core.Movement.FacingDirection * Core.CollisionSenses.WallCheckDistence));
}
if (Core.CollisionSenses.LedgeCheckTrans != null)
{
Gizmos.DrawLine(Core.CollisionSenses.LedgeCheckTrans.position, Core.CollisionSenses.LedgeCheckTrans.position +
(Vector3)(Vector2.down * Core.CollisionSenses.LedgeCheckDistence));
}
}
效果如下:两条小白线就是你的探测长度。
4. 检测玩家
毫无疑问,检测玩家也是个状态。怎么检测呢?
**脚本逻辑:**在敌人身上朝前发射两条长短不同的物理射线,当玩家进入小射线范围时进入检测状态,当玩家超出大范围时恢复站立。具体实现如下:
- 在EnemyEntity中声明两个范围检测函数:
[SerializeField] private Transform playerCheckTran; //挂在敌人身上 用用检测玩家位置的 点位 不是玩家的位置
//小范围检测玩家
public virtual bool CheckPlayerInMinAggraRange()
{
return Physics2D.Raycast(playerCheckTran.position,
transform.right, data.minAggrDistence, data.whatIsPlayer);
}
//大范围检测玩家
public virtual bool CheckPlayerInMaxAggraRange()
{
return Physics2D.Raycast(playerCheckTran.position,
transform.right, data.maxAggrDistence, data.whatIsPlayer);
}
- Physics2D.Raycast就是发送物理射线函数。

- 创建PlayerDetectedState,Boar_PlayerDetectedState:
脚本逻辑:当进入玩家检测状态则实时检测大小范围内有没有出现玩家。在野猪的玩家检测状态内,当玩家离开大检测范围则进入站立状态,也就是靠近了(小范围)才进入检测状态,但要当玩家离的足够远(大范围)才恢复站立。
public class PlayerDetectedState : EnemyState
{
//大小范围检测
protected bool isMinRangeDectctedPlayer;
protected bool isMaxRangeDectctedPlayer;
public PlayerDetectedState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName) : base(entity, stateMachine, animBoolName)
{
}
public override void Enter()
{
base.Enter();
entity.Core.Movement.SetVelocityZero();
isMinRangeDectctedPlayer = entity.CheckPlayerInMinAggraRange();
isMaxRangeDectctedPlayer = entity.CheckPlayerInMaxAggraRange();
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
isMinRangeDectctedPlayer = entity.CheckPlayerInMinAggraRange();
isMaxRangeDectctedPlayer = entity.CheckPlayerInMaxAggraRange();
}
}
因为Physics2D.Raycast是物理函数,所以在FixUpdate也就是PhysicsUpdate中检测。
public class Boar_PlayerDetectedState : PlayerDetectedState
{
Boar boar;
public Boar_PlayerDetectedState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Boar boar) : base(entity, stateMachine, animBoolName)
{
this.boar = boar;
}
public override void LogicUpdate()
{
//玩家离开最大检测范围(暂时)
if (!isMaxRangeDectctedPlayer)
{
boar.idleState.SetFlipAfterIdle(false); //进入站立状态时无需翻转
stateMachine.ChangState(boar.idleState);
}
base.LogicUpdate();
}
}
- 在Boar中实例化Boar_PlayerDetectedState:
public Boar_PlayerDetectedState playerDetectedState;
public override void Start()
{
playerDetectedState = new Boar_PlayerDetectedState(this, stateMachine, "playerDetected", this);
}
- 在抽象移动和站立脚本中添加状态切换:
在Enter和PhysicUpdate中保存CheckPlayerInMinAggraRange()的结果。
- 在野猪的移动和站立脚本的PhysicUpdate中:
//玩家进入小范围检测
if (isMinRangeDectctedPlayer)
{
stateMachine.ChangState(boar.playerDetectedState);
}
- 在Unity动画器里添加状态切换条件:
退出就是把条件对错反过来。
5. 向玩家冲锋
DoCheck():
在开始介绍之前,先讲一个小技巧。为了不在Enter()
和PhysicUpdate()
里都写相同的检查判断函数(比如是否碰到墙、离开平台、检测到玩家),我们可以直接在EnemyState也就是敌人基状态类中添加DoCheck()
函数,然后在基状态类的Enter()
和PhysicUpdate()
调用DoCheck()
。这样子类就只需重写(override)DoCheck()
函数,再把检测判断函数写进去DoCheck()
里面就好了。
public virtual void Enter()
{
DoCheck();
}
public virtual void PhysicUpdate()
{
DoCheck();
}
//小技巧:在进入和物理更新中调用,用于给子类检查一些判断时,不用在进入和物理更新中写两遍
public virtual void DoCheck()
{
}
public override void DoCheck()
{
base.DoCheck();
isMinRangeDectctedPlayer = entity.CheckPlayerInMinAggraRange();
isLedge = entity.Core.CollisionSenses.Ledge;
isWall = entity.Core.CollisionSenses.WallFront;
}
还是方便了一些的!话不多说,下面回到正题。
具体实现:
脚本逻辑:当检测到玩家时(进入PlayerDetected),等待一小段时间后(蓄力前摇),加速向玩家冲锋(进入charge)。冲锋过程中,如果玩家在攻击范围内,就攻击玩家(进入攻击状态),如果冲锋一段时间内玩家都不在攻击范围内,就进入寻找玩家状态。寻找玩家状态就是原地转几圈,当在小范围检测到玩家时再度进入玩家检测状态,如果转完了都没找到玩家那就进入移动状态。
public class EnemyChargeState : EnemyState
{
protected bool isMinRangeDectctedPlayer;
protected bool isLedge;
protected bool isWall;
protected bool isChargeTimeOver; //检测时间到
public EnemyChargeState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName) : base(entity, stateMachine, animBoolName)
{
}
public override void DoCheck()
{
base.DoCheck();
isMinRangeDectctedPlayer = entity.CheckPlayerInMinAggraRange();
isLedge = entity.Core.CollisionSenses.Ledge;
isWall = entity.Core.CollisionSenses.WallFront;
}
public override void Enter()
{
base.Enter();
entity.Core.Movement.SetVelocityX(entity.data.chargeSpeed);
isChargeTimeOver = false;
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (Time.time > startTime + entity.data.chargeTime)
{
isChargeTimeOver = true;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boar_ChargeState : EnemyChargeState
{
Boar boar;
public Boar_ChargeState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Boar boar) : base(entity, stateMachine, animBoolName)
{
this.boar = boar;
}
public override void LogicUpdate()
{
base.LogicUpdate();
if(!entity.Core.CollisionSenses.Ledge || entity.Core.CollisionSenses.WallFront)
{
stateMachine.ChangState(boar.lookforPlayerState);
}
//超过时间
else if (isChargeTimeOver)
{
if (isMinRangeDectctedPlayer)
{
stateMachine.ChangState(boar.playerDetectedState);
}
}
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
}
}
public class LookForPlayerState : EnemyState
{
protected bool isMinRangeDectctedPlayer;
protected bool isFlip;
protected bool isAllTurnsDone; //转身次数耗尽
protected bool isAllTurnsTimeDone; //转身时间结束
protected float lastTurnTime; //目前这轮转身的持续时间
protected int amountOfTurnsDone; //目前完成了几轮转身
public LookForPlayerState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName) : base(entity, stateMachine, animBoolName)
{
}
public override void DoCheck()
{
base.DoCheck();
isMinRangeDectctedPlayer = entity.CheckPlayerInMinAggraRange();
}
public override void Enter()
{
base.Enter();
isAllTurnsDone = false;
isAllTurnsTimeDone = false;
lastTurnTime = startTime;
amountOfTurnsDone = 0;
entity.Core.Movement.SetVelocityZero();
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (isFlip) //第一次转身
{
entity.Core.Movement.Flip();
lastTurnTime = Time.time;
amountOfTurnsDone++;
isFlip = false;
}
//第一次之后的转身
else if(Time.time > lastTurnTime + entity.data.timeBetweenTurn && !isAllTurnsDone)
{
entity.Core.Movement.Flip();
lastTurnTime = Time.time;
amountOfTurnsDone++;
}
//所有转身结束
if(amountOfTurnsDone >= entity.data.amountOfTurn)
{
isAllTurnsDone = true;
}
//轮数耗尽且最后一轮时间耗尽
if(Time.time > lastTurnTime + entity.data.timeBetweenTurn && isAllTurnsDone)
{
isAllTurnsTimeDone = true;
}
}
//在其他状态中控制啥时候转身
public void SetFilp(bool filp)
{
isFlip = filp;
}
}
public class Boar_LookForPlayerState : LookForPlayerState
{
Boar boar;
public Boar_LookForPlayerState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Boar boar) : base(entity, stateMachine, animBoolName)
{
this.boar = boar;
}
public override void DoCheck()
{
base.DoCheck();
}
public override void Enter()
{
base.Enter();
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (isMinRangeDectctedPlayer)
{
stateMachine.ChangState(boar.playerDetectedState);
}
else if (isAllTurnsTimeDone)
{
stateMachine.ChangState(boar.moveState); //转完身也没找到玩家就恢复移动状态
}
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
}
}
别忘了在Boar中实例化各种状态,步骤跟前面的状态实例一致。
在玩家检测状态中当进入状态的冲锋前摇(我设置的是1秒)结束后,进入冲锋状态:
//PlayerDetectedState类
public override void LogicUpdate()
{
base.LogicUpdate();
//冲锋前摇
if(Time.time > startTime + entity.data.longRangeActionTime)
{
performLongRangeAction = true;
}
}
//Boar_PlayerDetectedState类
public override void LogicUpdate()
{
base.LogicUpdate();
//检测到玩家且前摇结束 冲锋
if (performLongRangeAction)
{
stateMachine.ChangState(boar.chargeState);
}
//玩家离开最大范围就开始寻找玩家
else if(!isMaxRangeDectctedPlayer)
{
stateMachine.ChangState(boar.lookforPlayerState);
}
}
最后在Unity里设置各种状态动画,lookforPlayerState用的是站立动画。
6.攻击玩家
在玩家检测状态或者冲锋状态下,当玩家进入攻击范围时向玩家进攻
敌人攻击分为近战和远程,野猪是近战攻击,后面的弓箭手是远程射箭。
在野猪的攻击动画帧上添加TriggerAttack和FinishAttack两个函数。
collider.transform.SendMessage("damage", attackDetails);
是一个在 Unity 中向指定的游戏对象发送名为 “damage” 的消息的方法。该方法会调用接收者游戏对象上名为 “damage” 的方法,并将 attackDetails 参数作为消息的参数传递。这个方法的作用是发送一条消息给接收者游戏对象,告诉它受到了伤害。具体的行为取决于接收者游戏对象上的 “damage” 方法的实现。
请确保在使用 SendMessage 方法之前,接收者游戏对象上已经实现了名为 “damage” 的方法,并且该方法具有与传递的 attackDetails 参数相匹配的参数列表。否则,该方法将无法成功调用。
public class EnemyAttackState : EnemyState
{
protected Transform attackTransform;
protected bool isAnimationFinshed;
public EnemyAttackState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Transform attackPosition) : base(entity, stateMachine, animBoolName)
{
this.attackTransform = attackPosition;
}
public override void Enter()
{
base.Enter();
entity.atsm.attackState = this; //赋值给转接脚本
isAnimationFinshed = false;
entity.Core.Movement.SetVelocityZero();
}
public virtual void TriggerAttack()
{
}
public virtual void FinishAttack()
{
isAnimationFinshed = true;
Debug.Log("完成攻击");
}
}
public class EnemyMeleeAttackState : EnemyAttackState
{
protected EnemyAttackDetails attackDetails;
protected bool isMinRangeDectctedPlayer;
public EnemyMeleeAttackState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Transform attackPosition) : base(entity, stateMachine, animBoolName, attackPosition)
{
}
public override void DoCheck()
{
base.DoCheck();
isMinRangeDectctedPlayer = entity.CheckPlayerInMinAggraRange();
}
public override void Enter()
{
base.Enter();
attackDetails.damageAmount = entity.data.attackDamage;
attackDetails.position = entity.transform.position;
}
public override void TriggerAttack()
{
base.TriggerAttack();
//用碰撞体数组存储物理圆碰撞到的物体
Collider2D[] detectedObjects = Physics2D.OverlapCircleAll
(attackTransform.position, entity.data.attackRadius, entity.data.whatIsPlayer);
foreach(Collider2D collider in detectedObjects)
{
//SendMessage会调用接收者身上名为 "damage" 的方法,并将 attackDetails作为消息的参数传递
//collider.transform.SendMessage("damage", attackDetails);
IDamageable damageable = collider.GetComponent<IDamageable>();
if(damageable != null)
{
damageable.Damage(attackDetails.damageAmount);
}
}
}
}
public class Boar_MeleeAttackState : EnemyMeleeAttackState
{
Boar boar;
public Boar_MeleeAttackState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Transform attackPosition, Boar boar)
: base(entity, stateMachine, animBoolName, attackPosition)
{
this.boar = boar;
}
public override void LogicUpdate()
{
base.LogicUpdate();
//完成攻击动画
if (isAnimationFinshed)
{
if (isMinRangeDectctedPlayer)
{
stateMachine.ChangState(boar.playerDetectedState);
}
else
{
stateMachine.ChangState(boar.lookforPlayerState);
}
}
}
}
7. 弓箭手敌人

我没有做近战攻击,有兴趣的可以自己尝试或者看原视频
弓箭手英文名Archer,基于前面的敌人框架,我们可以复用多个状态,只需要创造弓箭手独有的一些逻辑即可。
弓箭手的攻击方式为:当玩家在射击范围内时射箭,如果玩家靠近弓箭手,弓箭手将会往后跳跃。

除此之外,其他逻辑与野猪几乎一致

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Archer : EnemyEntity
{
public Archer_IdleState idleState { get; private set; }
public Archer_MoveState moveState { get; private set; }
public Archer_PlayerDetectedState playerDetectedState { get; private set; }
public Archer_LookForPlayerState lookForPlayerState { get; private set; }
public Archer_DodgeState dodgeState { get; private set; }
public Archer_RangeAttactState rangeAttactState { get; private set; }
[SerializeField] private Transform rangeAttackTransform;
public override void Awake()
{
base.Awake();
moveState = new Archer_MoveState(this, stateMachine, "move", this);
idleState = new Archer_IdleState(this, stateMachine, "idle", this);
playerDetectedState = new Archer_PlayerDetectedState(this, stateMachine, "playerDetected", this);
lookForPlayerState = new Archer_LookForPlayerState(this, stateMachine, "lookForPlayer", this);
dodgeState = new Archer_DodgeState(this, stateMachine, "dodge", this);
rangeAttactState = new Archer_RangeAttactState(this, stateMachine, "rangeAttack", rangeAttackTransform, this);
}
private void Start()
{
stateMachine.Initialize(moveState); //初始为移动状态
}
public override void Update()
{
base.Update();
}
public override void OnDrawGizmos()
{
base.OnDrawGizmos();
if (rangeAttackTransform && data)
{
Gizmos.DrawWireSphere(rangeAttackTransform.position, data.attackRadius);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyRangeAttackState : EnemyAttackState
{
protected GameObject projectile;
protected Enemy_Projectile projectileScript;
public EnemyRangeAttackState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Transform attackPosition) : base(entity, stateMachine, animBoolName, attackPosition)
{
}
public override void TriggerAttack()
{
base.TriggerAttack();
//生成弓箭
projectile = GameObject.Instantiate(entity.data.projectile, attackTransform.position, attackTransform.rotation);
projectileScript = projectile.GetComponent<Enemy_Projectile>();
projectileScript.FireProjectile(entity.data.projectileSpeed, entity.data.projectileTravelDis);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Archer_RangeAttactState : EnemyRangeAttackState
{
Archer archer;
public Archer_RangeAttactState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Transform attackPosition, Archer archer) : base(entity, stateMachine, animBoolName, attackPosition)
{
this.archer = archer;
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (isAnimationFinshed)
{
if (entity.CheckPlayerInMinAggraRange())
{
stateMachine.ChangState(archer.playerDetectedState);
}
else
{
stateMachine.ChangState(archer.lookForPlayerState);
}
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Archer_DodgeState : EnemyDodgeState
{
Archer archer;
public Archer_DodgeState(EnemyEntity entity, FiniteStateMachine stateMachine, string animBoolName, Archer archer) : base(entity, stateMachine, animBoolName)
{
this.archer = archer;
}
public override void DoCheck()
{
base.DoCheck();
}
public override void Enter()
{
base.Enter();
}
public override void Exit()
{
base.Exit();
}
public override void LogicUpdate()
{
base.LogicUpdate();
if (isDodgeOver)
{
if (!entity.CheckPlayerInMaxAggraRange())
{
stateMachine.ChangState(archer.lookForPlayerState);
}
else if(entity.CheckPlayerInMaxAggraRange() && !entity.CheckPlayerInCloseRangeAction())
{
stateMachine.ChangState(archer.rangeAttactState);
}
else
{
stateMachine.ChangState(archer.idleState);
}
}
}
public override void PhysicUpdate()
{
base.PhysicUpdate();
}
}