首页 > 其他分享 >Unity基于状态机的架构与设计

Unity基于状态机的架构与设计

时间:2023-10-24 15:14:33浏览次数:38  
标签:架构 void 接口 curNode 状态机 Unity IFsmNode 节点

我们做游戏的时候经常会有流程控制,流程控制的方法有很多,行为决策树,状态机等。本质差别都不大,就是把每一段执行逻辑做成一个一个的节点,根据条件执行某个节点,切换到某个节点。今天给大家分享一下基于状态机来做游戏流程的控制。

 

1 一个简单的状态机案例

 

我们先来拆解一个使用案例,通过这个案例让大家对状态机的流程控制有一个基本的了解。首先我们来构建一些状态节点,放入到状态机中。编写伪代码如下:

创建一个状态机: 

FiniteStateMachine _fsm = new FiniteStateMachine()

往状态机里面加入所有控制流程状态的逻辑节点:

_fsm.AddNode(new NodeInit());

_fsm.AddNode(new NodeLogin());

_fsm.AddNode(new NodeTown());

初始化逻辑节点NodeInit,用来做初始化的逻辑控制, NodeLogin,用来做登录场景的逻辑控制, NodeTown节点用来做游戏战斗场景的逻辑控制。

对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础小白,也有一些正在从事游戏开发的技术大佬,欢迎你来交流学习。

1.png

每个状态机节点,都有几个统一的固定的入口,这些入口如何设计与行业相关,比如我们的游戏行业,设计状态机节点接口一般如下:

 public interface IFsmNode
	{
		/// <summary>
		/// 节点名称
		/// </summary>
		string Name { get; }

		void OnEnter();
		void OnUpdate();
		void OnFixedUpdate();
		void OnExit();
		void OnHandleMessage(object msg);
	}

Name: 状态机节点的名字;

OnEnter: 状态机进入到这个状态节点时执行,一般用于初始化;

OnExit: 状态机来开这个状态节点时执行,一般用户结束时候的一些销毁资源与释放等;

OnUpdate: 每一帧都会调用状态机节点的update, 很多每帧处理的事务可以放OnUpdate;

OnFixedUpdate: 每个FixedUpdate 都会调用状态机的OnFixedUpdate函数,一些固定迭代次数的更新可以放此接口。

OnHandleMessage(object msg): 给状态机节点触发事件消息的时候调用这个接口,来作为状态机节点处理事件消息的控制入口。

 

每个状态机节点,都实现IFsmNode所对应的接口,放入到状态机中统一管理。案例中我们在游戏开始时先执行NodeInit状态节点,完成游戏的初始化。

 	public void StartGame()
	{
		_fsm.Run(nameof(NodeInit));
	}

先来看NodeInit节点处理的逻辑,NodeInit只在OnEnter里面实现了初始化的相关逻辑,其它接口,没有任何逻辑处理。代码如下

 	void IFsmNode.OnEnter()
	{
		AudioPlayerSetting.InitAudioSetting();

		// 使用协程初始化
		this.StartCoroutine(Init());
	}

	private IEnumerator Init()
	{
		// 加载UIRoot
		var uiRoot = WindowManager.Instance.CreateUIRoot<CanvasRoot>("UIPanel/UIRoot");
		yield return uiRoot;

		// 加载常驻面板
		yield return GameObjectPoolManager.Instance.CreatePool("UIPanel/UILoading", true);

		// 进入到登录流程
		FsmManager.Instance.Change(nameof(NodeLogin));
	}

如上面的代码所示, 当状态机执行NodeInit节点状态的时候,会初始化时调用OnEnter接口, NodeInit的OnEnter接口中,调用了Init函数来做初始化,首先会创建一个UIRoot, 然后把资源加载界面显示出来,完成资源加载后,进入到登录逻辑节点场景,注意这里,状态机就由原来的NodeInit切换到NodeLogin状态机节点。当进入NodeLogin节点的时候,就会执行它的OnEnter接口,接下来我们看下登录节点的逻辑处理如下:

 void IFsmNode.OnEnter()
	{
		var uiwindow = UITools.OpenWindow<UILogin>();
		uiwindow.Completed += Uiwindow_Completed;

		string sceneName = "Scene/Login";
		SceneManager.Instance.ChangeMainScene(sceneName, null);
	}

显示一个登录的UI界面,同时切换场景到登录场景,这样我们的状态机控制逻辑就切换到登录场景了,如图所示:

2.png

