美文网首页
命令模式(2)

命令模式(2)

作者: 芒果和小猫 | 来源:发表于2017-11-09 20:34 被阅读0次

    实现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的功能了!

    相关文章

      网友评论

          本文标题:命令模式(2)

          本文链接:https://www.haomeiwen.com/subject/obvmmxtx.html