1. 程式人生 > >狀態模式C#版

狀態模式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");
        }
    }
}

問題

  1. 一直按“↑”,會不斷跳躍,產生懸空效果;
  2. 在跳躍姿態下按“↓”,會立即臥倒,同時在臥倒狀態下按“↑”,會立即跳躍;
  3. 無法做出下斬動作;

解決方式

引入兩個欄位分別標誌是否跳躍和臥倒,程式看起來如下。

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);
    }
}

 這樣我們就實現了狀態模式。