狀態模式C#版
目錄
作為設計模式中的一種,狀態模式在軟體、遊戲設計中有很重要的作用。為了理解這種模式,本文首先用普通方式實現了一個例子,然後分別用列舉方式和狀態模式實現了這個例子,所有的程式碼經過測試可以在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;
}
}
}
問題
此時,如果需要增加一個蓄能動作,角色在俯臥時可以蓄能,蓄能滿時可以釋放一個特殊攻擊,這樣,我們需要增加另外一個欄位來儲存蓄能時間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;
}
}
}
這樣,又出現了普通方式一樣的問題,程式修改變得混亂不堪,糾其原因,角色狀態和角色本身耦合性太高,如果需要新增一個狀態,必然會使角色本身進行修改。於是,我們需要一種新的思維方式來構建我們的程式,這種思維方式應該可以解耦角色狀態和角色本身,可以讓角色狀態內部改變角色本身狀態,從而要新增一個角色狀態,只需要修改角色狀態,而不需要修改角色本身。
狀態模式
狀態模式被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);
}
}
這樣我們就實現了狀態模式。