Unity中使用命令模式,实现撤销回放的操作

Posted 神码编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity中使用命令模式,实现撤销回放的操作相关的知识,希望对你有一定的参考价值。

关于命令模式,我这里就不再做过多详解了,网上随便一搜都会有很多的文章,所以我就直接进入今天的正题,在Unity中如何将命令模式的这种思想展现出来。

 

首先,我们的目的是要用户能够撤销他所做的任何操作,或者是在完成之后可以回放自己所做的每一步操作,这样的话根据万物皆对象的思想,将用户的每一步操作,比如“输入用户名”“点击地面移动”等,都抽象为一个个真实存在的对象,每个对象会记录每条命令所牵连的所有属性,那么在需要撤销或是回放时,根据这个对象的属性还原操作就可以了。

 

当然,更安全的方式并不是直接还原操作,而且根据对象记录的属性克隆出新的对象,因为如果当用户删掉了某一个物体,下一刻他要执行撤销时,会发现那个物体的对象已不存在,这样的话撤销操作就会出现找不到对象的问题,所以根据克隆对象的方式,当用户删掉物体再还原时,我们并非还原他原本的那个物体,而是根据操作记录里面记载的对象删除前的属性重新克隆一个物体。

 

好了,首先,我们先定义一个所有命令的基类,我们知道的是,每个命令有且只会有两个操作:执行命令或撤销命令,我们可以在基类中描述,但具体的实现细节交由派生类自己来处理,毕竟每种命令撤销的方式不一样。

 

 

public class BaseCommand : object

    //命令描述
    private string _commandDescribe;
    public string CommandDescribe
    
        set 
            _commandDescribe = value;
        
        get 
            return _commandDescribe;
        
    

    //执行命令
    public virtual void ExecuteCommand()
    
    

    //撤销命令
    public virtual void RevocationCommand()
    
    


然后我们针对不同的对象,定制各自的命令类,这里我暂时只针对UGUI的InputField和角色Player进行命令收集,通过命令收集者来监听对象的一举一动,记录每一条命令的属性,比如InputField的操作,我们需要的属性也就只有一个,那就是InputField的输入值,所以将之纳为命令的收集目标即可。

 

 

 

using UnityEngine.UI;
/// <summary>
/// InputField操作的命令
/// </summary>
public class InputFieldCommand : BaseCommand

    #region 命令操作、撤销所涉及到的属性
    //目标
    private InputField _commandTarget;
    //目标的值
    private string _commandValue;
    #endregion

    public InputFieldCommand(InputField commandTarget, string commandValue, string commandDescribe)
    
        _commandTarget = commandTarget;
        _commandValue = commandValue;
        CommandDescribe = commandDescribe;
    

    /// <summary>
    /// 执行命令
    /// </summary>
    public override void ExecuteCommand()
    
        base.ExecuteCommand();

        _commandTarget.text = _commandValue;
    

    /// <summary>
    /// 撤销命令
    /// </summary>
    public override void RevocationCommand()
    
        base.RevocationCommand();

        _commandTarget.text = _commandValue;
    

 

 

角色Player为了简便,我暂时只记录他的移动命令,也就是说只有一个移动位置的属性需要被命令收集者监测。

 

using UnityEngine;
/// <summary>
/// Player操作的命令
/// </summary>
public class PlayerCommand : BaseCommand

    #region 命令操作、撤销所涉及到的属性
    //目标
    private Player _commandTarget;
    //目标的位置
    private Vector3 _commandPosition;
    #endregion

    public PlayerCommand(Player commandTarget, Vector3 commandPosition, string commandDescribe)
    
        _commandTarget = commandTarget;
        _commandPosition = commandPosition;
        CommandDescribe = commandDescribe;
    

    /// <summary>
    /// 执行命令
    /// </summary>
    public override void ExecuteCommand()
    
        base.ExecuteCommand();

        _commandTarget.transform.position = _commandPosition;
    

    /// <summary>
    /// 撤销命令
    /// </summary>
    public override void RevocationCommand()
    
        base.RevocationCommand();

        _commandTarget.transform.position = _commandPosition;
    

 


然后是我们的命令收集者。

 

 

 

using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 命令收集者
/// </summary>
public class CommandManager 

    /// 命令集
	private List<BaseCommand> CommandSet;

    public CommandManager()
    
        CommandSet = new List<BaseCommand>();
    

    /// <summary>
    /// 执行新的命令
    /// </summary>
    public void ExecutiveCommand(BaseCommand command)
    
        CommandSet.Add(command);
        command.ExecuteCommand();

        Debug.Log("执行命令:" + command.CommandDescribe);
    

    /// <summary>
    /// 撤销上一个命令
    /// </summary>
    public void RevocationCommand()
    
        if (CommandSet.Count > 0)
        
            BaseCommand command = CommandSet[CommandSet.Count - 1];
            CommandSet.Remove(command);
            command.RevocationCommand();
            
            Debug.Log("撤销命令:" + command.CommandDescribe);
        
    

 

 

