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

news2024/12/23 7:07:48

在这里插入图片描述


前言

最近在学习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("角色处于闲置状态");
        // 检查是否应该转换到其他状态,如按下移动键则切换至MoveState
        if (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.coloradmin.cn/o/1843615.html

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

相关文章

PXE高效批量网络装机(补充) 实验部分

然后把防火墙、安全机制全都给关闭掉&#xff0c;不要让它们干扰后续的实验&#xff1a; 然后安装那几个需要用到的软件包&#xff1a; 如果重启了系统vsftpd是不能自动启动起来的&#xff0c;如果想让该服务每次开机都自动的启动起来&#xff0c;可以执行下图中的命令&#xf…

Python学习笔记15:进阶篇(四)文件的读写。

文件操作 学习编程操作中&#xff0c;我觉得文件操作是必不可少的一部分。不管是读书的时候学习的c&#xff0c;c&#xff0c;工作的前学的java&#xff0c;现在学的Python&#xff0c;没学过的php和go&#xff0c;都有文件操作的模块以及库的支持&#xff0c;重要性毫无疑问。…

eNSP学习——OSPF在帧中继网络中的配置

目录 主要命令 原理概述 实验目的 实验场景 实验拓扑 实验编址 实验步骤 1、基本配置 2、在帧中继上搭建OSPF网络 主要命令 //检查帧中继的虚电路状态 display fr pvc-info//检查帧中继的映射表 display fr map-info//手工指定OSPF邻居,采用单播方式发送报文 [R1]os…

这些帮助你成长的IOS应用,建议收藏

TrackIt TrackIt是一款功能丰富的任务清单、日程管理和习惯打卡应用&#xff0c;旨在帮助用户提高效率和专注力。通过这些功能&#xff0c;用户可以更好地规划时间和任务&#xff0c;从而实现个人目标和养成良好习惯。 在目标设定方面&#xff0c;SMART原则是一个常用的方法&a…

首个AI高考评测结果出炉,GPT-4o排名第二

近日&#xff0c;上海人工智能实验室利用其自主研发的“司南”评测体系OpenCompass&#xff0c;对国内外多个知名大模型进行了一场特殊的“高考”。这些来自阿里巴巴、智谱AI、Mistral等机构&#xff0c;以及OpenAI的GPT-4o等“考生”&#xff0c;接受了新课标I卷“语数外”的全…

wins系统资源监视器任务管理器运行监控CPU、内存、磁盘、网络运行状态

目录 1.Windows系统资源监视器的详细介绍2.通过任务管理器打开资源监视器3.任务管理中总体观察观察cpu、pid、应用程序、I/O次数或者说读写字节数 4.观察CPU观察cpu核心数&#xff0c;以及哪些占用cpu频率过高 5.观察内存观察各个应用占用的内存大小和对应线程 6.观察磁盘活动观…

hrome插件: JSONView 插件让你告别数据混乱!

在现代网页开发中&#xff0c;处理和查看JSON数据已经成为日常工作的一部分。对于开发者来说&#xff0c;如何快速、方便地查看和调试JSON数据显得尤为重要。正是在这样的背景下&#xff0c;JSONView插件应运而生&#xff0c;成为开发者们的得力助手&#xff0c;今天咱们来聊聊…

【机器学习】机器的登神长阶——AIGC

目录 什么是AIGC 普通用户接触AIGC网站推荐 通义千问 白马 普通用户如何用好AIGC 关键提示词的作用 AIGC的影响 就业市场&#xff1a; 教育领域&#xff1a; 创意产业&#xff1a; 经济活动&#xff1a; 社交媒体与信息传播&#xff1a; AIGC面临的挑战 什么是AIGC…

板凳-------unix 网络编程 卷1-1简介

unix网络编程进程通信 unpipc.h https://blog.csdn.net/u010527630/article/details/33814377?spm1001.2014.3001.5502 订阅专栏 1>解压源码unpv22e.tar.gz。 $tar zxvf unpv22e.tar.gz //这样源码就被解压到当前的目录下了 2>运行configure脚本&#xff0c;以生成正确…

基于51单片机的篮球计分器设计

一.硬件方案 本设计用由AT89C51编程控制LED七段数码管作显示的球赛计时计分系统。该系统具有赛程定时设置、赛程时间暂停、及时刷新甲乙双方的成绩等功能。 电路主要由STC89C52单片机最小系统数码管显示模块数码管驱动模块蜂鸣器模块按键模块&#xff1b; 二.设计功能 &…

Flow Matching For Generative Modeling

Flow Matching For Generative Modeling 一、基于流的&#xff08;Flow based&#xff09;生成模型 生成模型 我们先回顾一下所谓的生成任务&#xff0c;究竟是想要做什么事情。我们认为&#xff0c;世界上所有的图片&#xff0c;是符合某种分布 p d a t a ( x ) p_{data}(…

Serverless如何赋能餐饮行业数字化?乐凯撒思变之道

导语 | 在数字化浪潮席卷全球的今天&#xff0c;每一个行业都在经历着前所未有的变革。餐饮行业作为人们日常生活中不可或缺的一部分&#xff0c;更是面临着巨大的转型压力。如何完成数字化转型&#xff0c;打破传统经营模式的限制&#xff0c;成为摆在众多餐饮商家面前的一道难…

基于Docker搭建ELK(Elasticsearch、Logstash、Kibana)日志框架

一、引言 随着企业业务的不断增长&#xff0c;日志管理成为了系统运维中不可或缺的一部分。ELK&#xff08;Elasticsearch、Logstash、Kibana&#xff09;作为一套开源的日志管理系统&#xff0c;以其高效、灵活、可扩展的特性&#xff0c;成为了众多企业的首选。本文将详细介…

代码随想录刷题复习day01

day01 数组-二分查找 class Solution {public int search(int[] nums, int target) {// 左闭右闭int left 0;int right nums.length - 1;int mid 0;while (right > left) {mid left (right - left) / 2;if (nums[mid] > target)right mid - 1;else if (nums[mid]…

机器学习案例|使用机器学习轻松预测信用卡坏账风险,极大程度降低损失

01、案例说明 对于模型的参数&#xff0c;除了使用系统的设定值之外&#xff0c;可以进行再进一步的优化而得到更好的结果。RM提供了几种参数优化的方法&#xff0c;能够让整体模型的效率提高。而其使用的概念&#xff0c;仍然是使用计算机强大的计算能力&#xff0c;对于不同…

动态轮换代理在多账户管理中有何用处?

如果您要处理多个在线帐户&#xff0c;选择正确的代理类型对于实现流畅的性能至关重要。但最适合这项工作的代理类型是什么&#xff1f; 为了更好地管理不同平台上的多个账户并优化成本&#xff0c;动态住宅代理IP通常作用在此。 一、什么是轮换代理&#xff1f; 轮换代理充当…

SpringSecurity实战入门——认证

项目代码 gson/spring-security-demo 简介 Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比…

探索交互设计:五大关键维度全面剖析

交互式设计是用户体验&#xff08;UX&#xff09;设计的重要组成部分。在本文中&#xff0c;我将向大家解释什么是交互设计并简要描述交互设计师通常每天都做什么。 一、什么是交互设计 交互式设计用简单的术语来理解就是用户和产品之间的交互。在大多数情况下&#xff0c;当…

嵌入式Linux 中常见外设屏接口分析

今天将梳理下嵌入式外设屏幕接口相关的介绍,对于一个嵌入式驱动开发工程师,对屏幕都可能接触到一些相关的的调试,这里首先把基础相关的知识梳理。 1. 引言 在嵌入式开发过程中,使用到的液晶屏有非常多的种类,根据不同技术和特性分类,会接触到TN液晶屏,TN液晶屏 VA液晶屏…

JDBC(简介、入门与IDEA中导入MySQL的驱动)

&#xff08;建议学完 MySQL 的基础部分&#xff09; JDBC——简而言之&#xff1a;用 Java 语言操作数据库。 Java DataBase Connectivity&#xff08;Java 语言连接数据库&#xff09; 目录 一、引言 &#xff08;1&#xff09;基本介绍 &#xff08;2&#xff09;JDBC 简…