基于有限状态机实现角色逻辑控制与动画控制相统一(待机,移动,攻击)

前言:在写角色控制器的时候,我在写角色的逻辑控制的时候,比如是否按下wasd或者攻击键,都是将其放在一个庞大的update里面去检测,这样会写很多的if-else,并且,我在攻击时希望玩家停止移动,就要去增加一个bool值,而且这种大量的逻辑判断都是放在一个update,状态一切换,会改变很多数据,看着非常丑。参考了一下有限状态机的设计模式以及动画状态机的实现方式,设计一套结构清晰的角色控制器。

效果:这效果图片貌似看不出来啥,就文字描述下吧,待机和移动之间是秒切换,按住移动键再按攻击J,会停止移动,等到攻击结束若还按着移动键会切换为移动动画并进行逻辑移动,攻击结束后若没按任何键会回到待机状态,攻击的后摇时间可以通过代码添加附加系数控制。

简单介绍一下有限状态机的设计模式:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。比如按下攻击键,我就从移动或者待机->进入到攻击模式(不会进行坐标改变)

->攻击结束回调进入下一个阶段。这样很好的把一个状态抽象成一个节点,状态与状态间的耦合性大幅下降。

1.代码:

IState.CS脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//每个状态节点的内部设计
public interface IState
{
    public void OnEnter();
    public void OnUpdate();
    public void OnExit();
}

AllStates.CS脚本用来定义每个状态应该实现的逻辑控制。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//AllStates脚本


/// <summary>
/// 状态枚举类型
/// </summary>
public enum State_Enum
{
    idle,
    move,
    attack
}
/// <summary>
/// Idle状态
/// </summary>
public class Idle : IState
{
    private Role baseFSM;
    //构造函数获取刚刚创建脚本的属性方法
    public Idle(Role manager)
    {
        this.baseFSM = manager;
    }
    public void OnEnter()
    {
        baseFSM.animator.SetBool("Idle", true);
    }

    public void OnExit()
    {
        baseFSM.animator.SetBool("Idle", false);
    }

    public void OnUpdate()
    {
        if(Input.GetAxisRaw("Horizontal")!=0||Input.GetAxisRaw("Vertical")!=0)
        {
            baseFSM.TransitionState(State_Enum.move);
        }
        if (Input.GetKeyDown(KeyCode.J))
        {
            baseFSM.TransitionState(State_Enum.attack);
        }
    }
}

/// <summary>
/// Move状态
/// </summary>
public class Move : IState
{
    private Role baseFSM;
    //构造函数获取刚刚创建脚本的属性方法
    public Move(Role manager)
    {
        this.baseFSM = manager;
    }
    public void OnEnter()
    {
        baseFSM.animator.SetBool("Move", true);
    }

    public void OnExit()
    {
        baseFSM.animator.SetBool("Move", false);
    }

    public void OnUpdate()
    {
        baseFSM.MovePos();//逻辑移动
        if (Input.GetAxisRaw("Horizontal") == 0 &&Input.GetAxisRaw("Vertical") == 0)
        {
            baseFSM.TransitionState(State_Enum.idle);
        }
        if (Input.GetKeyDown(KeyCode.J))
        {
            baseFSM.TransitionState(State_Enum.attack);
        }
    }
}

/// <summary>
/// Attack状态
/// </summary>
public class Attack : IState
{
    private Role baseFSM;
    AnimationEvent attackEve = new AnimationEvent();
    public Attack(Role manager)
    {
        this.baseFSM = manager;
    }
    public void OnEnter()
    {
        attackEve.functionName = "EndAttack";
        attackEve.time = baseFSM.allClips["atk"].length;//可以修改后摇时长
        baseFSM.allClips["atk"].AddEvent(attackEve);
        baseFSM.animator.SetTrigger("Attack");
        
    }

    public void OnExit()
    {
        baseFSM.allClips["atk"].events = default;
    }

    public void OnUpdate()
    {
        //Attack状态动态的脚本写在这里
    }
}

上面重点讲一下Attack这个攻击类,与Move类和Idle不同,它多加了一个攻击结束后的回调方法,因为本身的设定,攻击动画的切换是通过触发来切换的,攻击动画结束后自动回到待机或者移动,所以不放在OnUpdate里面进行检测,而是给动画添加帧事件自动触发。而帧方法EndAttack是必须放到接下来的Role脚本里面的

