第二部分:敌人功能——状态机实现

        实现一个简易的敌人状态机。(以下代码以最简易的敌人AI为例:巡逻状态--->(发现敌人)-->追逐状态--->(进入攻击范围内)---> 攻击状态)

一:状态机基类:

        1:StateActionSO (抽象的状态基类)参数是为了实现状态的复用。 

public abstract class StateActionSO : ScriptableObject
{
    public virtual void OnEnter(StateMachineSystem stateMachineSystem) { }

    public abstract void OnUpdate(StateMachineSystem stateMachineSystem);

    public virtual void OnExit(StateMachineSystem stateMachineSystem) { }

}

        2:ConditionSO (抽象的转换类)

public abstract class ConditionSO : ScriptableObject
{
    public abstract bool ConditionSetUp(StateMachineSystem stateMachineSystem);//条件是否成立

}

        3:TransitionSO (转换控制器)  实现转换SO文件的复用(新增 isInit bool类型变量)

        内部类:StateTransitionConfig 描述了每一条转换的信息(包括优先级)。

private class StateTransitionConfig 
    {
        public StateActionSO fromState;
        public StateActionSO toState;
        public ConditionSO condition;
        public int priority;
    }

        存储了两个容器:一个字典(游戏中实际使用进行转换),一个列表(编辑器列表)

//存储所有状态转换信息和条件
    private Dictionary<StateActionSO, List<StateTransitionConfig>> states = new Dictionary<StateActionSO, List<StateTransitionConfig>>();

    //获取状态配置,即外部面板的手动配置信息
    [SerializeField] private List<StateTransitionConfig> configStateData = new List<StateTransitionConfig>();


    private StateMachineSystem stateMachineSystem; //当前管理的状态机系统

        初始化函数Init:从编辑器列表中获取到所有信息,存入到字典中。

        //可以在OnValidate函数中实现自动配置(不再需要isInit变量)

 public void Init(StateMachineSystem stateMachineSystem) 
    {
        isInit=true;
        SaveAllStateTransitionInfo();
    }
    

    /// <summary>
    /// 保存所有状态配置信息
    /// </summary>
    private void SaveAllStateTransitionInfo() 
    {
        foreach (var item in configStateData)
        {
            //这个时候外面面板已经配置好信息了。我们需要将它们的转换关系保存起来
            if (!states.ContainsKey(item.fromState)) 
            {
                //检测现在存储字典是否有存在的Key,如果没有我们需要创建一个,并且初始化它的条件存储容器
                states.Add(item.fromState, new List<StateTransitionConfig>());
                states[item.fromState].Add(item);
            }
            else 
            {
                states[item.fromState].Add(item);
            }
        }
    }

        转换的控制函数TryGetApplyCondition:

  /// <summary>
    /// 尝试去获取条件成立的新状态
    /// </summary>
    public void TryGetApplyCondition(StateMachineSystem stateMachineSystem) 
    {
        int transitionPriority = -1;
        StateActionSO toState = null;

        //遍历当前状态能转的状态是否有条件成立
        if (states.ContainsKey(stateMachineSystem.currentState))
        {
            foreach (var stateItem in states[stateMachineSystem.currentState])
            {
                if (stateItem.condition.ConditionSetUp(stateMachineSystem))
                {
                    if (stateItem.priority > transitionPriority)
                    {
                        transitionPriority = stateItem.priority;
                        toState = stateItem.toState;
                    }
                }
            }
        }
        //字典中没有当前状态的描述 直接返回
        else return;

        //可以进行转换状态
        if (toState != null) 
        {
            stateMachineSystem.currentState.OnExit(stateMachineSystem);
            Debug.Log(toState);
            stateMachineSystem.currentState = toState;
            stateMachineSystem.currentState.OnEnter(stateMachineSystem);            
        }
    }

二:敌人的状态机控制类:(挂载在每个敌人上)

要点:1:记录每个敌人及其当前的状态 

           2:每个敌人根据模板生成自己的转换器(保证不冲突)// 代码已更改:实现了转换器复用

           3:在进行tick之前判断是否需要转换状态。

