【Unity设计模式】状态编程模式

在这里插入图片描述


前言

最近在学习Unity游戏设计模式,看到两本比较适合入门的书,一本是unity官方的 《Level up your programming with game programming patterns》 ,另一本是 《游戏编程模式》

这两本书介绍了大部分会使用到的设计模式,因此很值得学习

本专栏暂时先记录一些教程部分,深入阅读后续再更新


文章目录

  • 前言
  • 有限状态机
  • 如何实现状态模式


有限状态机

在游戏中有一个可玩的角色,通常情况他是站在地面上的,当控制控制器,他就会进入行走状态,当按下跳跃,则他会跳到半空,下落后又会回到站立状态

在这里插入图片描述

如果你做过动画机Animator,你一定能理解这个状态切换是如何实现的。上述的状态图类似于流程图,但是有一些区别:

  • 它由多种状态构成,且每个时间点只有一个状态是处于活动的
  • 每个状态可以根据运行条件转换为其他状态
  • 当发生状态转换时,原来的状态由活动切换为不活动,而转换的目标状态变为活动

我们将上图这样的状态模型称为有限状态机FSM,有限状态机在角色AI,程序设计尤其操作系统中十分常见。

有限状态机由数量有限的状态构成,它有一个初始状态,并包含了其他状态以及转换状态的条件。状态机在任意时刻都处于其中的某一状态,并且在一定条件下会从一种状态切换为另一种状态,以响应转换的外部输入。

状态模式不仅可以用于管理角色,道具的状态,甚至可以用于管理整个游戏系统的状态。包括Mono Behavior的生命周期,实际上也可视作一种状态模式


如何实现状态模式

状态模式看起来似乎很简单,我们只需要让对象进行状态判断,根据状态来选择行为就行了。

那我是不是可以定义一个枚举类型来分出状态,然后让角色根据他们所处的状态在内部进行行为切换就行了呢?

public enum EnemyState
{Idle,Walk,Jump
}public class Enemy : MonoBehaviour
{private EnemyState state;private void Update(){GetInput();switch (state){case EnemyState.Idle:Idle();break;case EnemyState.Walk:Walk();break;case EnemyState.Jump:Jump();break;}}
}

看起来实现了状态模式,但显然这种实现是依托答辩。

首先,难道我们每定义一个角色,就需要在其内部管理它自身的状态?
齐次,如果我们每添加一个状态,就需要一个Switch Case,那代码会有多冗余?
最后,上述代码显然是高耦合的,如果我们需要添加或者删去某状态,那么所有使用了该状态的代码都需要被修改。

因此,用枚举类型实现状态显然不合适,记住设计模式的重要原则,对拓展开放,对修改关闭

因此同理,让所有角色继承一个状态基类,在基类中定义各种状态实现的方法,并在子类中重写状态实现的虚方法也是不行的,因为基类一旦改变子类也要改变。

所以,我们需要在不修改角色代码的情况下,既要实现状态的拓展和删除,又要方便我们对每个状态的角色事件进行定义。一个想法就是让状态持有角色并在状态中完成业务处理逻辑,而非角色根据状态来实现业务逻辑。

这个想法很像我之前学习的一个案例(也许是工厂模式),银行有很多业务,但是如果每增加一个业务就需要修改银行类的代码,显然违背了开闭原则,因此银行应当只负责返回给用户相应的业务,而具体的业务逻辑则需要业务类本身来执行。就方便对银行业务进行增减。

因此角色的状态事件则需要由状态类本身来进行定义,好处是减少了耦合,代码也会更加清晰。但坏处是我们可能要为每个角色类定义多个衍生出来的状态类,类的数量会爆炸式的增长(此时用命名空间和程序集来管理多个相关类的好处就凸显出来了)

Unity 状态模式(实例详解)

