Unity精华☀️ 哥哥,「设计模式」能解决游戏回放呀,你尝一口!
Posted 橙子SKODE
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity精华☀️ 哥哥,「设计模式」能解决游戏回放呀,你尝一口!相关的知识,希望对你有一定的参考价值。
哈喽大家好,你的橙哥突然出现~
本系列博客地址:传送门
人生不如意十之八九,这个面试啊,免不了会遇到些坎坷,比如说面试官问:
除了这些,还有吗?
那上节我们说了单例模式、观察者模式、代理模式
所以今天呢,橙哥再来和大家好好说道说道:工厂方法、迭代器模式、命令模式。
最后的命令模式,特别适合做回放,回放有Gif演示。
* 工厂模式
定义:工厂模式专门负责将大量有共同接口的类实例化。工厂模式可以动态决定实例化哪一个类,而不必实现知道要实例化的是哪一个类。
工厂模式是一个设计模式吗?
不是的,工厂模式分为三种,23种设计模式中,工厂模式就占了两种 ↓
在这个工厂模式家族中有3种形态:
- 简单工厂模式,这是他的中文名,英文名叫做Simple Factory。(它不属于23种设计方式之一)
- 工厂方法模式,这是他的中文名,英文名叫做Factory Method。
- 抽象工厂模式,这是他的中文名,英文名叫做Abstract Factory。
* 23种设计模式
设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式,共七种:适配器模式、装饰者模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
* 简单工厂模式
注意了啊,该模式不属于23种设计模式之一,面试时就不用说了,
但可以在Unity中使用。
简单工厂模式组成:
1)工厂类:工厂类在客户端的直接控制下(Create方法)创建产品对象。
2)抽象产品:是具体产品们的父类,或者是它们共同都继承的接口。抽象产品可以是一个普通类、抽象类(传送门:Abstract)或接口。
3)具体产品:实现抽象产品,定义工厂具体加工出的对象。
接口和抽象类的区别:
一个类可以继承很多个接口,但只能继承一个抽象类
由小老弟就问了,简单工厂模式怎样使用呢?
即先写抽象产品,把产品共同的内容写在一个脚本上
再写具体产品,继承抽象产品,接着写其它代码。因为继承了抽象产品,这样能少些很多代码。
最后写工厂类,供程序调用。输入不同的条件,工厂去调用不同的具体产品,得到不同的产品。
示例:
1、抽象产品:Config
public interface Config
{
/// <summary>
/// 芯片
/// </summary>
void Chip();
}
2、具体产品:IPhone
using UnityEngine;
//苹果手机
public class IPhone : Config
{
public void Chip()
{
Debug.Log("使用A14芯片");
}
}
3、具体产品:XiaoMi
using UnityEngine;
//小米手机
public class XiaoMi : Config
{
public virtual void Chip()
{
Debug.Log("使用高通芯片");
}
}
4、工厂类:ConcreteProduct
public class ConcreteProduct
{
//生产工厂,供外部调用
public static Config Create(int id)
{
switch (id)
{
case 1:
return new XiaoMi();
case 2:
return new IPhone();
}
return null;
}
}
一、工厂方法
工厂方法与简单工厂的区别在于:
工厂方法将工厂类进行了抽象,将实现逻辑延迟到工厂的子类。
不同的产品对应单独的工厂。
下图左图为简单工厂,右图为工厂方法:
书写方法:
先写抽象产品,把产品共同的内容写在一个脚本上
再写具体产品,继承抽象产品,接着写其它代码。因为继承了抽象产品,这样能少些很多代码。
最后写工厂类。与简单工厂模式不同的是,现在工厂类分成了 “抽象工厂脚本”、“具象工厂脚本”。
那现在该怎样使用呢?
现在我们使用该工厂模式的方法是,是直接调用需要的 “具象工厂” 方法,
而不是像简单工厂模式一样,输入条件,得到想要的内容。
下方展示工厂脚本改变的内容,其他脚本跟简单工厂模式相同。
1、抽象工厂
public interface IFactory
{
/// <summary>
/// 得到芯片
/// </summary>
IConfig Create();
}
2、具象工厂:IPhoneFactory
using UnityEngine;
public class IPhoneFactory : IFactory
{
public IConfig Create()
{
Debug.Log("这个工厂生产了 IPhoneAllConfig 配置的苹果手机");
return new IPhoneAllConfig();
}
}
3、具象工厂:XiaoMiFactory
using UnityEngine;
public class XiaoMiFactory : IFactory
{
public IConfig Create()
{
Debug.Log("这个工厂生产了 XiaoMiAllConfig 配置的小米手机");
return new XiaoMiAllConfig();
}
}
二、迭代器模式
迭代器模式: 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
Unity中实现迭代器模式的API是 foreach。
但是,foreach可能不包含我们想要的功能,
下面,我们就来自己实现一个通用的迭代器。
使用方法是:
1、首先自己的迭代器继承基础脚本的类:IEnumerable,可覆写里面的方法。
2、接着就可以使用啦!
基础类1:Iterator
using System.Collections.Generic;
public class Iterator : IteratorBase
{
private IList<object> items;
public int Count => items.Count;
public Iterator(IList<object> tempItems)
{
items = tempItems;
}
private int index = -1;
public object Current => items[index];
public bool MoveNext()
{
return items.Count > ++index;
}
public void Reset()
{
index = -1;
}
}
基础类2:IEnumerable
using System.Collections.Generic;
public interface IteratorBase
{
object Current { get; }
int Count { get; }
bool MoveNext();
/// <summary>
/// 将当前指针移动到第一位
/// </summary>
void Reset();
}
public class IEnumerable
{
private IList<object> items = new List<object>();
public virtual int Count => items.Count;
public virtual object this[int index]
{
get { return items[index]; }
set { items.Insert(index, value); }
}
public virtual IteratorBase GetIterator()
{
return new Iterator(items);
}
}
迭代器示例:Group
继承IEnumerable就好,Group便已实现了迭代器模式
你可以重写、拓展你的迭代器,实现想要的功能
using UnityEngine;
public class Group : IEnumerable
{
public override IteratorBase GetIterator()
{
Debug.Log("你可以重写你的迭代器");
return base.GetIterator();
}
}
下面是最后一步,有的同学别睡觉,敲黑板
使用示例:Test
using UnityEngine;
public class Test : MonoBehaviour
{
private void Start()
{
Group myGroup = new Group();
myGroup[0] = "s";
myGroup[0] = "k";
myGroup[0] = "o";
myGroup[0] = "d";
myGroup[0] = "e";
print(myGroup.Count);
IteratorBase iterator = myGroup.GetIterator();
print(iterator.Count);
while (iterator.MoveNext())
{
print("当前元素是:" + iterator.Current);
}
}
}
三、命令模式
命令模式是游戏中很有用的设计模式,书中有一句话是这样说的:
Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations.
—《Design Patterns: Elements of Reusable Object-Oriented Software》
意思是:命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。
适用于:
- Unity画画游戏的撤销、重做
- 小时候推箱子游戏的撤销操作、
- 五子棋的悔棋操作...
这个模式的特点是:
- 提供撤销操作(或者还有重做)
- 将输入命令封装成对象(方法):即从Update里面检测,拿到了一个方法里面,在Update里调用。
效果演示
点击录制后,我用的WASD操作cube移动
点击回放后,cube自动运动,演示回放。
下面我们来看一下示例脚本有哪些:
1、基础接口:command
/// <summary>
/// 供其他物体继承,实现不同功能的执行、撤销、重做功能
/// </summary>
public class command
{
protected float _time;
/// <summary>
/// 录制用到了时间。
/// 那些PS的撤销操作、推箱子的撤销操作等,就不需要时间了
/// </summary>
public float time => _time;
public virtual void Execute(BoxEntity avator)
{
}
public virtual void Undo(BoxEntity avator)
{
}
public virtual void Redo(BoxEntity avator)
{
}
}
2、盒子执行的命令:BoxCommand
继承了command,并进行了重写。
在后续工程中,我们可能不仅盒子的录制要用命令模式,同一个工程还有画画模块,那画画模块也继承command
这样我们就可以通过统一的接口command,去调用任意实现了command的盒子录制、画画撤销了
using UnityEngine;
public class BoxCommand : command
{
Vector3 _trans;
public BoxCommand(Vector3 m, float t)
{
_trans = m;
_time = t;
}
public override void Execute(BoxEntity avator)
{
avator.move(_trans);
}
public override void Undo(BoxEntity avator)
{
avator.move(-_trans);
}
}
3、要控制撤销重做的物体:BoxEntity
我们有了命令,也要有命令要控制的对象。
现在就把BoxEntity挂载到要控制的对象身上,并且根据需要,该脚本中有移动、或者隐藏显示、颜色变化等等的实际状态命令。
这些命令供BoxCommand去调用。
using UnityEngine;
/// <summary>
/// 挂载到实体身上,控制实体的运动
/// </summary>
public class BoxEntity : MonoBehaviour
{
Transform _transform;
void Start()
{
_transform = transform;
}
public void move(Vector3 T)
{
_transform.Translate(T);
}
}
4、BoxTest
该脚本封装了输入命令,并在Update实时检测;
有栈函数,执行了操作后就存上;
有开始记录、开始演示回放的方法,供程序调用。
using System;
using UnityEngine;
using System.Collections.Generic;
public class BoxTest : MonoBehaviour
{
//待操作对象
public BoxEntity boxEntity;
//保存的操作序列
//这儿如果增为两个栈:撤销栈与重做栈,那么便可在撤销时入重做栈,重做时入撤销栈。完成类似PS的操作。
Stack<command> commandStack = new Stack<command>();
//当前记录的时间节点
float recordTime;
//当前操作模式:无操作、录制、回放
private RecoderState recoderState = RecoderState.None;
void Update()
{
switch (recoderState)
{
case RecoderState.None:
break;
case RecoderState.Record:
Record();
break;
case RecoderState.PlayBack:
PlayBack();
break;
}
}
private enum RecoderState
{
None,
Record,
PlayBack
}
/// <summary>
/// 切换到回放模式,挂载到Button上
/// </summary>
public void callBack()
{
recoderState = RecoderState.PlayBack;
}
/// <summary>
/// 切换到记录模式,挂载到Button上
/// </summary>
public void run()
{
recoderState = RecoderState.Record;
}
/// <summary>
/// 控制对象运行,记录命令
/// </summary>
void Record()
{
recordTime += Time.deltaTime;
//得到当前帧是否操作了命令
command cmd = InputHandler();
if (cmd != null)
{
//记录当前执行的命令
commandStack.Push(cmd);
//去执行
cmd.Execute(boxEntity);
}
}
/// <summary>
/// 回放操作
/// </summary>
void PlayBack()
{
recordTime -= Time.deltaTime;
//返回在堆栈顶部的物体。(不移除)
if (commandStack.Count > 0 && recordTime < commandStack.Peek().time)
{
commandStack.Pop().Undo(boxEntity);
}
}
/// <summary>
/// 根据输入获取操作命令
/// </summary>
command InputHandler()
{
if (Input.GetKey(KeyCode.W))
return new BoxCommand(new Vector3(0, 0.1f, 0), recordTime);
if (Input.GetKey(KeyCode.S))
return new BoxCommand(new Vector3(0, -0.1f, 0), recordTime);
if (Input.GetKey(KeyCode.A))
return new BoxCommand(new Vector3(-0.1f, 0, 0), recordTime);
if (Input.GetKey(KeyCode.D))
return new BoxCommand(new Vector3(0.1f, 0, 0), recordTime);
return null;
}
}
甭管你现在有没有跳槽升职的想法,赶紧先备着,
面试前天背一背,对吧?
好了,今天的分享就到这儿了,按照国际惯例,一键三连走一波~
如果你有技术上的问题或困扰
都可以加我的vx(skode250)
和我聊一聊你的故事🧡
以上是关于Unity精华☀️ 哥哥,「设计模式」能解决游戏回放呀,你尝一口!的主要内容,如果未能解决你的问题,请参考以下文章
Unity精华☀️ 哥哥,你会这么多「设计模式」,面试官会心疼你吧
Unity精华☀️Audio Mixer终极教程:用《双人成行》讲解它的用途