Unity精华☀️ 「设计模式」的终极详解!

简介: 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️⃣ 工厂接口:IFactory

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️⃣ 基础类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️⃣ 基础类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);
    }
}

3️⃣ 迭代器示例:Group

继承IEnumerable就好,Group便已实现了迭代器模式

你可以重写、拓展你的迭代器,实现想要的功能

using UnityEngine;
 
public class Group : IEnumerable
{
    public override IteratorBase GetIterator()
    {
        Debug.Log("你可以重写你的迭代器");
        return base.GetIterator();
    }
}

下面是最后一步,有的同学别睡觉,敲黑板


4️⃣ 使用示例: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里调用。

1️⃣ 效果演示

点击录制后,我用的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;
    }
}

甭管你现在有没有跳槽升职的想法,赶紧先备着,

面试前天背一背,对吧?

相关文章
|
存储 缓存 NoSQL
数据库实体与关系模型
【5月更文挑战第16天】本文介绍了数据库模型和UML关系,UML包括依赖、关联(聚合、组合)、泛化和实现4类关系。UML有13种图,分为结构图、行为图和交互图。数据库的基本数据模型包括外模式(用户视图)、概念模式(全局逻辑结构)和内模式(物理存储)。数据模型三要素是数据结构、操作和约束条件。简单易用的缓存数据模型适用于需求灵活、高性能、大数据量且不要求强一致性的场景。
424 5
数据库实体与关系模型
|
消息中间件 存储 算法
嵌入式操作系统服务机制
嵌入式操作系统服务机制
397 0
|
设计模式 安全 图形学
Unity精华☀️ 面试官眼中的「设计模式」
Unity精华☀️ 面试官眼中的「设计模式」
|
10月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
11月前
|
机器学习/深度学习
 GHSL: 全球1975 年到 2030 年以 5 年间隔建成面积的分布情况(100m)
全球人类住区图层(GHSL)项目提供了1975年至2030年建成面积的分布数据,以每100米网格单元为单位,涵盖建筑总面积及非住宅用途面积。该数据集由欧盟委员会联合研究中心(EC JRC)提供,时间间隔为5年,支持时空分析与预测。详情参见[GHSL 数据包 2023](https://ghsl.jrc.ec.europa.eu/documents/GHSL_Data_Package_2023.pdf?t=1683540422)。
348 7
|
缓存 前端开发 JavaScript
9大高性能优化经验总结,Java高级岗必备技能,强烈建议收藏
关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。本文介绍了9种性能优化方法,涵盖代码优化、数据库优化、连接池调优、架构层面优化、分布式缓存、异步化、Web前端优化、服务化、硬件升级、搜索引擎和产品逻辑优化。欢迎留言交流。
|
SQL 存储 缓存
MySQL是如何保证数据不丢失的?
文章详细阐述了InnoDB存储引擎中Buffer Pool与DML操作的关系。在执行插入、更新或删除操作时,InnoDB为了减少磁盘I/O,会在Buffer Pool中缓存数据页进行操作,随后将更新后的“脏页”刷新至磁盘。为防止服务宕机导致数据丢失,InnoDB采用了日志先行(WAL)机制,通过将DML操作记录为Redo Log并异步刷新到磁盘,结合双写机制和合理的日志刷新策略,确保数据的持久性和一致性。尽管如此,仍需合理配置参数以平衡性能与数据安全性。
422 1
MySQL是如何保证数据不丢失的?
|
存储 机器学习/深度学习 算法
Adam-mini:内存占用减半,性能更优的深度学习优化器
论文提出一种新的优化器Adam-mini,在不牺牲性能的情况下减少Adam优化器的内存占用。
477 10
Adam-mini:内存占用减半,性能更优的深度学习优化器
Unity精华☀️点乘、叉乘终极教程:用《小小梦魇》讲解这个面试题~
Unity精华☀️点乘、叉乘终极教程:用《小小梦魇》讲解这个面试题~
|
存储 API 图形学
Unity精华☀️二、到底是什么原因导致“万向锁”?旋转翻车的终极解析!
Unity精华☀️二、到底是什么原因导致“万向锁”?旋转翻车的终极解析!