Unity敌人AI状态机的搭建原理

敌人AI状态机:

0. 写在前面

​ 本文章的方法来源于视频教程:(视频教程),本人只是总结归纳,算是学习笔记,可用于查漏补缺,省去看视频的时间,希望能帮到你。不懂的可以评论区留言,看到了我就会回答。

注意:代码无法直接粘贴使用,只是解释核心代码的原理和机制。

本人只是小菜鸡,轻喷。

1. 为什么要使用状态机?

​ 在游戏中每个角色通常存在大量动作,如站立、跑动、跳跃、蹲下,有自己写过游戏的朋友肯定知道,当切换人物的状态时,通常需要无数个bool判断,在Update里写无数个if判断条件嵌套,耦合度很高,后期成为“屎山”。当不断增加新功能时,不仅难以修改,还容易出错。

​ 这主要是因为所有条件在一个类中判断,当然容易出错,也不够优雅。这时,就需要状态机了!

状态切换图

​ 如上图,状态机的基本思想是使角色在某一给定时刻进行一个特定的动作,也就是状态。角色从一个状态立即切换到另一个状态是需要一定的限制条件的。比如角色只能从跑步或者站立切换到蹲下,而不能直接从跳跃切换到蹲下。角色从当前状态进入下一个状态的选项被称为状态过渡条件。状态集合、状态过渡条件以及记录当前状态的变量放在一起,形成了一个状态机

2. 为什么写敌人状态机而不写玩家的?

​ 因为玩家的我之前写完了,现在边学边写敌人的,也算复习了。大体是差不多的,可以触类旁通。

​ 如果有人看的话,之后可能会出玩家的。

1. 搭建结构框架

框架脚本具体有三:

  1. EnemyEntity:所有敌人都将继承该实体类,该类用于存放每个敌人都拥有的特性。
  2. State:拥有所有敌人状态的基本功能,如Enter(), Exit(), LogicUpdate(), FixedUpdate();
  3. 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. 移动和站立状态

状态参考图:看不懂不用担心,具体写到什么状态时再回来看相应的部分就好。

image-20230910194836845

​ 两个基础状态,地面敌人都有的状态。继承于EnemyState,这两个状态类也并不会直接挂载到敌人身上,而是具体敌人再定义具体的类继承这两个基础类。因为每个敌人的特性会有所不同,学过继承多态的朋友应该懂的。

脚本逻辑:

  1. EnemyMoveState:向前移动直到碰到墙或者平台边缘。
  2. 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. 具体敌人:野猪

创建具体敌人的步骤:

image-20230904185850252

第一个敌人:能站立、跑动和攻击玩家。我把他命名为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挂载:

image-20230903220855578

  1. Core、Movement、CollisionSenses挂载对应的同名脚本。
  2. Ground Detector放在野猪脚下,其他两个放在野猪的前面。

image-20230903221200840

image-20230903221229560

  1. 具体参数大小:

image-20230903221321558

小技巧:

  1. 在代码中写下//TODO:待完成的事之后,可以在Visual Studio中的任务列表里看见它,方便后期的查找,防止疏漏。

image-20230904203203196

  1. 当你要想要知道物理射线的长度具体在游戏中是怎么个事时,可以在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));
        }
    }

效果如下:两条小白线就是你的探测长度。

image-20230903221802786

4. 检测玩家

毫无疑问,检测玩家也是个状态。怎么检测呢?

**脚本逻辑:**在敌人身上朝前发射两条长短不同的物理射线,当玩家进入小射线范围时进入检测状态,当玩家超出大范围时恢复站立。具体实现如下:

  1. 在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就是发送物理射线函数。
image-20230905160518936
  1. 创建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();
    }
}
  1. 在Boar中实例化Boar_PlayerDetectedState:
	public Boar_PlayerDetectedState playerDetectedState;

	public override void Start()
    {
        playerDetectedState = new Boar_PlayerDetectedState(this, stateMachine, "playerDetected", this);
    }
  1. 在抽象移动和站立脚本中添加状态切换:

在Enter和PhysicUpdate中保存CheckPlayerInMinAggraRange()的结果。

  1. 在野猪的移动和站立脚本的PhysicUpdate中:
//玩家进入小范围检测
if (isMinRangeDectctedPlayer)
{
	stateMachine.ChangState(boar.playerDetectedState);
}
  1. 在Unity动画器里添加状态切换条件:

image-20230904205405003

image-20230905182214556

退出就是把条件对错反过来。

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.攻击玩家

  1. 在玩家检测状态或者冲锋状态下,当玩家进入攻击范围时向玩家进攻

  2. 敌人攻击分为近战和远程,野猪是近战攻击,后面的弓箭手是远程射箭。

  3. 在野猪的攻击动画帧上添加TriggerAttack和FinishAttack两个函数。

  4. 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. 弓箭手敌人

Enemy2 FSM

我没有做近战攻击,有兴趣的可以自己尝试或者看原视频

弓箭手英文名Archer,基于前面的敌人框架,我们可以复用多个状态,只需要创造弓箭手独有的一些逻辑即可。

弓箭手的攻击方式为:当玩家在射击范围内时射箭,如果玩家靠近弓箭手,弓箭手将会往后跳跃。

Enemy2

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

image-20231118165527276
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();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值