1. 程式人生 > >遊戲程式設計之命令模式

遊戲程式設計之命令模式

1、什麼是命令模式

最近看了《遊戲程式設計模式》這本書,裡面介紹了遊戲開發時常用的設計模式,當然這些設計模式不只是在開發遊戲時才管用,它們同樣適用於其他軟體開發,適用於各種語言。這裡我記錄一下自己的學習筆記以及結合unity的使用方法。命令模式是常用的設計模式之一,它的定義是這樣:將一個請求封裝為一個物件,從而使你可以用不同的請求對客戶進行引數化;對請求排隊或記錄請求日誌,以及支援可撤銷的操作。這個定義聽起來似乎晦澀難懂,下面用unity遊戲開發的例子來說明:  

2、對客戶進行引數化

比如在遊戲開發中,產品經理給你提了這樣一個需求:按下按鍵A,控制角色攻擊;按下按鍵B,控制角色奔跑;按下按鍵C,控制角色跳躍。面對這樣一個簡單的需求,我們或許會這樣寫:
void
HandleInput() { if (Input.GetKeyDown(KeyCode.A)) { Attack(); } else if (Input.GetKeyDown(KeyCode.B)) { Run(); } else if (Input.GetKeyDown(KeyCode.C)) { Jump(); } }
然後,產品經理又提了需求,使用者可以自定義按鍵功能,在很多遊戲中都有做這樣的功能,為了實現這樣的功能,我們應該將這些對Attack()和Run()的呼叫轉化成可以變換的東西,下面用命令模式來重寫一下這個功能: 先定義一個抽象類Command作為基類,再定義具體的子類來重寫Excute();

public abstract class Command{
    public abstract void Excute(GameActor actor);
}

public class AttackCommand : Command
{
    public override void Excute()
    {
        //攻擊邏輯
    }
}

public class RunCommand : Command
{
    public override void Excute()
    {
        //奔跑邏輯
    }
}

public class
JumpCommand : Command { public override void Excute() { //跳躍邏輯 } }
在MonoBehaviour的Update函式中,每幀去監聽使用者輸入,並返回對應的command

public class GameControl : MonoBehaviour
{
    private Command buttonA;
    private Command buttonB;
    private Command buttonC;

    private void Start()
    {
        buttonA = new AttackCommand();
        buttonB = new JumpCommand();
        buttonC = new RunCommand();
    }

    private void Update()
    {
        Command cmd = HandleInput();
        if (cmd != null)
        {
            cmd.Excute(actor);
        }
    }

    //處理使用者輸入
    private Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            return buttonA;
        }
        else if (Input.GetKeyDown(KeyCode.B))
        {
            return buttonB;
        }
        else if (Input.GetKeyDown(KeyCode.C))
        {
            return buttonC;
        }
        else
        {
            return null;
        }
    }

}

這樣,在按鍵觸發和函式呼叫中間就加了一層Command,如果要自定義按鍵功能,直接修改Button對應的Command就行了。現在我們也可以修改一下上面的程式碼,讓我們可以用這套機制去控制任意角色物件,只需將要控制的角色物件傳進來即可:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameActor { }

public class Actor1 : GameActor { }

public class Actor2 : GameActor { }

public abstract class Command{
    public abstract void Excute(GameActor actor);
}

public class AttackCommand : Command
{
    public override void Excute(GameActor actor)
    {
        //攻擊邏輯
    }
}

public class RunCommand : Command
{
    public override void Excute(GameActor actor)
    {
        //奔跑邏輯
    }
}

public class JumpCommand : Command
{
    public override void Excute(GameActor actor)
    {
        //跳躍邏輯
    }
}


public class GameControl : MonoBehaviour
{
    private Command buttonA;
    private Command buttonB;
    private Command buttonC;

    private GameActor actor;

    private void Start()
    {
        buttonA = new AttackCommand();
        buttonB = new JumpCommand();
        buttonC = new RunCommand();

        actor = new Actor1();
    }

    private void Update()
    {
        Command cmd = HandleInput();
        if (cmd != null)
        {
            cmd.Excute(actor);
        }
    }

    //處理使用者輸入
    private Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            return buttonA;
        }
        else if (Input.GetKeyDown(KeyCode.B))
        {
            return buttonB;
        }
        else if (Input.GetKeyDown(KeyCode.C))
        {
            return buttonC;
        }
        else
        {
            return null;
        }
    }

}

3、支援可撤銷的操作

命令模式在需要支援可撤銷操作的情況下也能輕鬆應對,假如我們需要給玩家提供撤銷移動操作的功能時,我們可以先把玩家輸入產生的command存入棧中(或者其他資料結構),在撤銷時,從棧中取出棧頂的Command,再呼叫該Command的Undo(),就實現了撤銷功能(Undo()為撤銷方法,與Excute()相反),程式碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameActor {
    public Transform selfTra;
    public void Move(Vector3 offset)
    {
        selfTra.Translate(offset);
    }
}

public class Actor1 : GameActor { }

public class Actor2 : GameActor { }

public abstract class Command{
    public abstract void Excute(GameActor actor);//執行
    public abstract void Undo(GameActor actor);//撤銷
}

public class MoveCommand : Command
{
    public Vector3 moveOffset;
    public MoveCommand(Vector3 offset)
    {
        moveOffset = offset;
    }

    public override void Excute(GameActor actor)
    {
        actor.Move(moveOffset);
    }

    public override void Undo(GameActor actor)
    {
        actor.Move(-moveOffset);
    }
}

public class CommandControl : MonoBehaviour
{
    private Command moveCommand;
    private GameActor actor;
    private Stack<Command> commandStack;

    private void Start()
    {
        moveCommand = new MoveCommand(Vector3.one);
        actor = new Actor1();
        commandStack = new Stack<Command>();
    }

    private void Update()
    {
        Command cmd = HandleInput();
        if (cmd != null)
        {
            commandStack.Push(cmd);
            cmd.Excute(actor);
        }
    }

    //需要撤銷操作時呼叫這個函式
    public void PlayReverse()
    {
        if (commandStack.Count > 0)
        {
            commandStack.Pop().Undo(actor);
        }
    }

    //處理使用者輸入
    public Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            return new MoveCommand(new Vector3(2, 4, 5));
        }
        if (Input.GetKeyDown(KeyCode.B))
        {
            return new MoveCommand(new Vector3(1, 2, 4));
        }
        else 
        {
            return null;
        }
    }

}
上面程式碼中, 每次產生一個command時就將它存到Stack中,當需要撤銷操作時,就取出Stack頂部的command,並執行它的Undo(),按照這種方法,可以實現多重撤銷。  

4、總結

通過上面的例子,我們再看命令模式的定義:將一個請求封裝為一個物件,從而使你可以用不同的請求對客戶進行引數化;對請求排隊或記錄請求日誌,以及支援可撤銷的操作。現在我們差不多明白了命令模式的用法,它優點很明顯,缺點也是有的:第一個優點是類間解耦,呼叫者和接收者之間沒有任何依賴關係,呼叫者在實現功能時只需呼叫Command抽象類的Excute方法即可,不需要關注是哪個接收者執行;第二個優點是可擴充套件性,Command的子類可以很容易地擴充套件;缺點是如果有大量命令,那麼Command的子類將會非常龐大。我們在實際開發中,應該發揮出命令模式的優點,並結合其他模式,減少Command子類龐大的問題。