浅谈设计模式和其Unity中的应用:四、状态模式

目录

  • 什么是状态模式
  • 怎么实现状态模式(C#实现)
    • 实现方法一:简单易用
      • 步骤一:状态枚举
      • 步骤二:玩家控制器(核心代码)
      • 问题引出
    • 实现方法二:状态模式,有限状态机FSM
      • 关于有限状态机的知识
        • 什么是有限状态机?
        • 有限状态机有什么特点?
      • 注意
      • 步骤一:建立所有状态的接口
      • 步骤二
      • 步骤三:修改PlayerController代码
    • 作业

什么是状态模式

在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。

简单来说,就是一个对象会有很多种状态,每一种状态都会做不同地事情。
比如人,有走路、跑步、吃饭、睡觉等等状态,它们可以做的事都不一样。

更详细的介绍可以看看这个:状态模式

怎么实现状态模式(C#实现)

假设玩家有三种状态

  1. 站立:Stand
  2. 跳跃:Jump
  3. 下蹲:Crouch

实现方法一:简单易用

步骤一:状态枚举

我们可以把所有的状态用枚举来表示,新建一个脚本,叫做State,代码如下

public enum State
{
    StandState,
    JumpState,
    CrouchState,
}

步骤二:玩家控制器(核心代码)

新建一个PlayerController脚本,目的是根据玩家输入修改当前状态。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{

    private State currentState;//当前状态
    
    private void Awake()
    {
    	//初始为站立状态
        currentState = State.StandState;
    }
    
    private void Update()
    {
    	//监听输入
        HandleInput();
    }
    
    public void HandleInput()
	{
		switch (currentState)
        {
        	//玩家在站立状态可以-->跳跃、下蹲
            case State.StandState:
                if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    SetState(State.JumpState);
                }
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    SetState(State.CrouchState);
                }
                break;
            //玩家在跳跃状态可以-->站立
            case State.JumpState:
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    SetState(State.StandState);
                }
                break;
            //玩家在下蹲状态可以-->站立
            case State.CrouchState:
                if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    SetState(State.StandState);
                }
                break;
            default:
                break;
        }
	}
	//修改当前状态
    public void SetState(State targetState)
    {
        currentState = targetState;
    }
    
}

我们运行游戏,按下对应的键,就可以修改当前状态了。
这样我们就实现了一个简单的状态模式。

问题引出

  • 策划:程序你过来下,我有个好点子。
  • 程序:你说(程序Σ(っ °Д °;)っ)。
  • 策划:玩家就三种状态,玩家会不会觉得游戏太单调了?我想玩家会游泳不过分吧?会下潜不过分吧?玩家会飞行不过分吧?玩家在游泳的时候可以下潜,在跳跃的时候可以飞行,在飞行的时候可以变成站立状态或者游泳状态,就加三个状态多加几个条件判断嘛,我觉得应该很好实现,那么就交给你了哈。
  • 程序:啊对对对,就按照你的来(开始摆烂)。

哈哈开个玩笑,但是玩笑归玩笑,这种情况在现实生活中是很常见的。那假设我们就用的是上面的代码,现在我们来加需求。

  1. 首先:需要在枚举里面加三个状态:游泳:Swim,飞行:Fly,下潜:Dive
  2. 根据策划的要求,我们需要在跳跃状态的case里面加一个判断,如果玩家按下 ↑ 键,修改当前状态为飞行。
  3. 游泳状态的case里面加一个判断,如果玩家按下 ↓ 键,修改当前状态为下潜。
  4. 下潜状态的case里面加一个判断,如果玩家按下 ↑ 键,修改当前状态为游泳。
  5. 飞行状态的case里面加一个判断,如果玩家按下 下 键,如果下方是河,修改当前状态为游泳,如果下方是陆地,修改当前状态为站立。

最后代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum State
{
    StandState,
    JumpState,
    CrouchState,
    Swim,
    Fly,
    Dive,
}

public class PlayerController : MonoBehaviour
{

    private State currentState;
    
    private void Awake()
    {
        currentState = new StandState(this);
    }
    