public class StateMachineSystem : MonoBehaviour
{
    public TransitionSO transition;

    public StateActionSO currentState;

    //敌人相关组件
    public EnemyState currentStateType;

    public EnemyController currentEnemy;

    private void Awake()
    {
        currentEnemy = GetComponent<EnemyController>();
        
        if(!transition.isInit)transition.Init();
    }

    private void Start()
    {
        if(currentState!=null)currentState.OnEnter(this);
    }

    private void Update()
    {
        StateMachineTick();
    }

    private void StateMachineTick() 
    {
        if(transition!=null)transition.TryGetApplyCondition(this);//每一帧都去找是否有成立的条件
        //TODO:当前默认为敌人的状态机,后续采用继承,继承出不同种类的状态机(敌人,NPC,友军)等等
        if (CanEnemyAction())
            if(currentState!=null)currentState.OnUpdate(this);

    }

    private bool CanEnemyAction()
    {
        if (currentEnemy == null || currentEnemy.IsDead || currentEnemy.IsHurt || currentEnemy.enemyCharacterStats.IsWeakState||currentEnemy.IsExecuted) return false;
        return true;
    }
}

三:简易AI案例

        1:具体的状态(只包含状态的行为,不用考虑状态的转换)

要点:在传入的StateMachineSystem 中内含了调用敌人的EnemyController,实现不冲突的复用。由于状态类是被所有同种类的敌人复用的,故对于一些在状态中改变的值要上升到EnemyContrller类中,而不能再状态中定义在过程中变化的变量。

        (1)巡逻状态 PatrolState

变量waitTime表示等待时间,对于同一种类的敌人来说是固定不变的,故在状态中定义。

逻辑是每次在出生点周围随机生成一个目标点,达到目标点后进行等待,等待时间完成后随机一个新的目标点进行移动。

[CreateAssetMenu(fileName = "New Patrol State", menuName = "State Machine/State/EnemyPatrolState")] 
public class EnemyPartolState : StateActionSO
{

    [SerializeField]protected float waitTime;  //巡逻到点位后的等待时间

    public override void OnEnter(StateMachineSystem stateMachineSystem)
    {
        EnemyController currentEnemy = stateMachineSystem.currentEnemy;
        stateMachineSystem.currentStateType = EnemyState.PartolState;
        currentEnemy.partolTargetPos=currentEnemy.GetRandomPartolPoint();
        currentEnemy.StartCoroutine(currentEnemy.WaitPatrolTime(waitTime/2, currentEnemy.partolTargetPos));
    }

    public override void OnUpdate(StateMachineSystem stateMachineSystem)
    {
        //如果到达巡逻点 则进入等待,后选择下一个巡逻点
        EnemyController currentEnemy = stateMachineSystem.currentEnemy;
        if (currentEnemy == null || currentEnemy.IsDead||currentEnemy.IsHurt) return;
        if (!currentEnemy.IsWait&&FinishTargetPos(currentEnemy))
        {
            TurnToNewPatrolPoint(currentEnemy);
        }
    }

    
    //到达了指定的巡逻点
    private bool FinishTargetPos(EnemyController currentEnemy)
    {
        return Vector3.Distance(currentEnemy.transform.position, currentEnemy.partolTargetPos) < 1f;
    }

    //Wait,之后转向新的巡逻点
    private void TurnToNewPatrolPoint(EnemyController currentEnemy)
    {
        currentEnemy.partolTargetPos = currentEnemy.GetRandomPartolPoint();
        currentEnemy.StartCoroutine(currentEnemy.WaitPatrolTime(waitTime,currentEnemy.partolTargetPos));
    }

}

        (2)追击状态  ChaseState

进入时改变取消之前可能的巡逻协程并更改速度。敌人朝向时刻朝着主角即可。Update中的条件可忽略(已经在StateMachineSystem的tick前进行了判断)。

