目录
例子
普通方式
实现
问题
解决方式
枚举方式
实现
问题
状态模式
状态接口
状态对象
游戏玩家
作为设计模式中的一种,状态模式在软件、游戏设计中有很重要的作用。为了理解这种模式,本文首先用普通方式实现了一个例子,然后分别用枚举方式和状态模式实现了这个例子,所有的代码经过测试可以在Unity(2017.2)中运行。
例子
游戏中有一个处于站立姿态的女英雄,按“↑”时会跳跃;按住“↓”时,会卧倒,松开时则会恢复站立;在跳跃姿态按“↓”时会下斩;
普通方式
实现
不考虑状态模式,我们分别实现这三个状态及其过渡。
using UnityEngine;
public class HeroBasic : MonoBehaviour
{
private void Update()
{
//站立到跳跃
if(Input.GetKeyDown(KeyCode.UpArrow))
{
print("jump");
}
//站立到卧倒
else if(Input.GetKeyDown(KeyCode.DownArrow))
{
print("duck");
}
//卧倒到站立
else if(Input.GetKeyUp(KeyCode.DownArrow))
{
print("stand");
}
}
}
问题
- 一直按“↑”,会不断跳跃,产生悬空效果;
- 在跳跃姿态下按“↓”,会立即卧倒,同时在卧倒状态下按“↑”,会立即跳跃;
- 无法做出下斩动作;
解决方式
引入两个字段分别标志是否跳跃和卧倒,程序看起来如下。
using UnityEngine;
public class HeroBasic : MonoBehaviour
{
private bool isJumping = false;
private bool isDucking = false;
private void Update()
{
//站立到跳跃
if(Input.GetKeyDown(KeyCode.UpArrow))
{
if (!isJumping && !isDucking)
{
print("jump");
isJumping = true;
}
}
else if(Input.GetKeyDown(KeyCode.DownArrow))
{
//站立到卧倒
if (!isJumping && !isDucking)
{
print("duck");
isDucking = true;
}
//下斩
if(isJumping)
{
isJumping=false;
print("dive");
}
}
//卧倒到站立
else if(Input.GetKeyUp(KeyCode.DownArrow))
{
if(isDucking)
{
print("stand");
isDucking = false;
}
}
}
}
每次添加一个新的状态过渡时,我们不得不添加一个新的字段,并且需要在源代码中各个位置进行修改;另外,逻辑判断复杂,因为isJumping和isDucking并不会同时为真。
枚举方式
实现
我们用枚举数组替代之前的布尔字段,从而得到有限状态机形式的实现。
using UnityEngine;
public enum HeroState
{
Stand,
Jump,
Duck,
Dive
}
public class HeroEnum : MonoBehaviour
{
private HeroState state;
private void Update()
{
switch(state)
{
case HeroState.Stand:
if(Input.GetKeyDown(KeyCode.UpArrow))
{
state = HeroState.Jump;
print("jump");
}
else if(Input.GetKeyDown(KeyCode.DownArrow))
{
state = HeroState.Duck;
print("duck");
}
break;
case HeroState.Jump:
if(Input.GetKeyDown(KeyCode.DownArrow))
{
state = HeroState.Dive;
print("dive");
}
break;
case HeroState.Duck:
if(Input.GetKeyUp(KeyCode.DownArrow))
{
state = HeroState.Stand;
print("stand");
}
break;
}
}
}
问题
此时,如果需要增加一个蓄能动作,角 {MOD}在俯卧时可以蓄能,蓄能满时可以释放一个特殊攻击,这样,我们需要增加另外一个字段来存储蓄能时间chargeTime。
using UnityEngine;
public enum HeroState
{
Stand,
Jump,
Duck,
Dive
}
public class HeroEnum : MonoBehaviour
{
private HeroState state;
private int chargeTime = 0;
public int MaxTime = 100;
private void Update()
{
switch (state)
{
case HeroState.Stand:
if (Input.GetKeyDown(KeyCode.UpArrow))
{
state = HeroState.Jump;
print("jump");
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
state = HeroState.Duck;
print("duck");
}
chargeTime = 0;
break;
case HeroState.Jump:
if (Input.GetKeyDown(KeyCode.DownArrow))
{
state = HeroState.Dive;
print("dive");
}
chargeTime = 0;
break;
case HeroState.Duck:
if (Input.GetKeyUp(KeyCode.DownArrow))
{
state = HeroState.Stand;
print("stand");
}
chargeTime ++;
if(chargeTime > MaxTime)
{
chargeTime = 0;
print("super");
}
break;
}
}
}
这样,又出现了普通方式一样的问题,程序修改变得混乱不堪,纠其原因,角 {MOD}状态和角 {MOD}本身耦合性太高,如果需要新增一个状态,必然会使角 {MOD}本身进行修改。于是,我们需要一种新的思维方式来构建我们的程序,这种思维方式应该可以解耦角 {MOD}状态和角 {MOD}本身,可以让角 {MOD}状态内部改变角 {MOD}本身状态,从而要新增一个角 {MOD}状态,只需要修改角 {MOD}状态,而不需要修改角 {MOD}本身。
状态模式
状态模式被GoF这样描述:
允许一个对象在其内部状态发生改变时改变自己的行为,看起来就像是修改了本身的类型
状态接口
为所有的状态定义一个接口,在该接口中定义了所有状态类必须实现的方法。
public interface IHeroState
{
void HandleInput(Hero hero);
}
状态对象
为所有状态定义一个具体的类,继承状态接口,并实现状态接口中的方法。
//站立状态
using UnityEngine;
using System.Threading;
public class IdleState : IHeroState
{
public void HandleInput(Hero hero)
{
if (Input.GetKeyDown(KeyCode.DownArrow))
{
hero.printMessage("duck");
hero.State = new DuckingState();
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
hero.printMessage("jump");
hero.State = new JumpingState();
Thread waitForSeconds = new Thread(WaitingSeconds);
waitForSeconds.Start(hero);
}
}
void WaitingSeconds(object hero)
{
Thread.Sleep(1000);
Hero heroo = (Hero)hero;
heroo.printMessage("stand");
heroo.State = this;
}
}
//下斩状态
public class DiveState : IHeroState
{
public void HandleInput(Hero hero)
{
}
}
//跳起状态
using UnityEngine;
using System.Threading;
public class JumpingState : IHeroState
{
public void HandleInput(Hero hero)
{
if(Input.GetKeyDown(KeyCode.DownArrow))
{
hero.printMessage("dive");
hero.State = new DiveState();
Thread waitForSeconds = new Thread(WaitingSeconds);
waitForSeconds.Start(hero);
}
}
void WaitingSeconds(object hero)
{
Thread.Sleep(1000);
Hero heroo = (Hero)hero;
heroo.printMessage("stand");
heroo.State = this;
}
}
//下蹲状态
public class DiveState : IHeroState
{
public void HandleInput(Hero hero)
{
}
}
游戏玩家
最后,在游戏玩家中定义一个状态变量,该状态变量不再是之前的枚举类型,而是状态接口类型。
using UnityEngine;
public class Hero : MonoBehaviour
{
public IHeroState State;
private void Start()
{
State = new IdleState();
}
private void Update()
{
HandleInput();
}
void HandleInput()
{
State.HandleInput(this);
}
public void printMessage(string s)
{
print(s);
}
}
这样我们就实现了状态模式。