Base.CS脚本用来定义玩家自己的基本属性,这个可以自己设定。这边单独抽出来是习惯把数据库的东西单独抽出来方面后续的封装,也可以直接放到地下的Role角色控制脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Base : MonoBehaviour
{
    protected int attack;//攻击力
    protected int blood;//血量
    protected int mana;//法力
    protected int moveXSpeed;//X轴移动速度
    protected int moveYSpeed;//Y轴移动速度
    protected Vector2 moveDir;
    protected Vector2 curPos;
    protected int lookDir;//设置朝向
    protected void SetAttribute()
    {
        attack = 10;
        moveXSpeed = 3;
        moveYSpeed = 2;
        blood = 100;
        mana = 20;
        moveDir = Vector2.zero;
        curPos = Vector2.zero;
    }
}

Role.CS脚本最终用来控制角色的脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;



public class Role :Base
{
    //一个状态生命周期的三个关键函数
    public IState currenState;
    [HideInInspector]
    public Animator animator;
    //存储动画状态机上的所有动画
    public Dictionary<string, AnimationClip> allClips = new Dictionary<string, AnimationClip>();
    //定义字典通过键值对建立状态与其对象的联系
    private Dictionary<State_Enum, IState> states = new Dictionary<State_Enum, IState>();
    private void Awake()
    {
        //获得动画控制器上的所有动画
        animator = GetComponent<Animator>();
        AnimationClip[] clips= animator.runtimeAnimatorController.animationClips;
        for(int i = 0; i < clips.Length; i++)
        {
            allClips.Add(clips[i].name, clips[i]);
        }
        //设置人物的基础属性
        SetAttribute();
        //向字典中添加对应的状态与其对象
        states.Add(State_Enum.idle, new Idle(this));
        states.Add(State_Enum.move, new Move(this));
        states.Add(State_Enum.attack, new Attack(this));
        //默认状态为idle
        TransitionState(State_Enum.idle);

    }
    private void Update()
    {
        currenState.OnUpdate();
    }
    /// <summary>
    /// 有限状态机状态切换函数
    /// </summary>
    /// <param name="type"></param>
    public void TransitionState(State_Enum type)
    {
        if (currenState != null)
        {
            currenState.OnExit();
        }
        currenState = states[type];
        currenState.OnEnter();
    }

    public void SetLookDir()
    {
        lookDir = (int)Input.GetAxisRaw("Horizontal");
        if (lookDir != 0)
        {
            transform.localScale = new Vector3(lookDir, 1, 1);
        }
    }
    public void MovePos()
    {
        SetLookDir();
        moveDir.x = Input.GetAxisRaw("Horizontal") * moveXSpeed;
        moveDir.y = Input.GetAxisRaw("Vertical") * moveYSpeed;
        curPos = (Vector2)transform.position;
        curPos = curPos + moveDir * Time.deltaTime;
        transform.position = curPos;
    }
    public void EndAttack()
    {
        if (Input.GetAxisRaw("Horizontal") == 0 && Input.GetAxisRaw("Vertical") == 0)
        {
            TransitionState(State_Enum.idle);
        }
        else
        {
            TransitionState(State_Enum.move);
        }
    }
}

2.动画状态机切换条件设置:

Idle和Move动画都是循环动画

Idle->Move:取消勾选HasExitTime,可以适当调小idle到move的过度时间,就是相交的小蓝条宽度,Move=true,Idle=false

Move->Idle:取消勾选HasExitTime,Move=false,Idle=true

Move->Attack:取消勾选HasExitTime(因为我们有通过代码添加攻击动画的回调函数,所以不需要勾选),有Attack Trigger参数,Move=false

Attack->Move:取消勾选HasExitTime,无Attack Trigger参数,Move=true

Idle->Attack:取消勾选HasExitTime,有Attack Trigger参数,Idle=false

Attack->Idle:取消勾选HasExitTime,无Attack Trigger参数,Idle=true

参考链接:https://blog.csdn.net/xinzhilinger/article/details/115840911

本篇参考了别人写的FSM的架构,在其基础上增加了逻辑控制的具体内容,实现逻辑控制与动画表现相统一

猜你喜欢

转载自blog.csdn.net/ysn11111/article/details/128768635