Unity笔记-状态机

有限状态机(FSM)

有限状态:状态持续时长有限(例:人物有很多状态,如跑步,攻击,游泳。这些状态需进行切换,不可能无限进行一个状态,如人物不可能一直保持攻击状态,
不进行跑步,游泳)
切换这些状态的东西叫做状态机。
举例:下面是一个角色相关脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Update(){
if(按下wasd){
移动
播放移动动画
}else{
静止
播放站立动画
}
if(按下空格){
跳跃
播放跳跃动画
播放声音
}
}

该方法优点:简单
该方法缺点:繁琐,if过多,重复元素多,状态过多时可读性降低,代码简洁性低
状态机把这些状态独立出来,这时状态机只做切换,不用知道状态实现过程
这是一种面向对象思想
简单有限状态机:将每个状态独立成一个方法
优点:简单,体现面向对象思想,封装成方法使用便捷
缺点:大型使用场景依然不够方便
普通有限状态机:将每个状态独立成一个类,继承一个状态基类
优点:逻辑清晰有条理,更加简单
缺点:代码量提升

Unity

在工程项目中导入 Character Pack:Free Sample包(去Unity资源商城下,免费)
在Assets\Supercyan Character Pack Free Sample\Prefabs\Base\High Quality选择MaleFree1,创建一个平面,将模型放置到平面上。
在文件夹中找到common_people@idle(待机)common_people@run(跑步)common_people@wave(招手)再创建一个动画控制器(Player),将三个动画
拖进里面,再创建一个Bool参数IsRun。在待机和跑步中相互创建过渡,过渡条件是IsRun的值。将两个过渡的退出时间关掉。
创建一个Trigger参数Wave,将待机切换为挥手的过渡条件设置为Wave,并且将此过渡的退出时间和固定持续时间关闭(第一个和第三个参数)