[CreateAssetMenu(fileName = "New Chase State", menuName = "State Machine/State/EnemyChaseState")]
public class EnemyChaseState : StateActionSO
{
    public override void OnEnter(StateMachineSystem stateMachineSystem)
    {
        stateMachineSystem.currentStateType = EnemyState.ChaseState;
        stateMachineSystem.currentEnemy.StopAllCoroutines();
        stateMachineSystem.currentEnemy.IsWait = false;
        stateMachineSystem.currentEnemy.curSpeed = stateMachineSystem.currentEnemy.chaseSpeed;
        //currentEnemy.StartCoroutine(currentEnemy.WaitPatrolTime(0, currentEnemy.player.transform.position));  和update中的转视角冲突了
    }

    public override void OnUpdate(StateMachineSystem stateMachineSystem)
    {
        EnemyController currentEnemy = stateMachineSystem.currentEnemy;
        if (currentEnemy == null || currentEnemy.IsDead || currentEnemy.IsHurt) return;
        Vector3 playerPos = currentEnemy.player.transform.position;
        //敌人的视野始终跟随人物
        Quaternion toRotation = Quaternion.LookRotation(new Vector3(playerPos.x, currentEnemy.transform.position.y, playerPos.z) - currentEnemy.transform.position);
        currentEnemy.transform.rotation=Quaternion.Slerp(currentEnemy.transform.rotation, toRotation, Time.deltaTime * currentEnemy.rotateSpeed);
    }


}

        (3)攻击状态  AttackState

1:停止移动    2:判断能否进行攻击   3:调整面向

[CreateAssetMenu(fileName = "New Attack State", menuName = "State Machine/State/EnemyAttackState")]
public class EnemyAttackState : StateActionSO
{
    public override void OnEnter(StateMachineSystem stateMachineSystem)
    {
        stateMachineSystem.currentStateType = EnemyState.AttackState;
        stateMachineSystem.currentEnemy.curSpeed = 0;//停下移动准备攻击
    }

    public override void OnUpdate(StateMachineSystem stateMachineSystem)
    {
        EnemyController currentEnemy = stateMachineSystem.currentEnemy;
        if (currentEnemy == null || currentEnemy.IsDead||currentEnemy.IsHurt) return;
        float distance = Vector3.Distance(currentEnemy.transform.position, currentEnemy.player.transform.position);
        //敌人在攻击范围内,先判断敌人现在是否正在攻击
        if(distance<currentEnemy.AttackStateDistance&&!currentEnemy.IsAttack)
        {
            if (!currentEnemy.IsHurt)
                Attack(currentEnemy, distance);
        }
        //敌人的视野始终跟随人物
        Vector3 playerPos = currentEnemy.player.transform.position;
        Quaternion toRotation = Quaternion.LookRotation(new Vector3(playerPos.x, currentEnemy.transform.position.y, playerPos.z) - currentEnemy.transform.position);
        currentEnemy.transform.rotation=(Quaternion.Slerp(currentEnemy.transform.rotation, toRotation, Time.deltaTime * currentEnemy.rotateSpeed));
    }

    public override void OnExit(StateMachineSystem stateMachineSystem)
    {
        stateMachineSystem.currentEnemy.curSpeed = stateMachineSystem.currentEnemy.chaseSpeed;
    }

    private void Attack(EnemyController currentEnemy,float distance)
    {
        //敌人在近战攻击距离内 使用近战攻击
        if (distance < currentEnemy.AttackDistanceNear)
        {
            AttackNear(currentEnemy);
        }
        //否则,即如果敌人在远程攻击距离内,使用远程攻击
        else if (distance < currentEnemy.AttackDistanceFar)
        {
            AttackFar(currentEnemy);
        }
    }

    private void AttackNear(EnemyController currentEnemy)
    {
        currentEnemy.anim.SetTrigger("AttackNear");
    }

    private void AttackFar(EnemyController currentEnemy)
    {
        currentEnemy.anim.SetTrigger("AttackFar");
    }
}

       2:具体的转换条件

        (1)巡逻 ---> 追击    发现敌人

        (2)追击 ---> 巡逻    没有发现敌人

        (2)追击 ---> 攻击    进入攻击范围

        (2)攻击--->  追击    脱离攻击范围

        3:编辑器中的TransitionSO配置:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值