前篇链接:Unity之C#学习笔记(17):对象池模式 Object Pooling
命令模式是一种行为型设计模式。通常,当对象A需要对象B做某件事时,是在A中直接调用B的函数实现,这个过程增加了A和B的耦合度。命令模式在这个过程中增加一个中间的抽象层,将A要求做某件事抽象封装为一个“命令”对象,A作为发出者只需发出命令,交由中间类处理,而无需知道具体接收者是谁,同理接收者也是接收来自中间类的命令,实现了命令发出者和接收者的解耦合。当然,代价就是增加了一个抽象层,使代码结构变得复杂。
命令模式有一个很实用的应用:实现撤销和重做功能。正常情况下,A调用B的函数是一个一次性的过程,执行完就结束了。而命令模式抽象出的命令作为一个独立的实体可以被记录在列表或栈中,进而通过对命令集合的追踪实现命令的撤销和重做。命令模式是一个非常重要的设计模式,一些框架会提供封装好的类(例如Qt就提供了QUndoCommand和QUndoStack)。有关命令模式更详细的概念,UML图和一般实现,可以参考这篇文章。
在游戏设计中,可以借助命令模式的特性实现需要记录玩家操作步骤,提供撤回和回放功能的场景,例如一些策略类游戏。下面就来实现这样的一个例子。
在场景中,我们放置了3个Cube,将其Tag标为新建的Cube标签。又添加了4个按钮:Play, Rewind, Done和Reset。我们要实现的功能是,当用鼠标点击Cube时,给Cube随机设置一种颜色,并记录每次设置。完成后,点击Done按钮,所有Cube变回白色。点击Play,开始重新播放所有步骤(隔一秒执行一项)。点击Rewind,逆向播放所有步骤。点击Reset按钮,清空记录的所有步骤。
鼠标点击变色用射线Raycast实现,就不展开说明了。新建UserClick脚本,挂载到Main Camera上,Update函数中:
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray rayOrigin = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if (Physics.Raycast(rayOrigin, out hitInfo))
{
if (hitInfo.collider.tag == "Cube")
{
hitInfo.collider.GetComponent<MeshRenderer>().material.color = new Color(Random.value, Random.value, Random.value);
}
}
}
}
实现命令模式,首先创建一个命令接口ICommand:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ICommand
{
void Execute();
void Undo();
}
各种命令都需要实现这个接口。本例中,我们要操作的是点击方块变色的命令,这个命令的构造函数接收一个GameObject和一个Color,同时有一个记录之前颜色的成员previousColor。Execute()时,先将当前的颜色记录到previousColor,再将颜色设置为newColor。Undo()时,将颜色设置为previousColor。
ClickCommand类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickCommand : ICommand
{
private GameObject _cube; // 只操作颜色,也可以直接保存MeshRenderer引用,消除每次GetComponent开销
private Color _newColor;
private Color _previousColor;
public ClickCommand(GameObject cube, Color color)
{
this._cube = cube;
this._newColor = color;
}
public void Execute()
{
_previousColor = _cube.GetComponent<MeshRenderer>().material.color;
_cube.GetComponent<MeshRenderer>().material.color = _newColor;
}
public void Undo()
{
_cube.GetComponent<MeshRenderer>().material.color = _previousColor;
}
}
在UserClick脚本中,我们只要创建一个ClickCommand对象,传入GameObject和Color,然后执行命令即可:
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray rayOrigin = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if (Physics.Raycast(rayOrigin, out hitInfo))
{
if (hitInfo.collider.tag == "Cube")
{
ICommand clickCommand = new ClickCommand(hitInfo.collider.gameObject, new Color(Random.value, Random.value, Random.value));
clickCommand.Execute();
}
}
}
}
现在我们已经可以创建命令对象,接下来要记录它们。创建CommandManager类:
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class CommandManager : MonoSingleton<CommandManager> // 使用单例模式
{
private List<ICommand> _commandBuffer = new List<ICommand>();
public void AddCommand(ICommand newCommand)
{
_commandBuffer.Add(newCommand);
}
// Play按钮回调函数
public void Play()
{
// 使用StartCoroutine实现隔一秒执行一项
StartCoroutine(ExecuteAll());
}
IEnumerator ExecuteAll()
{
foreach (var command in _commandBuffer)
{
command.Execute();
yield return new WaitForSeconds(1.0f);
}
Debug.Log("All commands executed");
}
// Rewind按钮回调函数
public void Rewind()
{
StartCoroutine(UndoAll());
}
IEnumerator UndoAll()
{
foreach (var command in Enumerable.Reverse(_commandBuffer)) // 使用Linq实现逆向遍历
{
command.Undo();
yield return new WaitForSeconds(1.0f);
}
Debug.Log("All commands reversed");
}
// Done按钮回调函数,所有Cube设为白色
public void Done()
{
var cubes = GameObject.FindGameObjectsWithTag("Cube");
foreach (var cube in cubes)
{
cube.GetComponent<MeshRenderer>().material.color = Color.white;
}
}
// Reset按钮回调函数,清空记录的指令
public void Clear()
{
_commandBuffer.Clear();
}
}
测试效果: