Unity中使用状态机控制角色行为

接着上一篇说的随谈过去2019参与的一款Unity项目开发,这篇专门记录下FSM的使用和别人写的一个具体实现。

其实很多普遍功能,网上一搜一般都可以找到具体实现,而且有很多重复的文章。我平时要了解学习某个功能,一般都会看多篇文章,而不是只看一篇,以此了解下不同人的想法。我这里再写一篇,只是让自己重新梳理一下流程,留个记录,也方便自己以后忘了可以直接看自己当时的思路。

Demo里我使用了别人实现的SuperStateMachine和Unity自带的Animator组件(当然,Animator也是一个状态机)来控制物体的行为和状态间的切换。SuperStateMachine的源码不多,我会讲下它的具体实现。由于Animator看不到源码,所以就只是讲下它的使用。不过两个状态机用起来都很方便(写完Demo后,感觉Animator没那么方便…)。

先上Demo的效果图在这里插入图片描述
什么是状态机这里就不多说了,这里直接贴上源码。图中的逻辑也不难,代码并不多,简单的怪物生成逻辑、文本显示逻辑和相机的控制也不说,感兴趣的可以看下原工程,文章末尾放出链接。只关注状态机,先来SuperStateMachine

/*
 * original code 源码地址
 * https://github.com/liangdong-xd/SuperMario64HD/blob/master/Assets/SuperCharacterController/Core/SuperStateMachine.cs
 */
using System;
using System.Collections;
using System.Reflection;
using System.Collections.Generic;
using UnityEngine;

/*
 * 这个状态机和别的不太一样,一般都是把状态机和状态分成单独的两个类
 * 然后不同的状态都继承状态类去具体实现状态中的那三个方法
 * 而这个状态机直接在派生的状态机中实现各个状态的三个方法
 * 这样的好处是,这个状态机所在的对象的属性是共享给所有状态的
 * 而独立成类的状态,每个都需要单独保存一份对象属性的引用,像Animator的使用就是如此
*/
public class SuperStateMachine : MonoBehaviour
{
	// 这个只是描述一个状态,具体的状态不需要继承它来实现
    struct State {
        public Action onEnter;
        public Action onUpdate;
        public Action onExit;

        public Enum current;
    }

	// 保存当前状态的三个回调
    State state = new State();

	// 通过该枚举变量来切换当前状态
    public Enum currentState {
        get => state.current;
        set {
            ChangingState();
            state.current = value;
            TranslateState();
        }
    }

	// 用于记录上一个状态,切换状态时,需执行上一状态的onExit()回调
    public Enum lastState {get; private set;}

	// 进入当前状态的时间
    protected float timeEnteredState;

	// 每帧更新函数,用于执行当前状态的onUpdate()回调
    void Update() {
        OnEarlyUpdate();
        state.onUpdate();
        OnLateUpdate();
    }

	// 状态更新之前执行的更新,方便派生类实现具体的逻辑
    virtual protected void OnEarlyUpdate() {}

	// 状态更新之后执行的更新,方便派生类实现具体的逻辑
    virtual protected void OnLateUpdate() {}

	// 开始改变状态
    void ChangingState() {
        lastState = currentState;
        timeEnteredState = Time.time;
    }

	// 切换状态
    void TranslateState() {
    	// 此时state保存的还是上一个状态的数据。先执行上一个状态的onExit()
        if (state.onExit != null) {
            state.onExit();
        }

		// 获取并更新当前状态的数据
        state.onEnter = GetAction("EnterState");
        state.onUpdate = GetAction("UpdateState");
        state.onExit = GetAction("ExitState");

		// 执行当前状态的onEnter()
        if (state.onEnter != null) {
            state.onEnter();
        }
    }

	// 通过键值对保存各个状态的回调数据,避免每次都通过映射来获取
    Dictionary<Enum, Dictionary<string, Action>> cache = new Dictionary<Enum, Dictionary<string, Action>>();

