命令模式(2)

实现undo和redo-------基于Unity3D

命令模式(1)中,我们已经知道了什么是命令模式,一个命令即是一个对象。
撤销和重做是命令模式成名之作, 利用撤销,我们可以回滚一些不满意的操作。例如在策略游戏中,我们常常需要排兵布阵,布置自己的战术,往往在没有确定之前,对某个或某些操作不是很满意,进而希望撤销之前的操作,或者自己的误操作撤销了某些步骤,希望重做。在策略游戏中,我们更希望玩家的注意力集中在策略上,而不是因为误操作而无法回滚。

如果我们不使用命令模式,我们将很难实现撤销和重做的功能,但事实上,我们利用命令模式就可以轻而易举的实现这个功能。还是很之前一样,我将基于Unity3D来实现这些功能。

为了简单起见,我将完成一个最简单的undo和redo的功能。这个可以undo和redo的命令是:移动玩家一个单位。

这个命令和之前的命令有所区别,最本质的区别在于之前的命令只要创建一次,例如Jump功能,默认会绑定给K,在之后如果不进行修改的话,这个命令始终会保持只有一个实例。而之上我们描述的命令更加具体,这也就意味着每次玩家选择一个动作,输入处理程序都要创建一个新的命令实例。

既然我们希望撤销和重做,那么毫无疑问的是我们必须保存下来这些命令,如果不保存,那么当我们需要撤销或重做的时候,去哪里找这些已经执行了的命令呢?

我们再来仔细想想,通常情况下,我们后执行的命令会被先撤销。后撤销的命令会被先执行。后……先……,没错stack,我们可以利用栈这一数据结构来保存我们使用的命令。具体来说我是这样设计的:

  • commands栈,当每执行一个命令的时候(在这里我们执行的对象移动的操作),将这个命令压入到commands这个栈中去。
  • redoCommands栈,当我们每撤销一个命令的时候,我们将这个撤销的命令压入到 redoCommands这个栈中去,当我们需要重做时,将命令弹出。注意此时的命令相当于是又执行了命令,于是我们再次将这个命令压入到commands这个栈中去。当有新的命令产生的时候,将清空redoCommands这个栈


    new-undo.jpg

    redo.jpg

接下来我会对命令模式(1)中的代码进行进一步的修改,来达到undo和redo的效果。

1. 创建一个move的命令类:

x_,y_代表此命令,希望传递进来的游戏对象移动到的位置坐标xBefore_,yBefore_代表游戏对象在执行这个命令之前的坐标execute和undo分别会调用actor的方法,让其移动。

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

public class MoveCommand : Command {
    private int x_, y_;
    private int xBefore_, yBefore_;
    public MoveCommand(ActorAction actor, int x,int y)
    {
        x_ = x;
        y_ = y;
        xBefore_ = actor.getX();
        yBefore_ = actor.getY();
    }

    public override void execute(ref ActorAction actor)
    {
        actor.moveTo(x_,y_);
    }
    public override void undo(ref ActorAction actor)
    {
        actor.moveTo(xBefore_, yBefore_);
    }
}

2. ActorAction中增加移动的方法

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

public class ActorAction : MonoBehaviour {
    private int x_, y_;
    private Transform actorTrans_;

    void Start()
    {
        actorTrans_ = this.gameObject.GetComponent<Transform>();
    }

    public int getX()
    {
        return x_;
    }
    public int getY()
    {
        return y_;
    }

    public void moveTo(int x,int y)
    {
        x_ = x;
        y_ = y;
        actorTrans_.position = new Vector3(x, y, actorTrans_.position.z);
    }

    public void attack()
    {
        Debug.Log("attack");
    }
    public void jump()
    {
        Debug.Log("jump");
    }
    public void avoid()
    {
        Debug.Log("avoid");
    }

}