    private void Update()
    {
        switch (state)
        {
            case State.StandState:
                if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    SetState(State.JumpState);
                }
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    SetState(State.CrouchState);
                }
                break;
            case State.JumpState:
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    SetState(State.StandState);
                }
                break;
            case State.CrouchState:
                if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    SetState(State.StandState);
                }
                break;
            case State.Swim:
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    SetState(State.Dive);
                }
                break;
            case State.Dive:
                if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    SetState(State.Swim);
                }
                break;
            case State.Fly:
                if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    if (下方是河)
                    {
                        SetState(State.Swim);
                    }
                    if (下方是陆地)
                    {
                        SetState(State.StandState);
                    }
                }
                break;
            default:
                break;
        }
    }
    
    public void SetState(State targetState)
    {
        currentState = targetState;
    }
    
}

你会发现,会导致很多问题

  1. 代码会显得臃肿。
  2. 耦合性特别高,不易维护。
  3. 不能一下子看出来当前状态可以转换为什么状态
  4. 最重要的是如果你需要加一个状态,要维护修改的地方也会太多。

我们可不可以这样呢,把每一个状态都设置为一个类,让每一个状态都做自己的事,状态自己决定可以切换到什么状态,而不让PlayerController去控制。
这样我们就解决了上述问题了,也就是这篇文章的重点。

实现方法二:状态模式,有限状态机FSM

关于有限状态机的知识

什么是有限状态机?

有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

有限状态机有什么特点?
  1. 可以用状态来描述事物,并且任一时刻,事物总是处于一种状态;
  2. 事物拥有的状态总数是有限的;
  3. 通过触发事物的某些行为,可以导致事物从一种状态过渡到另一种状态;
  4. 事物状态变化是有规则的,A状态可以变换到B,B可以变换到C,A却不一定能变换到C;
  5. 同一种行为,可以将事物从多种状态变成同种状态,但是不能从同种状态变成多种状态。

注意

本次实现的是较为简单的FSM,实际上常用的FSM比下面的要稍微复杂点,会多一个管理者,还有每一个状态的生命周期也会增加。
详细的FSM可以看看我的这篇博客:Unity学习笔记–FSM有限状态机的实现

步骤一:建立所有状态的接口

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IBaseState
{
    //处理玩家输入
    void HandleInput();
    //处理状态的更新:比如动画、移动
    void Update();
}

步骤二

新建多个脚本文件。

  • 站立:StandState
  • 跳跃:JumpState
  • 下蹲:CrouchState
    浅谈设计模式和其Unity中的应用:四、状态模式_第1张图片
  • StandState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StandState : IBaseState
{
    private PlayerController m_player;
    public StandState(PlayerController player) => m_player = player;
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            m_player.SetState(new JumpState(m_player));
        }
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            m_player.SetState(new CrouchState(m_player));
        }
    }

    public void Update()
    {
        //DoSomething
    }
}
  • JumpState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class JumpState : IBaseState
{
    private PlayerController m_player;
    public JumpState(PlayerController player) => m_player = player;
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            m_player.SetState(new StandState(m_player));
        }
    }
    void IBaseState.Update()
    {
        //DoSomething
    }
}
  • CrouchState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CrouchState : IBaseState
{
    private PlayerController m_player;
    public CrouchState(PlayerController player) => m_player = player;
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            m_player.SetState(new StandState(m_player));
        }
    }

    public void Update()
    {
        //DoSomething
    }
}

步骤三:修改PlayerController代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private IBaseState currentState;
    private void Awake()
    {
        currentState = new StandState(this);
    }
    private void Update()
    {
        currentState?.HandleInput();
        currentState?.Update();
    }
    public void SetState(IBaseState targetState)
    {
        currentState = targetState;
    }
}

如果我们需要添加多个状态和转换条件,只需要新建一个状态脚本,让这个状态去实现IBaseState这个接口。然后修改状态的HandleInput方法就可以了。

这样代码耦合性就大大降低了,代码易读性也更强了,维护的时候也会很简单。

作业

大家可以试着把之前问题引出:屑策划(bushi)的需求实现一下,来更直观的感受有限状态机的魅力。


要更努力地成为一名优秀的游戏开发者!

你可能感兴趣的:(设计模式,状态模式,unity,游戏引擎)