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精华☀️四元数(Quaternion)解决万向锁

Unity精华☀️Audio Mixer终极教程:用《双人成行》讲解它的用途

Unity精华☀️Audio Mixer终极教程:用《双人成行》讲解它的用途

Unity精华☀️UI和物体可见性的判断方法

Unity精华☀️UI和物体可见性的判断方法