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


前言

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

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

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


文章目录


有限状态机

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

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

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

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

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

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


如何实现状态模式

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

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

csharp 复制代码
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 状态模式(实例详解)

csharp 复制代码
// 定义抽象状态类
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的文件

csharp 复制代码
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();
		}
	}
	
}

角色基类定义代码:

csharp 复制代码
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用于手动切换状态

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

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

csharp 复制代码
	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);
		}
	}
	

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

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

相关推荐
小白不太白95035 分钟前
设计模式之 观察者模式
观察者模式·设计模式
小白不太白9502 小时前
设计模式之 责任链模式
python·设计模式·责任链模式
吾与谁归in2 小时前
【C#设计模式(13)——代理模式(Proxy Pattern)】
设计模式·c#·代理模式
吾与谁归in2 小时前
【C#设计模式(14)——责任链模式( Chain-of-responsibility Pattern)】
设计模式·c#·责任链模式
闲人一枚(学习中)2 小时前
设计模式-创建型-原型模式
设计模式
Iced_Sheep3 小时前
干掉 if else 之策略模式
后端·设计模式
哪 吒10 小时前
最简单的设计模式,抽象工厂模式,是否属于过度设计?
设计模式·抽象工厂模式
Theodore_102210 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
向宇it13 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
转世成为计算机大神13 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式