接下来输入用户名+密码,点击”Run Game”按钮,看下RunGame按钮的处理:

 private void OnClickLogin()
	{
		// 替换按钮图片
		if (_loginSprite.SpriteName == "Button_Rectangular_Large_Green_Background")
			_loginSprite.SpriteName = "Button_Rectangular_Large_Red_Background";
		else
			_loginSprite.SpriteName = "Button_Rectangular_Large_Green_Background";

		// 发送登录事件
		var message = new LoginEvent.ConnectServer
		{
			Account = _account.text,
			Password = _password.text
		};
		EventManager.Instance.SendMessage(message);
	}

 

给状态机的节点发送一个登录事件消息, 这样就可以调用到状态机节点的事件处理函数,

 	private void OnHandleEvent(IEventMessage msg)
	{
		if(msg is LoginEvent.ConnectServer)
		{
			FsmManager.Instance.Change(nameof(NodeTown));
		}
	}

在事件处理函数中,调用状态机切换到NodeTown状态机节点运行。最后我们来看下NodeTown游戏战斗场景中的节点处理,初始化OnEnter接口如下:

 	void IFsmNode.OnEnter()
	{
		string sceneName = "Scene/Town";
		SceneManager.Instance.ChangeMainScene(sceneName, OnSceneLoad);
		UITools.OpenWindow<UILoading>(sceneName);
		UITools.OpenWindow<UIMain>();
		AudioManager.Instance.PlayMusic("Audio/Music/town", true);
	}

 

切换到游戏战斗场景,显示战斗的主UI, 播放游戏的背景音乐。在看下其它接口,OnUpdate迭代游戏世界变化,OnExit, 删除掉游戏世界释放掉资源,代码如下:

 	void IFsmNode.OnExit()
	{
		_gameWorld.Destroy();
		UITools.CloseWindow<UIMain>();
	}

如图所示:

3.png

通过这个案例的分析,我们确定了游戏状态机的设计,总结如下:

Step1: 设计一些游戏状态节点,节点中实现具体的一些逻辑处理接口;

Step2: 将游戏状态节点加入到游戏状态机中;

Step3: 给状态机编写好”切换节点”的接口,进入节点之前,先调用上一个节点的离开OnExit接口,然后调用新节点的OnEnter接口, 根据游戏的需求,每次Update, FixedUpdate, 迭代状态机节点的OnUpdate与OnFixedUpdate接口。

 

2基于状态机控制的具体实现与设计

 

有了上面的分析,我们对状态机就了解的很清楚了,自然设计一个状态机用来控制游戏的跳转控制逻辑就是非常简单的事情了,我们把游戏中的基于状态机的控制分成“与项目无关”与“与游戏项目相关”的两个部分来设计与处理。先来看下”与项目无关”的状态机部分设计: 两个代码: IFsmNode.cs与FiniteStateMachine.cs, IFsmNode.cs代码负责定义状态机节点的接口,上文中的代码已经给出了游戏开发中状态机节点常用接口。开发者在实现具体业务逻辑的时候,只要继承这个接口并实现即可。

FiniteStateMachine.cs, 主要实现了对状态机节点的管理,主要数据成员与接口如下:

privatereadonly List<IFsmNode> _nodes = new List<IFsmNode>(); 定义一个数据成员保存所有的状态机节点。

private IFsmNode _curNode;

private IFsmNode _preNode;

定义两个数据成员 curNode与prevNode来保存当前正在运行的状态节点与上一个状态节点;

publicvoid AddNode(IFsmNode node) 定义一个接口,将新的状态节点加入到状态机中;

publicvoid Run(string entryNode) 定义一个接口,作为执行第一个状态节点的接口;

publicvoid Transition(string nodeName)定义一个接口,作为执行由当前状态切换到新的状态机节点的接口;