	// 通过name来获取当前状态的对应回调
    Action GetAction(string name) {
    	// 用于保存当前状态的三个回调
        Dictionary<string, Action> actions;
        // 首次获取状态回调,缓存中是不存在数据的,需先new一个
        if (!cache.TryGetValue(currentState, out actions)) {
            actions = new Dictionary<string, Action>();
            cache.Add(currentState, actions);
        }

		// name对应的回调
        Action action = null;
        // 首次获取该回调,也是不存在的,需通过映射来获取。之后都直接通过缓存获取就行了
        if (!actions.TryGetValue(name, out action)) {
        	// 状态回调是在状态机的派生类中实现的,命名规则举个例子:Idle_EnterState()这样
            var methodInfo = GetType().GetMethod(currentState.ToString() + "_" + name, BindingFlags.Instance | BindingFlags.NonPublic); // 这是C#的官方API
            if (methodInfo != null) {
            	// 通过映射信息创建回调
                action = (Action)Delegate.CreateDelegate(typeof(Action), this, methodInfo);
            } else {
            	// 映射信息为空,避免调用回调时报错,直接赋值一个空函数
                action = DoNothing;
            }
            // 保存到该状态对应的回调变量里
            actions.Add(name, action);
        }

		/*
		 * 其实那个cache字典就是一个enum对应一个状态的回调数据的字典
		 * 状态回调数据的字典通过一个string对应一个回调
		 */

		// 返回对应的回调
        return action;
    }

    static void DoNothing() {}
}

状态机的逻辑实现就是这样了,很简洁。我是精简了一些源码的,具体内容可以点开代码开头的链接。

用法也很简单,贴个缩略版代码:

public class FSMAI : SuperStateMachine
{
    public enum EntityState {
        Idle,
		// 其它更多的state
    }

	void Awake() {
		// 切换到Idle状态
		currentState = EntityState.Idle;
	}

	// 注意命名,前缀必须跟上面定义的enum一致,后缀则必须用那三个,然后中间一个下划线
    void Idle_EnterState() {

    }

    void Idle_UpdateState() {

    }

    void Idle_ExitState() {

    }

将上面的脚本挂到对象上,写好控制对象的逻辑就可以控制对象的行为了。至于截图中的效果,点开文章末尾的github链接就可以看到源码了。代码有点点长,所以就不贴出来了。

SuperStateMachine的整体就这样了。下面再简单说下Animator,用Animator也可以实现同样的效果,虽然平时都是用Animator播放动画。

在Animator的编辑面板建四个空的State如下图所示。因为角色本身就挂了个Animator播放动画用的,而且每个对象只能挂一个Animator组件,不过Animator是个分层状态机,所以新加个层Behaviors,然后在这里建State。
在这里插入图片描述
选中State,到Inspector面板给当前的State添加一个脚本-AddBehavior:
在这里插入图片描述
脚本内容大概就这样:

public class Idle : StateMachineBehaviour
{
	// 这种将State独立成一个类的,就有前面说的那个问题了:需单独保存一份控制对象的数据
    private object data;

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
    	// 避免每次进来都调用,确保只执行一次
        if (data == null) {
            data = animator.gameObject.GetComponent<Component>();
        }

		// Animator版的状态切换
		animator.Play("Idle", animator.GetLayerIndex("Behaviors"));
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        
        }
    }

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        
    }
}

Animator的用法大致就这样了。还是那句话,图片中的效果,感兴趣的可以点开github地址。

最后就是Demo代码的地址了:Unity-SimpleFSMDemo

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity,角色状态机是一种用于管理角色状态和行为的系统。通过将角色状态类作为角色类的属性,并将角色的行为拆分成多个状态子类,可以降低代码的耦合度。这种实现方式可以帮助开发者更好地组织和管理角色的各种状态和行为。 需要注意的是,状态机适用于具有多种较为复杂状态的角色或其他物体。如果角色只有个别几个特别简单的状态,可能不需要使用状态机。 在Unity,还有一种称为Mecanim的状态机系统。Mecanim借用了计算机科学状态机概念来简化对角色动画的控制。它提供了一种纵览角色所有动画片段的方法,并且允许通过各种事件来触发不同的动画效果。使用Mecanim状态机,开发者可以更灵活地控制角色的动画和动作表现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Unity简单角色状态机实现](https://download.csdn.net/download/z625309640/9995038)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [Unity 状态机入门-用状态机开发人物](https://blog.csdn.net/tynewbilar/article/details/130463494)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Unity--状态机基础](https://blog.csdn.net/qq_42698957/article/details/107092795)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值