Command Pattern 命令模式
Command Pattern is nothing more than a behavioral pattern that lets you record the actions or events of your game or application. It also allows you the ability to isolate your features into their classes. For example, instead of having a player class that checks for user input and then performs as an action, you would isolate the entire action into its own class, allowing u to keep track of the entire action and control it without any distraction. This would ultimately allow u to create a rewind system or cue system in your game.
When to use?
- Rewind system -----> record player’s actions
- Pros: greatly decouples your code. Your features are isolated and contained within themselves. They are not relied upon for your software to work. Essentially u can change one system in your project without affecting another.
- Cons: it adds a layer of complexity to your projects and additional class files.
- [应用场景] : 悔棋, 农药:对局回放
Setup and Implementation
Scene Setup
这块就是构建一下场景 几个方块和按钮 就不说了
Implement the command pattern
- command interface ----> Two methods: execute method & undue method
(接口相关知识看这里
我们就写一个包含这两个method的接口,第一个method是执行,第二个method是倒带(恢复到上一步的状态
public interface ICommand
{
void Execute();
void Undue();
}
- 我们希望点击方块它可以变色,我们专门写一个脚本(代码里面的UserClick脚本)来做这件事,因为是玩家本人做这件事嘛,那这个脚本就是附在Main Camera上的。这个代码要实现: press left key --> cast a ray —> detect a cube —> assign random color
public class UserClick : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
//press left key --> cast a ray ---> detect a cube ---> assign random color
if(Input.GetMouseButtonDown(0)){
Ray rayOrign = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.Raycast(rayOrign, out hitInfo)){
if(hitInfo.collider.tag == "Cube"){
hitInfo.collider.GetComponent<MeshRenderer>().material.color = new Color(Random.value, Random.value, Random.value);
}
}
}
}
}
上面这个脚本是和玩家有直接联系的。这时候我们想要把它转换成一个命令,也就是我们说想要以命令的方式执行的话要怎么做? 前面我们写了一个接口,我们现在写一个命令相关的脚本,这个脚本是继承这个接口的。在这个专门用于存放点击命令的脚本ClickCommand里,我们要强制实现execute method 和 undue method这两个方法。这两个方法分别做以下的事情:
- Execute(): change the color of the cube to a random color
- Undue(): take previous color
注意 这个时候,我们的命令脚本并没有继承MonoBehaviour,也就是说我们无法直接获取物体的信息, 那么我们就要创造一个构造函数来获取信息。
附上代码
///* ClickCommand.cs *///
public class ClickCommand : ICommand
{
private GameObject _cube;
private Color _color;
private Color _previousColor;
public ClickCommand(GameObject cube, Color color)
{
this._cube = cube;
this._color = color;
}
public void Execute()
{
// get the previous color before change
_previousColor = _cube.GetComponent<MeshRenderer>().material.color;
// change the color of cube to a random color
_cube.GetComponent<MeshRenderer>().material.color = _color;
}
public void Undue(){
// take previous color
_cube.GetComponent<MeshRenderer>().material.color = _previousColor;
}
}
///* UserClick.cs *///
public class UserClick : MonoBehaviour
{
// Update is called once per frame
void Update()
{
//press left key --> cast a ray ---> detect a cube ---> assign random color
if(Input.GetMouseButtonDown(0)){
Ray rayOrign = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.Raycast(rayOrign, out hitInfo)){
if(hitInfo.collider.tag == "Cube"){
//hitInfo.collider.GetComponent<MeshRenderer>().material.color = new Color(Random.value, Random.value, Random.value);
//execute the click command
ICommand click = new ClickCommand(hitInfo.collider.gameObject, new Color(Random.value, Random.value, Random.value));
click.Execute();
}
}
}
}
}
在这边呢,我们就可以很好的保存上一步的记录,如果愿意的话甚至可以保存多个颜色记录,弄一个list来存放previousColor (啊不要杠为什么不直接在玩家脚本那边存这些了要有多乱有多乱
Challenge: Command Manager
前面我们将转换颜色的操作转化成指令的形式,现在我们要做的是将这些指令记录存储起来。这个时候用到的是manager class啦。
这个CommandManager是单例,就不用多说了。我们看看这class要做什么吧。
- 首先要有个list来存放各种记录,这个List的类型是ICommand,因为我们各式各样的指令都是继承这个接口的,在这边我们只写了一个换颜色的指令,后面可能还会有各式各样的操作比如移位什么的,统一管理用父类准没错
- 然后是我们要写的几个函数,保存指令,执行一编过去的指令,倒着执行过去的指令,复原所有方块颜色以及清空记录(详细的要求写在代码注释里了) 另外,我们希望在执行过去的指令时,可以每次间隔一秒(毕竟一个for循环下来就直接显示最后结果了,这个时候就要利用协程进行挂起(是之前的知识哦
///* Command Manager *///
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
public class CommandManager : MonoBehaviour
{
private static CommandManager _instance;
public static CommandManager Instance{
get{
if(_instance == null){
Debug.LogError("Command Manager is Null");
}
return _instance;
}
}
// a list store the commands, we don't need to know the type of command
// but all our commands inherit the ICommand interface
private List<ICommand> _commandBuffer = new List<ICommand>();
private void Awake() {
_instance = this;
}
// create a method to "add" commands to the command buffer//
public void AddCommands(ICommand command){
_commandBuffer.Add(command);
}
// create a play routine triggered by a play method that's going to play back all the commands//
// 1 second delay //
public void Play(){
StartCoroutine(PlayRoutine());
}
IEnumerator PlayRoutine(){
Debug.Log("Running Play Routine...");
foreach(var command in _commandBuffer){
command.Execute();
yield return new WaitForSeconds(1);
}
Debug.Log("Finish!");
}
// create a rewsing routine triggered by a rewind method that's going to play in reverse//
// 1 second delay
public void Rewind(){
StartCoroutine(RewindRoutine());
}
IEnumerator RewindRoutine(){
Debug.Log("Running Rewind Routine...");
//List<ICommand> reverseCommand = _commandBuffer;
//reverseCommand.Reverse();
foreach(var command in Enumerable.Reverse(_commandBuffer)){
command.Undue();
yield return new WaitForSeconds(1);
}
Debug.Log("Finish!");
}
// Done ---> Finished with changing colors, turn them all white //
public void Done(){
var cubes = GameObject.FindGameObjectsWithTag("Cube");
foreach(var cube in cubes){
cube.GetComponent<MeshRenderer>().material.color = Color.white;
}
}
// Reset ---> Clear the command buffer //
public void Reset() {
_commandBuffer.Clear();
}
}
- 在代码这边有个地方注意以下哦,就是List的翻转,我开始是想的是另外声明一个空间用于存放翻转后的List(就是被我注释掉的部分),但是视频里他是这么做的,利用Linq,将这个List当作一个collection然后进行了翻转(
我也不知道哪个更省空间或者效率更高
Command Pattern Testing
这节没什么好讲的,复习一波给按钮添加函数的方法
Command Implementation - Practical
这节是一个应用场景,记录Player的移动记录。这边复习一下做Command Pattern的三要素:
- Behaviour Script: 这个脚本用于管理玩家的输入 , 附在玩家角色上
- Command Classes: 用于存放指令, left command, right command, up command, down command,这些指令类型都是继承一个指令接口。 We could create a generic ICommand for move that takes in parameters of a direction, however that’s no the true command pattern. The true command pattern is where you are essentially creating a packet for each command.
- Command Manager: 用于存放指令记录。(注意 manager class都是单例
这节是个练习 代码就不扔了
哦对,有个注意的是,之前我们进程挂起时长不都是用秒来记的嘛,但是在这边,我们物体移动是连续的位置变换,而且是输入键保持按下的状态移动(getkey),所以他存储的记录不是一个时间点的变换,而是一个按帧计算的过程。所以当我们想要倒带或者重放时,为了保证是和原来一样的丝滑,我们用的是WaitForEndOfFrame()
。
但测了一下感觉WaitForFixedUpdate()
也可以???