基于Update,来调用当前执行的状态机节点的Update,FixedUpdate, HandleMessage接口。

 public class FiniteStateMachine
	{
		private readonly List<IFsmNode> _nodes = new List<IFsmNode>();
		private IFsmNode _curNode;
		private IFsmNode _preNode;

		/// <summary>
		/// 节点转换关系图
		/// 注意:如果为NULL则不检测转换关系
		/// </summary>
		public FsmGraph Graph;

		/// <summary>
		/// 当前运行的节点名称
		/// </summary>
		public string CurrentNodeName
		{
			get { return _curNode != null ? _curNode.Name : string.Empty; }
		}

		/// <summary>
		/// 之前运行的节点名称
		/// </summary>
		public string PreviousNodeName
		{
			get { return _preNode != null ? _preNode.Name : string.Empty; }
		}


		/// <summary>
		/// 启动状态机
		/// </summary>
		/// <param name="entryNode">入口节点</param>
		public void Run(string entryNode)
		{
			_curNode = GetNode(entryNode);
			_preNode = GetNode(entryNode);

			if (_curNode != null)
				_curNode.OnEnter();
			else
				MotionLog.Error($"Not found entry node : {entryNode}");
		}

		/// <summary>
		/// 显示帧更新
		/// </summary>
		public void Update()
		{
			if (_curNode != null)
				_curNode.OnUpdate();
		}

		/// <summary>
		/// 物理帧更新
		/// </summary>
		public void FixedUpdate()
		{
			if (_curNode != null)
				_curNode.OnFixedUpdate();
		}

		/// <summary>
		/// 加入一个节点
		/// </summary>
		public void AddNode(IFsmNode node)
		{
			if (node == null)
				throw new ArgumentNullException();

			if (_nodes.Contains(node) == false)
			{
				_nodes.Add(node);
			}
			else
			{
				MotionLog.Warning($"Node {node.Name} already existed");
			}
		}

		/// <summary>
		/// 转换节点
		/// </summary>
		public void Transition(string nodeName)
		{
			if (string.IsNullOrEmpty(nodeName))
				throw new ArgumentNullException();

			IFsmNode node = GetNode(nodeName);
			if (node == null)
			{
				MotionLog.Error($"Can not found node {nodeName}");
				return;
			}

			// 检测转换关系
			if (Graph != null)
			{
				if (Graph.CanTransition(_curNode.Name, node.Name) == false)
				{
					MotionLog.Error($"Can not transition {_curNode} to {node}");
					return;
				}
			}

			MotionLog.Log($"FSM transition {_curNode.Name} to {node.Name}");
			_preNode = _curNode;
			_curNode.OnExit();
			_curNode = node;
			_curNode.OnEnter();
		}

		/// <summary>
		/// 返回到之前的节点
		/// </summary>
		public void RevertToPreviousNode()
		{
			Transition(PreviousNodeName);
		}

		/// <summary>
		/// 接收消息
		/// </summary>
		public void HandleMessage(object msg)
		{
			if (_curNode != null)
				_curNode.OnHandleMessage(msg);
		}

		private bool IsContains(string nodeName)
		{
			for (int i = 0; i < _nodes.Count; i++)
			{
				if (_nodes[i].Name == nodeName)
					return true;
			}
			return false;
		}
		private IFsmNode GetNode(string nodeName)
		{
			for (int i = 0; i < _nodes.Count; i++)
			{
				if (_nodes[i].Name == nodeName)
					return _nodes[i];
			}
			return null;
		}
	}

这样驱动了状态机节点的相关接口的调用与执行。写好FiniteStateMachine, IFsmNode两个代码以后,状态机就已经设计完成了,接下来就是具体游戏项目中的使用。也就是与使用相关的代码了。其实非常简单,主要有3步:

Step1: 创建一个状态机对象;

Step2: 我们要添加一个状态机的逻辑节点,只要继承IFsmNode,实现相关接口,并把逻辑节点放到状态机对象中统一管理起来。

Step3: 根据业务逻辑来切换运行的状态机的节点。从而到达逻辑控制的目的。

 

3: 基于状态机扩展一些特殊的状态控制

 

状态机设计完成以后,我们还可以基于状态机来做一些特殊的状态控制,让我们的逻辑代码更清晰,维护起来更方便,比如最常见的顺序执行状态机ProcedureFsm。就是说执行完一个状态节点,马上执行第二个状态节点。这样我们做顺序流程就非常方便了,比如热更新的顺序流程状态机:

1: 检查版本状态节点;

2: 增量下载信息比对节点;

3: 增量下载资源节点;

4: 下载完成后进入游戏节点;

把这些状态机节点加入到ProcedureFsm中,那么它就会从第一个节点开始运行,后面每个节点依次执行。

 

项目中是否用状态机的方式来做为你的逻辑控制,这个可以根据具体的需求来进行分析。没有绝对的好与坏,适合即可。

 

今天的分享就到这里,关注我(加入到学习群),可以获取”Unity 状态机”相关源码与实现。

标签:架构,void,接口,curNode,状态机,Unity,IFsmNode,节点
From: https://www.cnblogs.com/bycw/p/17784823.html