3. 在InputHandler实现上述的两个栈

  • 在InputHandler类中添加两个新成员
    private Stack<Command> commands;
    private Stack<Command> redoCommands;
    在开始的时候初始化它们(在Start函数中进行初始化)
    commands = new Stack<Command>();
    redoCommands = new Stack<Command>();

  • 将handleInput这个函数做出修改
    当前我们可以撤销的操作是向上下左右四个方向进行移动。当接到新的命令时,我们将清空redo栈,即来了新的命令以后,就不可以重做了。接着我们根据输入创建新的移动命令,并压入到commands栈中,返回这个命令。

  • 实现undo方法
    当commands里不为空的时候,我们将当前的命令压入要redoCommands栈中,然后弹出这个命令并执行

  • 实现redo方法
    当redoCommands不为空时,同样将其压入commands中,然后弹出并执行

  • 总而言之
    如果要是在撤销和重做之间来回切换的话,执行的操作也就是在commands和redoCommands这两个栈之间进行pop和push操作。

以下是完整的代码

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

public class InputHandler :MonoBehaviour{

    private Command buttonJ_;
    private Command buttonK_;
    private Command buttonL_;
    private Command buttonUp_;
    private Command buttonDown_;
    private Command buttonLeft_;
    private Command buttonRight_;
    private ActorAction actor_;
    private Stack<Command> commands;
    private Stack<Command> redoCommands;
   void Start()
    {
        actor_ = this.gameObject.GetComponent<ActorAction>();
        commands = new Stack<Command>();
        redoCommands = new Stack<Command>();
        buttonJ_ = new AttackCommand();
        if (buttonJ_ == null) Debug.Log("buttonJ_ is null!");

        buttonK_ = new JumpCommand();
        if (buttonK_ == null) Debug.Log("buttonK_ is null!");

        buttonL_ = new AvoidCommand();
        if (buttonL_ == null) Debug.Log("buttonL_ is null!");

    }

    public void bindCommand(string buttonName,Command command)
    {
        switch (buttonName)
        {
            case "J":
            case "j": buttonJ_ = command; break;
            case "K":
            case "k": buttonK_ = command; break;
            case "L":
            case "l": buttonL_ = command; break;
        }
    }

    public Command handleInput(string keyName)
    {
        switch (keyName)
        {
            case "J":
                return buttonJ_;
            case "K":
                return buttonK_;
            case "L":
                return buttonL_;
            case "Up":
                redoCommands.Clear();
                buttonUp_ = new MoveCommand(actor_, actor_.getX(), actor_.getY() + 1);
                if (buttonUp_ == null) Debug.Log("buttonUp_ is null!");
                commands.Push(buttonUp_);
                return buttonUp_;
            case "Down":
                redoCommands.Clear();
                buttonDown_ = new MoveCommand(actor_, actor_.getX(), actor_.getY() - 1);
                if (buttonDown_ == null) Debug.Log("buttonDown_ is null!");
                commands.Push(buttonDown_);
                return buttonDown_;
            case "Left":
                redoCommands.Clear();
                buttonLeft_ = new MoveCommand(actor_, actor_.getX() - 1, actor_.getY());
                if (buttonLeft_ == null) Debug.Log("buttonLeft_ is null!");
                commands.Push(buttonLeft_);
                return buttonLeft_;
            case "Right":
                redoCommands.Clear();
                buttonRight_ = new MoveCommand(actor_, actor_.getX() + 1, actor_.getY());
                if (buttonRight_ == null) Debug.Log("buttonRight_ is null!");
                commands.Push(buttonRight_);
                return buttonRight_;
            default:
                return null;
        }
    }

    public void undo()
    {
        if(commands.Count!=0)
        {
            redoCommands.Push(commands.Peek());
            commands.Pop().undo(ref actor_);
        } 
    }
    public void redo()
    {
        if (redoCommands.Count != 0)
        {
            commands.Push(redoCommands.Peek());
            redoCommands.Pop().execute(ref actor_);
        }
    }
}

4. 修改判断输入的主逻辑

在判断输入的地方加上Z和X对应的功能,我定义Z为undoX为redo。

else if (Input.GetKeyDown(KeyCode.Z))
            inputHandler.undo();
else if (Input.GetKeyDown(KeyCode.X))
            inputHandler.redo();

至此我们已经完成了基本的undo和redo的功能了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容