代码

  • 创建Script文件夹。
  • 创建一个csharp脚本PlayerController

    V1

    不使用状态机,初始版本代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class PlayerController : MonoBehaviour
    {
    // Start is called before the first frame update
    //获取动画控制器
    private Animator ani;
    void Start()
    {
    ani = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
    //获取用户的水平和垂直方向的输入
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    //创建为一个方向向量
    Vector3 dir = new Vector3(h,0,v);
    //如果向量不为空
    if(dir!=Vector3.zero){
    //用户按下移动按键
    //移动
    transform.rotation = Quaternion.LookRotation(dir);
    transform.Translate(Vector3.forward * 3 * Time.deltaTime);
    //播放动画
    ani.SetBool("IsRun",true);
    }else{
    //用户松开移动按键
    ani.SetBool("IsRun",false);
    }
    //按下空格
    if(Input.GetKeyDown(KeyCode.Space))
    {
    ani.SetTrigger("Wave");
    } }
    }
    挂载到Player身上,运行。
    小人成功移动,除招手动画无异常,即可阅读下一模块。

    V2

    在刚才的V1代码中,小人招手的时候可以进行移动,但不会实现移动动画。看起来很奇怪。这是因为代码是由上往下执行的。当挥手动作执行时也会
    执行上面的移动。在以后代码中会改进这一问题。
    创建PlayerControl2脚本
    代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    public enum PlayerState{//枚举
    idle,
    run,
    wave
    }
    public class PlayerContorl2 : MonoBehaviour
    {
    public Animator ani;
    private PlayerState state = PlayerState.idle;
    /// <summary>
    /// Update is called every frame, if the MonoBehaviour is enabled.
    /// </summary>
    private void Update()
    {
    switch(state){
    case PlayerState.idle:
    idle();
    break;
    case PlayerState.run:
    run();
    break;
    case PlayerState.wave:
    wave();
    break;
    }
    }
    void idle(){
    //获取用户的水平和垂直方向的输入
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    //创建为一个方向向量
    Vector3 dir = new Vector3(h,0,v);
    //如果向量不为空
    if(dir==Vector3.zero){

    ani.SetBool("IsRun",false);
    }else{
    state = PlayerState.run;//不管实现过程,只去调用
    }
    if(Input.GetKeyDown(KeyCode.Space)){
    state = PlayerState.wave;
    }
    }
    void run(){
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    //创建为一个方向向量
    Vector3 dir = new Vector3(h,0,v);
    //如果向量不为空
    if(dir!=Vector3.zero){
    //用户按下移动按键
    //移动
    transform.rotation = Quaternion.LookRotation(dir);
    transform.Translate(Vector3.forward * 3 * Time.deltaTime);
    //播放动画
    ani.SetBool("IsRun",true);
    }else{
    state = PlayerState.idle;
    }
    }
    void wave(){
    //播放挥手动画
    ani.SetTrigger("Wave");
    if(!ani.GetCurrentAnimatorStateInfo(0).IsName("Wave")){
    //如果没有播放挥手动画
    state = PlayerState.idle;
    }
    }
    }
    解析一下:首先定义了一个枚举,定义了idle,run,wave三个成员(值分别是1,2,3)
    然后就是定义三个方法,状态切换时每个状态不用像V1一样关注另一个状态,直接切换枚举值。
    然后使用Switch来互相切换状态,达到状态机的作用。
    在Wave方法里,使用了!ani.GetCurrentAnimatorStateInfo(0).IsName(“Wave”)判断当前动画是否播放完。

    V3

    状态基类

    代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public abstract class FSMState//状态基类
    {
    //当前状态ID
    public int StateID;
    //状态拥有者
    public MonoBehaviour Mono;
    //状态所属管理器
    public FSMManager FsmManager;
    public FSMState(int stateID,MonoBehaviour mono,FSMManager manager){
    StateID = stateID;
    Mono = mono;
    FsmManager = manager;
    }
    //进入状态,会调用一次方法
    public abstract void OnEnter();
    //在状态中每帧调用
    public abstract void OnUpdate();
    }

    状态管理器基类

    代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    //状态机管理类
    public class FSMManager//非单例 可以有很多个类型的状态机
    {
    //状态列表
    public List<FSMState> StateList = new List<FSMState>();
    public int CurrentIndex = -1;
    //改变状态
    public void ChangeState(int StateID){
    CurrentIndex = StateID;
    //执行一次该状态的进入方法
    StateList[CurrentIndex].OnEnter();
    }
    //更新
    public void Update()
    {
    if(CurrentIndex != -1){
    StateList[CurrentIndex].OnUpdate();
    }
    }
    }

    普通状态机

  • 创建 PlayerControl3 脚本
  • 创建 Idle Run Wave 脚本

    Idle

    代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class Idle : FSMState
    {
    public enum PlayerState{//枚举
    idle,
    run,
    wave
    }
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
    public override void OnEnter()
    {
    //播放站立动画
    Mono.GetComponent<Animator>().SetBool("IsRun",false);

    }
    public override void OnUpdate()
    {
    //判断是否变为跑步
    //获取用户的水平和垂直方向的输入
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    //创建为一个方向向量
    Vector3 dir = new Vector3(h,0,v);
    //如果向量不为空
    if(dir!=Vector3.zero){
    //切换跑步状态
    FsmManager.ChangeState((int)PlayerState.run);
    }
    //监听是否挥手
    if(Input.GetKeyDown(KeyCode.Space)){
    FsmManager.ChangeState((int)PlayerState.wave);
    }
    }
    public Idle(int stateID,MonoBehaviour mono,FSMManager manager) : base(stateID,mono,manager){

    }
    }

    Run

    代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class Run : FSMState
    {
    public enum PlayerState{//枚举
    idle,
    run,
    wave
    }
    // Start is called before the first frame update
    public Run(int stateID,MonoBehaviour mono,FSMManager manager) : base(stateID,mono,manager){

    }
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
    public override void OnEnter()
    {
    Mono.GetComponent<Animator>().SetBool("IsRun",true);
    }
    public override void OnUpdate(){
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    //创建为一个方向向量
    Vector3 dir = new Vector3(h,0,v);
    //如果向量不为空
    if(dir!=Vector3.zero){
    //用户按下移动按键
    //移动
    Mono. transform.rotation = Quaternion.LookRotation(dir);
    Mono. transform.Translate(Vector3.forward * 3 * Time.deltaTime);
    }else{
    FsmManager.ChangeState((int)PlayerState.idle);
    }
    }
    }

    Wave

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class Wave : FSMState
    {
    // Start is called before the first frame update
    public Wave(int stateID,MonoBehaviour mono,FSMManager manager) : base(stateID,mono,manager){

    }
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
    public override void OnEnter()
    {
    Mono.GetComponent<Animator>().SetTrigger("Wave");
    }
    public override void OnUpdate()
    {
    if(!Mono.GetComponent<Animator>().GetCurrentAnimatorStateInfo(0).IsName("wave")){
    //切换为Idle
    FsmManager.ChangeState((int)PlayerState.idle);
    }
    }
    }

    PlayerControl3

    开始编辑V3状态机:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class PlayerControl3 : MonoBehaviour
    {
    // Start is called before the first frame update
    private FSMManager fsmManager;
    void Start()
    {

    //实例化状态机管理器
    fsmManager = new FSMManager();
    //创建状态
    Idle idle = new Idle(0,this,fsmManager);
    Run run = new Run(1,this,fsmManager);
    Wave wave = new Wave(2,this,fsmManager);
    //状态注册
    fsmManager.StateList.Add(idle);
    fsmManager.StateList.Add(run);
    fsmManager.StateList.Add(wave);
    //给一个默认状态
    fsmManager.ChangeState((int)PlayerState.idle);
    }

    // Update is called once per frame
    void Update()
    {
    fsmManager.Update();//调用自己写的Update

    }
    }
    然后将idle到wave的过渡时间设置为0,不然小人挥手时还是能动。(具体原因未知,知道的可以评论下)
    挂载到player身上,运行。