相关文章

  • unity shader入门精要第六章原理总结
    前言开发中常常有一些画面表现上的需求,但苦于不会写Shader,没办法实现,现在特地来学习相关原理知识。UnityShader入门精要的第六章前的内容已经很详尽了,在此不做记录。但第六章中的基础光照模型比较常见,这部分内容在GAMES101中也有讲解,可见其重要性,在此记录一下。标准光照模型(Bli......
  • JAVA架构师具备的技术和能力
    JAVA架构师是一种高级职位,需要具备深厚的技术实力和广泛的能力。以下是JAVA架构师常见的技术和能力要求:1、扎实的编程基础:JAVA架构师需要具备良好的编程能力和深入理解JAVA语言特性和编程范式,熟悉面向对象编程和设计模式。2、深入理解框架和技术栈:JAVA架构师需要熟练掌握常用的JAVA......
  • 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(1)
    在我们的SqlSugar的开发框架中,整合了Winform端、Vue3+ElementPlus的前端、以及基于UniApp+Vue+ThorUI的移动前端几个前端处理,基本上覆盖了我们日常的应用模式了,本篇随笔进一步介绍前端应用的领域,研究集成WPF的应用端,循序渐进介绍基于CommunityToolkit.Mvvm和HandyControl的WPF应用......
  • 架构师必备的37项技能清单
    几年前,我被问到“你是如何变成一名架构师的?”。基于这个话题,我们讨论了很多,比如必要的技术、经验以及所需要的知识储备等。这一次讨论促使我开始思考要成为一名架构师应该具备和学习的东西有哪些,成为一个优秀的架构师应该具备哪些能力和做哪些事情。为此我查阅资料,走访各位大佬,......
  • Unity中国、Cocos为OpenHarmony游戏生态插上腾飞的翅膀
    Unity中国、Cocos为OpenHarmony游戏生态插上腾飞的翅膀2023年是OpenHarmony游戏生态百花齐放的一年!为了扩展OpenHarmony游戏生态,OpenHarmony在基金会成立了游戏SIG小组,游戏SIG小组联合cocos,从cocos2dx入手一周内快速适配了cocos2.2.6的MVP版本,随后又分别适配了cocos2dx 3.x、4.x版......
  • 云原生架构实战03 核心实战
    1、资源创建方式命令行YAML2.Namespace名称空间隔离资源kubectlcreatenshellokubectldeletenshelloapiVersion:v1kind:Namespacemetadata:name:hellokubectlgetnskubectlgetpods-Akubectldeletensmy-istio-ns3、Pod运行中的一组容器,Pod是kubernetes中应用......
  • Unity3D学习记录04——利用射线实现角色类似LOL的移动
    首先新建一个空白的GameObject,挂在一个MouseManager的脚本实现思路:通过获取鼠标点击的位置,获得该位置的信息,然后使角色移动到该位置MouseManager脚本的代码如下:1usingSystem.Collections;2usingSystem.Collections.Generic;3usingUnityEngine;4usingUnityEngi......
  • 这些文件都是7-Zip自解压缩文件(SFX)的默认模板。每个文件名后缀代表不同类型的操作系统
    这些文件都是7-Zip自解压缩文件(SFX)的默认模板。每个文件名后缀代表不同类型的操作系统和处理器架构。Default.SFX:这是用于32位Windows操作系统的默认SFX模板,其中包含了7-Zip解压程序。Default64.SFX:这是用于64位Windows操作系统的默认SFX模板,其中包含了7-Zip解压程序。WinCon.S......
  • Unity中国、Cocos为OpenHarmony游戏生态插上腾飞的翅膀
     2023年是OpenHarmony游戏生态百花齐放的一年!为了扩展OpenHarmony游戏生态,OpenHarmony在基金会成立了游戏SIG小组,游戏SIG小组联合cocos,从cocos2dx入手一周内快速适配了cocos2.2.6的MVP版本,随后又分别适配了cocos2dx 3.x、4.x版本以及cocos creator的2.4.12和3.18版本并在官......
  • Unity3D学习记录03——Navigation智能导航地图烘焙
    首先还是在PackageManager中安装AINavigation接着选择我们场景的地面,右键,找到AI的NavMeshSurface,它会为我们的Ground添加一个叫NavMeshSurface的子物体在Inspector窗口中可以看到它的详细的参数:图中的R,H为你人物的参数,45°为你的人物可以爬行的最大角度AgentType里面可......