对了,还有我们的角色类。

 

 

using UnityEngine;

public class Player : MonoBehaviour 

    //角色移动事件
    public delegate void MoveDelegate(Vector3 pos);
    public event MoveDelegate MoveEvent;

    /// <summary>
    /// 角色移动
    /// </summary>
    public void Move(Vector3 pos)
    
        if (MoveEvent != null)
            MoveEvent(pos);
    

 

 

 

 

 

 

 

如此,我们的简易命令模式结构就完成了,我们新建一个脚本CommandModeTest,在其中输入如下代码:

 

 

using UnityEngine;
using UnityEngine.UI;

public class CommandModeTest : MonoBehaviour 

    public Camera MainCamera;
    public GameObject UIPanel;

    public InputField userName;
    public InputField passWord;
    public Player player;
    
    private CommandManager _commandManager;

    private void Awake()
	
        Init();
    

    private void Init()
    
        //定义一个命令收集者
        _commandManager = new CommandManager();

        //命令收集者:监听userName的值改变操作
        userName.onEndEdit.AddListener((string value) =>
        
            _commandManager.ExecutiveCommand(new InputFieldCommand(userName, value, "修改用户名输入框的值为 " + value));
        );

        //命令收集者:监听passWord的值改变操作
        passWord.onEndEdit.AddListener((string value) =>
        
            _commandManager.ExecutiveCommand(new InputFieldCommand(passWord, value, "修改密码输入框的值为 " + value));
        );

        //命令收集者:监听player的移动操作
        player.MoveEvent += (Vector3 pos) =>
        
            _commandManager.ExecutiveCommand(new PlayerCommand(player, pos, "角色移动到 " + pos));
        ;
    
:监听userName的值改变操作
        userName.onEndEdit.AddListener((string value) =>
        
            _commandManager.ExecutiveCommand(new InputFieldCommand(userName, value, "修改用户名输入框的值为 " + value));
        );

        //命令收集者:监听passWord的值改变操作
        passWord.onEndEdit.AddListener((string value) =>
        
            _commandManager.ExecutiveCommand(new InputFieldCommand(passWord, value, "修改密码输入框的值为 " + value));
        );

        //命令收集者:监听player的移动操作
        player.MoveEvent += (Vector3 pos) =>
        
            _commandManager.ExecutiveCommand(new PlayerCommand(player, pos, "角色移动到 " + pos));
        ;
    


定义一个命令收集者,并对场景中的两个InputField输入框和Player进行操作监听,当目标处在被监听中的事件发生时,记录此时需要记录的属性,并存入命令集。

 

 

然后我们做一个点击地面让角色移动的简易例子,以便于我们测试。

 

 

/// <summary>
    /// 点击地面移动角色
    /// </summary>
    private void PlayerMove()
    
        if (Input.GetMouseButtonDown(0))
        
            Ray ray = MainCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit) && hit.transform.name == "Root")
            
                player.Move(hit.point);
            
        
    


再加上一个快捷键撤销操作的回调,当按下空格键时,就撤销掉上一步操作。

 

 

 

    /// <summary>
    /// 撤销操作
    /// </summary>
    private void RevocationCommand()
    
        if (Input.GetKeyDown(KeyCode.Space))
        
            _commandManager.RevocationCommand();
        
    

 

 

 

 

 

好了,到这里就可以测试了,我们在场景中加上这些需要的东西,UI啊,Player什么的。

 

运行场景。

 

 

我们在用户名输入框或密码输入框多次输入值,每次输入之后让其失去焦点,以提交输入,因为我们这里的命令收集者只监听输入框的输入提交操作。

 

 

然后我们连续按空格键撤销操作。

 

 

 

 

我们点击地面,让Player移动到点击位置。

 

 

按空格键撤销移动。

 

 

综上所述,命令模式的本质其实就是记录每一步操作为一个可再次访问的对象,当然一般为了解耦合,标准命令模式会将命令的触发者和调用者都分开处理,也就是说一方面只管触发新的命令,另一方面只管将命令分发给他的调用者执行,实现一种高强度的松耦合形式。

 

好了,今天写得比较匆忙,内容也比较简单,快放端午了事情有点多,忙里偷闲写的,后面在完善吧。

 

[获取源码]

以上是关于Unity中使用命令模式,实现撤销回放的操作的主要内容,如果未能解决你的问题,请参考以下文章

Unity学习基于JSON的UI回放系统(倍速)

Unity学习基于JSON的UI回放系统(倍速)

命令模式

C#使用命令模式实现撤销和恢复功能

Unity3D3D 物体概念 ② ( 空物体概念 | 创建空物体 | 利用空物体管理多个子节点 | 世界坐标系和本地坐标系操作模式 | 切换坐标系操作模式 | 轴心和几何中心模式 )

python设计模式之命令模式