// 定义抽象状态类
public abstract class CharacterState
{protected Character character;public void SetCharacter(Character _character){this.character = _character;}// 抽象方法,子类需要实现具体行为public abstract void Update();
}// 具体状态类:IdleState
public class IdleState : CharacterState
{public override void Update(){Debug.Log("角色处于闲置状态");// 检查是否应该转换到其他状态,如按下移动键则切换至MoveStateif (Input.GetKey(KeyCode.W)){character.ChangeState(new MoveState());}}
}// 具体状态类:MoveState
public class MoveState : CharacterState
{public override void Update(){Debug.Log("角色正在移动");// 检查是否应返回闲置状态或切换至其他状态if (!Input.GetKey(KeyCode.W)){character.ChangeState(new IdleState());}}
}------------------------------------------------------// 角色类持有当前状态并处理状态切换
public class Character : MonoBehaviour
{private CharacterState currentState;public void ChangeState(CharacterState newState){if (currentState != null){currentState.SetCharacter(null);}currentState = newState;currentState.SetCharacter(this);}void Update(){currentState.Update();}
}

在上述例子中,我们把状态的业务逻辑本身定义到了状态类中,并将对应的持有角色传入状态类,那么当角色进行状态改变时,则行为逻辑也就切换为对应状态类提供的Update方法。由状态类中对角色逻辑进行处理。

为了进一步解除角色类和状态类的耦合(角色未必需要有状态切换的需求),可以创建一个抽象的上下文类(Context),由它来持有当前状态并处理状态之间的切换:

管理StateSystem的文件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace StatePattern.StateSystem
{/// <summary>/// 状态抽象基类/// </summary>public abstract class State{public abstract void Handle();}/// <summary>/// 状态生命周期抽象基类/// </summary>public abstract class StateBehaviour:State{// 状态持有者protected ContextBehaviour Context;// 几个用于状态生命周期调度的抽象方法public abstract void Update();public abstract void Enter();public abstract void Exit();}/// <summary>/// 管理状态的上下文基类/// </summary>public class Context{// 当前状态private State _state;public void SetState<T>(T state) where T:State{_state = state;}public State GetState(){return _state;}public void Requst(){_state?.Handle();}}/// <summary>/// 上下文管理状态生命周期基类/// </summary>public class ContextBehaviour : Context{// 当前持有状态private StateBehaviour _stateBehaviour;// 覆盖父类的获取状态方法public new void SetState<T>(T state) where T:StateBehaviour{_stateBehaviour = state;}public new StateBehaviour GetState(){return _stateBehaviour;}// 几个用于状态生命周期调度的虚方法public virtual void ChangeState(StateBehaviour stateBehaviour){_stateBehaviour.Exit();SetState(stateBehaviour);_stateBehaviour.Enter();}public virtual void Update(){_stateBehaviour.Update();}public virtual void NotifyStateEnter(){_stateBehaviour.Enter();}public virtual void NotifyStateExit(){_stateBehaviour.Exit();}}}

角色基类定义代码:

using StatePattern.StateSystem;
using System;
using UnityEngine;
using UnityEngine.UI;namespace CharacterClass
{#region 基类定义/// <summary>/// 角色状态基类/// </summary>public abstract class CharacterState : StateBehaviour { }/// <summary>/// 角色状态上下文基类/// </summary>public class CharacterContext : ContextBehaviour { }/// <summary>/// 角色基类/// </summary>public class Character : MonoBehaviour{private CharacterContext _context;public CharacterContext Context => _context;public Button StateChangeBtn;private void Start(){var riginState = new IdleState();_context = new CharacterContext();_context.SetState(riginState);var newState = new MoveState();StateChangeBtn.onClick.AddListener(() => { ChangeState(newState); });}private void Update(){_context.Update();}public void ChangeState(CharacterState characterState){_context.ChangeState(characterState);}}#endregion/// <summary>/// 角色状态类IdleState/// </summary>public class IdleState : CharacterState{public override void Update(){Debug.Log("处于IdleState");}public override void Enter(){Debug.Log("进入IdleState");}public override void Exit(){Debug.Log("退出IdleState");}public override void Handle(){Debug.Log("IdleState下执行事件");}}/// <summary>/// 角色状态类MoveState/// </summary>public class MoveState : CharacterState{public override void Update(){Debug.Log("处于MoveState");}public override void Enter(){Debug.Log("进入MoveState");}public override void Exit(){Debug.Log("退出MoveState");}public override void Handle(){Debug.Log("MoveState下执行事件");}}}

我们把Character脚本挂载,然后传入Button用于手动切换状态

在这里插入图片描述

这样我们就实现状态模式了。上面的代码写的实在太漂亮了,我都忍不住想夸我自己

我们还有更丧心病狂的想法,如果我们需要管理的状态不是单个,而是一系列的状态,那么我们可能就需要维护一个状态队列或者状态栈,此时一个状态切换上下文已经不够我们用了,我们需要一个状态机!

	public class NullState : StateBehaviour{public override void Handle(){throw new System.NotImplementedException();}public override void Update(){throw new System.NotImplementedException();}public override void Enter(){throw new System.NotImplementedException();}public override void Exit(){throw new System.NotImplementedException();}}public class StateMachine{private ContextBehaviour _contextBehaviour;public ContextBehaviour ContextBehaviour => _contextBehaviour;private NullState _nullState = new NullState();private StateBehaviour _prevState= new NullState();public StateMachine (ContextBehaviour contextBehaviour){_contextBehaviour = contextBehaviour;}public StateMachine (ContextBehaviour contextBehaviour,StateBehaviour riginState){_contextBehaviour = contextBehaviour;_contextBehaviour.SetState(riginState);}private Queue<StateBehaviour> _stateQueue = new Queue<StateBehaviour>();public void StateEnQueue(StateBehaviour stateBehaviour){_stateQueue.Enqueue(stateBehaviour);}public StateBehaviour StateDeQueue(){if (_stateQueue.Count > 0){return _stateQueue.Dequeue();}else{return _nullState;}}public void Update(){_contextBehaviour.Update();}public void NextState(){_prevState = _contextBehaviour.GetState();_contextBehaviour.ChangeState(StateDeQueue());}public void PrevState(){_contextBehaviour.ChangeState(_prevState);}}

这样我们就不是让角色持有上下文,而是让角色持有状态机本身。

在某些需要的时候更新状态机就可以处理一系列状态。我们就可以对状态进行各种操作,例如回到上一个状态,例如在一个事件中根据我们的需要传入一系列状态,并按照我们的想法对状态机中的状态进行触发。甚至多个角色持有同个状态机,一个状态机持有多个状态的上下文等等奇思妙想。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/31204.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pta 实验九 类库和标准库 C++

编程题 7-1 对称排序 分数 14 全屏浏览 作者 李廷元 单位 中国民用航空飞行学院 你供职于由一群丑星作为台柱子的信天翁马戏团。你刚完成了一个程序编写&#xff0c;它按明星们姓名字符串的长度非降序&#xff08;即当前姓名的长度至少与前一个姓名长度一样&#xff09;顺…

豆瓣电影top250网页爬虫

设计思路 选择技术栈:确定使用Python及其相关库&#xff0c;如requests用于发送网络请求&#xff0c;获取网址&#xff0c;用re(正则表达式)或BeautifulSoup用于页面内容解析。设计流程:规划爬虫的基本流程&#xff0c;包括发起请求、接受响应、解析内容、存储数据等环节。模块…

performance_schema.events_statements_current

performance_schema.events_statements_current 是 MySQL 中 performance_schema 库中的一个表&#xff0c;它用于显示当前正在执行的 SQL 语句的性能事件。这个表提供了关于当前正在运行的 SQL 语句的实时信息&#xff0c;允许数据库管理员和开发者监控和分析 SQL 语句的性能。…

小程序中用font-spider压缩字体后,字体没效果(解决办法)

因为项目中需要引入外部字体&#xff0c;有两种方案&#xff0c; 第一是把字体下载到本地&#xff0c; 第二种是cdn请求服务器放字体的地址 但是小程序是有大小限制的&#xff0c;所以必须要压缩字体大小&#xff0c;这时候有些人就说了&#xff0c;那把字体放在服务器上&a…

【人工智能】—基于K-Means算法商场顾客聚类实战教程

在这篇博文之前一直是给大家做机器学习有监督学习教程&#xff0c;今天来一篇无监督学习教程。 K-Means算法是一种基于中心的聚类方法&#xff0c;它试图找到数据点的K个簇&#xff0c;使得簇内的数据点尽可能相似&#xff0c;而簇间的数据点尽可能不同。下面是K-Means算法的详…

Spring Boot集成tablesaw插件快速入门

1 什么是tablesaw&#xff1f; Tablesaw是一款Java的数据可视化库&#xff0c;主要包括两部分&#xff1a; 数据解析库&#xff0c;主要用于加载数据&#xff0c;对数据进行操作(转化&#xff0c;过滤&#xff0c;汇总等)&#xff0c;类比Python中的Pandas库&#xff1b; 数据…

未定义行为总结

解引用空指针&#xff0c;例如解引用空的this指针、解引用空的函数指针 下面的代码尽管能正常运行&#xff0c;但c->fun()等价于(*c).fun()&#xff0c;也就是对空指针解引用了 #include <iostream> struct C {void fun(){std::cout<<"fun"<<st…

苹果cms10影视网整站源码下载/苹果cms模板MXone Pro自适应影视电影网站模板

下载地址&#xff1a;苹果cms10影视网整站源码下载/苹果cms模板MXone Pro自适应影视电影网站模板 模板带有夜间模式、白天晚上自动切换&#xff0c;有观影记录、后台设置页。全新UI全新框架&#xff0c;加载响应速度更快&#xff0c;seo更好&#xff0c;去除多余页面优化代码。…

Ubuntu 22.04 下 CURL(C++) 实现分块上传/下载文件源码

为了帮助大家理解代码&#xff0c;先介绍文件上传/下载流程&#xff1a; 上传文件流程说明&#xff1a;首先向服务器 restful api 接口 /common发送 Post 请求 &#xff0c;服务器端返回 project guid。读取本地文件&#xff0c;按照给定 chunk_size&#xff08;例如 10240 by…

java中函数式编程apply的用法实例?

在Java中&#xff0c;函数式编程的概念主要体现在Lambda表达式、函数接口以及Stream API的使用上。"apply"这个术语通常与函数式接口中的抽象方法关联&#xff0c;比如Function<T, R>接口中的apply方法。这个方法接收一个类型为T的输入参数&#xff0c;并返回一…

从零开始搭建创业公司全新技术栈解决方案

从零开始搭建创业公司全新技术栈解决方案 关于猫头虎 大家好&#xff0c;我是猫头虎&#xff0c;别名猫头虎博主&#xff0c;擅长的技术领域包括云原生、前端、后端、运维和AI。我的博客主要分享技术教程、bug解决思路、开发工具教程、前沿科技资讯、产品评测图文、产品使用体…

Ollma本地大模型沉浸式翻译【403报错解决】

最终效果 通过Chrome的 沉浸式翻译 插件&#xff0c;用OpenAI通用接口调用本地的Ollma上的模型&#xff0c;实现本地的大模型翻译文献。 官方文档指导的Ollama的配置&#xff1a;一定要配置环境变量&#xff0c;否则会出现【403报错】

GoogLeNet(InceptionV3)模型算法

GoogLeNet 团队在给出了一些通用的网络设计准则&#xff0c;以期望在不提高网络参数 量的前提下提升网络的表达能力&#xff1a; 避免特征图 (feature map) 表达瓶颈&#xff1a;从理论上讲&#xff0c;尺寸 (seize) 才包含了相关结构等重要因素&#xff0c;维度(channel) 仅仅…

torch.optim 之 Algorithms (Implementation: for-loop, foreach, fused)

torch.optim的官方文档 官方文档中文版 一、Implementation torch.optim的官方文档在介绍一些optimizer Algorithms时提及它们的implementation共有如下三个类别&#xff1a;for-loop, foreach (multi-tensor), and fused。 Chat-GPT对这三个implementation的解释是&#xf…

账号和权限的管理

文章目录 管理用户账号和组账号用户账号的分类超级用户普通用户程序用户 UID&#xff08;用户id)和(组账号)GIDUID用户识别号GID组标识号 用户账号文件添加用户账号设置/更改用户口令 管理用户账号和组账号 用户账号的分类 超级用户 root 用户是 Linux 操作系统中默认的超级…

React state 更新时机以及强制更新

设置 state 只会为下一次渲染变更 state 的值 一个 state 变量的值永远不会在一次渲染的内部发生变化 React 会使 state 的值始终"固定"在一次渲染的各个事件处理函数内部 React 会等到事件处理函数中的所有代码都运行完毕再处理 state 更新 用 flushSync 可以同步更…

PostgreSQL查询用户

在 PostgreSQL 中&#xff0c;可以通过查询系统表来确定当前用户是否是超级管理员&#xff08;超级用户&#xff09;。具体来说&#xff0c;可以使用 pg_roles 系统表&#xff0c;该表包含数据库中所有角色的信息。 以下是查询当前用户是否是超级用户的 SQL 语句&#xff1a; …

第N5周:调用Gensim库训练Word2Vec模型

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制&#x1f680; 文章来源&#xff1a;K同学的学习圈子 目录 本周任务: 1.安装Gensim库 2.对原始语料分词 3.停用词 4.训练Woed2Vec模型 …

办展览如何盈利?论办展的商业模式

想要弄清楚办展览怎么赚钱这个问题&#xff0c;我可以来说说。 首先来说说展览收益的大头&#xff1a;门票收入。 这个其实是可以大致预测的。简单来说&#xff0c;就是用流量乘以到店率。 但别忘了&#xff0c;这背后得有合适的展览定位、方便的展览场地和合理的票价。 说…

封禁SSH登录的IP

在公网上开放SSHD服务时常被黑客扫描&#xff0c;可用以下方式封禁其IP&#xff1a; 1. 在/etc/hosts/deny中加&#xff1a;sshd: /etc/sshd.deny.hostguard echo "sshd: /etc/sshd.deny.hostguard" > /etc/hosts/deny 2. 然后将/var/log/secure中探测密码的远…