Unity实现:23种设计模式、《游戏编程模式》
⽬录
⼯⼚⽅法模式、抽象⼯⼚模式、单例模式、建造者模式、原型模式
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
策略模式、模板⽅法模式、观察者模式、迭代⼦模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
Unity3D中各种设计模式的实践与运⽤。
⽬前已经在Unity中实现了《》⼀书中提出的23种设计模式。
每种模式都包含对应的结构实现、应⽤⽰例以及图⽰介绍。类似的结构,此repo中的每种模式⽤单独的⽂件夹分开。每种模式对应的⽂件夹中包含了名为“Structure”的⼦⽂件夹,⾥⾯存放的是此模式在Unity中的使⽤代码的基本框架实现,⽽另外包含的Example⼦⽂件夹中存放的是此模式在Unity中使⽤的实际⽰例。每种框架实现或实例⽰例实现都包含对应的场景,每种模式⽂件夹中可能包含⼀个或者多个Example。
《》⼀书中介绍的常⽤游戏设计模式的Unity版实现也有部分实现。
⼀、23种设计模式在Unity实现
Creational Patterns 创建型模式(5种)
:⽤原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
:确保某⼀个类只有⼀个实例,⽽且⾃⾏实例化并向整个系统提供这个实例。
:提供⼀个接⼝,⽤于创建相关或者依赖对象的家族,⽽不需要指定具体的实现类。
:将⼀个复杂对象的构造与它的表⽰分离,使同样的构建过程可以创建不同的表⽰,这样的设计模式被称为建造者模式。
:定义⼀个⽤于创建对象的接⼝,让⼦类决定实例化哪⼀个类。⼯⼚⽅法使⼀个类的实例化延迟到其⼦类。
Structural Patterns 结构型模式(7种)
:将⼀个类的接⼝转换成客户希望的另外⼀个接⼝。适配器模式使得原本由于接⼝不兼容⽽不能⼀起
⼯作的那些类可以⼀起⼯作。
:将抽象和实现解耦,使得两者可以独⽴地变化。
:将对象组合成树形结构以表⽰“部分-整体”的层次结构,使得⽤户对单个对象和组合对象的使⽤具有⼀致性。
:动态地给⼀个对象添加⼀些额外的职责。就增加功能来说,装饰模式相⽐⽣成⼦类更为灵活。
:要求⼀个⼦系统的外部与其内部的通信必须通过⼀个统⼀的对象进⾏。外观模式提供⼀个⾼层次的接⼝,使得⼦系统更易于使⽤。
:使⽤共享对象可有效地⽀持⼤量的细粒度的对象。
:为其他对象提供⼀种代理以控制对这个对象的访问。
Behavioral Patterns ⾏为型模式(11种)
:命令模式将“请求”封装成对象,以便使⽤不同的请求、队列或者⽇志来参数化其他对象,同时⽀持可撤消的操作。
:
当⼀个对象内在状态改变时允许其改变⾏为,这个对象看起来像改变了其类。
:定义对象间⼀种⼀对多的依赖关系,使得每当⼀个对象改变状态,则所有依赖于它的对象都会得到通知并被⾃动更新。
:使多个对象都有机会处理请求,从⽽避免了请求的发送者和接受者之间的耦合关系。将这些对象连成⼀条链,并沿着这条链传递该请求,直到有对象处理它为⽌。
:⽤⼀个中介对象封装⼀系列的对象交互,中介者使各对象不需要显⽰地相互作⽤,从⽽使其耦合松散,⽽且可以独⽴地改变它们之间的交互。
:给定⼀门语⾔,定义它的⽂法的⼀种表⽰,并定义⼀个解释器,该解释器使⽤该表⽰来解释语⾔中的句⼦。
:提供⼀种⽅法访问⼀个容器对象中各个元素,⽽⼜不需暴露该对象的内部细节。
:在不破坏封装性的前提下,捕获⼀个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状
态。
:定义⼀组算法,将每个算法都封装起来,并且使它们之间可以互换
:定义⼀个操作中的算法的框架,⽽将⼀些步骤延迟到⼦类中。使得⼦类可以不改变⼀个算法的结构即可重定义该算法的某些特定步骤。
:封装⼀些作⽤于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作⽤于这些元素的新的操作。
⼆、《游戏编程模式》的Unity实现
意义
使⽤基类提供的操作集合来定义⼦类中的⾏为。
模式描述
⼀个基类定义了⼀个抽象的沙盒⽅法和⼀些预定义的操作集合。通过将它们设置为受保护的状态以确保它们仅供⼦类使⽤。每个派⽣出的沙盒⼦类根据⽗类提供的操作来实现沙盒函数。
使⽤情形
沙盒模式是运⽤在多数代码库⾥、甚⾄游戏之外的⼀种⾮常简单通⽤的模式。如果你正在部署⼀个⾮虚的受保护⽅法,那么你很可能正在使⽤与之类似的模式。沙盒模式适⽤于以下情况:
你有⼀个带⼤量⼦类的基类。
基类能够提供所有⼦类可能需要执⾏的操作集合。
在⼦类之间有重叠的代码,你希望在它们之间更简便地共享代码。
你希望使这些继承类与程序其他代码之间的耦合最⼩化。
意义
单例模式的几种实现方式通过创建⼀个类来⽀持新类型的灵活创建,其每个实例都代表⼀个不同对象类型。
两个类,可以实现⽆限的种类
模式描述
定义⼀个类型对象类和⼀个持有类型对象的类。每个类型对象的实例表⽰⼀个不同的逻辑类型。每个持有类型对象类的实例引⽤⼀个描述其类型的类型对象。
实例数据被存储在持有类型对象的实例中,⽽所有同概念类型所共享的数据和⾏为被存储在类型对象中。引⽤同⼀个类型对象的对象之间能表⽰出“同类”的特性。这让我们可以在相似对象集合中共享数据和⾏为,这与类派⽣的作⽤有⼏分相似,但却⽆需硬编码出⼀批派⽣类。
使⽤情形
当你需要定义⼀系列不同“种类”的东西,但⼜不想把那些种类硬编码进你的类型系统时,本模式都适⽤。尤其其是当下⾯任意⼀项成⽴时:
你不知道将来会有什么类型(例如,我们的游戏是否需要指出包含怪物新种类的资料包下载?)
你需要在不重新编译或修改代码的情况下,修改或添加新的类型。
意义
允许⼀个单⼀的实体跨越多个不同域⽽不会导致耦合。
模式描述
A single entity spans multiple domains. To keep the domains isolated, the code for each is placed in its own component class. The entity is reduced to a simple container of components.
单⼀实体横跨了多个域。为了能够保持域之间相互隔离,每个域的代码都独⽴地放在⾃⼰的组件类中。实体本⾝则可以简化为这些组件的容器。
组件最常见于游戏中定义实体的核⼼类,但是它们也能够⽤在别的地⽅。当如下条件成⽴时,组件模式就能够发挥它的作⽤:你有⼀个涉及多个域的类,但是你希望这些域保持相互解耦。
⼀个类越来越庞⼤,越来越难以开发。
你希望定义许多共享不同能⼒的对象,但采⽤继承的办法却⽆法令你精确地重⽤代码。
Tips
Unity引擎的主要设计正是围绕组件模型来进⾏的。
意义
对消息或事件的发送与受理进⾏时间上的解耦。
模式描述
事件队列是⼀个按照先进先出顺序存储⼀系列通知或者请求的队列。发出通知时系统会将该请求置⼊
队列并返回,请求处理器随后从事件队列中获取并处理这些请求。请求可由处理器直接处理或转交给对其感兴趣的模块。这⼀模式对消息的发送者与受理者进⾏了解耦,使消息的处理变得动态且⾮实时。
使⽤情形
如果你只是想解耦接收者和发送者,像观察者模式和命令模式都可以⽤较⼩的复杂度来进⾏处理。在需要解耦某些实时的内容时才建议使⽤事件队列。
不妨⽤推和拉来的情形来考虑。有⼀块代码A需要另⼀块代码B去做些事情。对A⾃然的处理⽅式是将请求推给B。
同时,对B⾃然的处理⽅式是在B⽅便时将请求拉⼊。当⼀端有推模型另⼀端有拉模型时,你就需要在它们间放⼀个缓冲的区域。 这就是队列⽐简单的解耦模式多出来的那⼀部分。
队列给了代码对拉取的控制权——接收者可以延迟处理,合并或者忽视请求。发送者能做的就是向队列发送请求然后就完事了,并不能决定什么时候发送的请求会受到处理。⽽当发送者需要⼀些回复反馈时,队列模式就不是⼀个好的选择。
意义
游戏循环模式,实现游戏运⾏过程中对⽤户输⼊处理和时间处理的解耦。
模式描述
游戏循环模式:游戏循环在游戏过程中持续运转。每循环⼀次,它⾮阻塞地处理⽤户的输⼊,更新游戏状态,并渲染游戏。它跟踪流逝的时间并控制游戏的速率。
游戏循环将游戏的处理过程和玩家输⼊解耦,和处理器速度解耦,实现⽤户输⼊和处理器速度在游戏⾏进时间上的分离。
游戏循环也许需要与平台的事件循环相协调。如果在操作系统的⾼层或有图形UI和内建事件循环的平台上构建游戏,那就有了两个应⽤循环在同时运作,需要对他们进⾏相应的协调。
使⽤情形
任何游戏或游戏引擎都拥有⾃⼰的游戏循环,因为游戏循环是游戏运⾏的主⼼⾻。
Unity已经内建了游戏循环模式,即Update( )⽅法。
意义
提供服务的全局接⼊点,⽽不必让⽤户和实现它的具体类耦合。
模式描述
的服务提供者实现这个接⼝。⼀个单独的服务定位器通过查⼀个合适的提供器来提供这个服务的访问,它同时屏蔽了提供器的具体类型和定位这个服务的过程。
⼀般通过使⽤单例或者静态类来实现服务定位模式,提供服务的全局接⼊点。 服务定位模式可以看做是更加灵活,更加可配置的单例模式。如果⽤得好,它能以很⼩的运⾏时开销,换取很⼤的灵活性。相反,如果⽤得不好,它会带来单例模式的所有缺点以及更多的运⾏时开销。 使⽤服务定位器的核⼼难点是它将依赖,也就是两块代码之间的⼀点耦合,推迟到运⾏时再连接。这有了更⼤的灵活度,但是代价是更难在阅读代码时理解其依赖的是什么
Tips
服务定位器模式在很多⽅⾯和单例模式⾮常相近,所以值得考虑两者来决定哪⼀个更适合你的需求。
Unity引擎把这个模式和组件模式结合起来,并使⽤在了GetComponent()⽅法中。
意义
通过合理组织数据利⽤CPU的缓存机制来加快内存访问速度。
模式描述
现代的CPU有缓存来加速内存读取,其可以更快地读取最近访问过的内存毗邻的内存。基于这⼀点,我们通过保证处理的数据排列在连续内存上,以提⾼内存局部性,从⽽提⾼性能。
为了保证数据局部性,就要避免的缓存不命中。也许你需要牺牲⼀些宝贵的抽象。你越围绕数据局部性设计程序,就越放弃继承、接⼝和它们带来的好处。没有银弹,只有权衡。
使⽤情形
使⽤数据局部性的第⼀准则是在遇到性能问题时使⽤。不要将其应⽤在代码库不经常使⽤的⾓落上。 优化代码后其结果往往更加复杂,更加缺乏灵活性。
就本模式⽽⾔,还得确认你的性能问题确实由缓存不命中⽽引发的。如果代码是因为其他原因⽽缓慢,这个模式⾃然就不会有帮助。
简单的性能评估⽅法是⼿动添加指令,⽤计时器检查代码中两点间消耗的时间。⽽为了到糟糕的缓存使⽤情况,知道缓存不命中有多少发⽣,⼜是在哪⾥发⽣的,则需要使⽤更加复杂的⼯具—— profilers。
组件模式是为缓存优化的最常见例⼦。⽽任何需要接触很多数据的关键代码,考虑数据局部性都是很重要的。
意义
将⼯作推迟到必要时进⾏以避免不必要的⼯作。
模式描述
⼀组原始数据随时间变化。⼀组颜⾊数据经过⼀些代价昂贵的操作由这些数据确定。⼀个脏标记跟踪这个衍⽣数据是否和原始数据同步。它在原始数据改变时被设置。如果它被设置了,那么当需要衍⽣数据时,它们就会被重新计算并且标记被清除。否则就使⽤缓存的数据。
使⽤情形
就像其他优化模式⼀样,此模式会增加代码复杂度。只在有⾜够⼤的性能问题时,再考虑使⽤这⼀模式。
脏标记在这两种情况下适⽤:
当前任务有昂贵的计算开销
当前任务有昂贵的同步开销。 若满⾜这两者之⼀,也就是两者从原始数据转换到⽬标数据会消耗很多时间,都可以考虑使⽤脏标记模式来节省开销。
若原始数据的变化速度远⾼于⽬标数据的使⽤速度,此时数据会因为随后的修改⽽失效,此时就不适合使⽤脏标记模式。
意义
使⽤固定的对象池重⽤对象,取代单独地分配和释放对象,以此来达到提升性能和优化内存使⽤的⽬的。
模式描述
定义⼀个保持着可重⽤对象集合的对象池类。其中的每个对象⽀持对其“使⽤(in use)”状态的访问,以确定这⼀对象⽬前是否“存活(alive)”。在对象池初始化时,它预先创建整个对象集合(通常为⼀块连续堆区域),并将它们都置为“未使⽤(not in use)”状态。
当我们想要创建⼀个新对象时,就向对象池请求。它将搜索到⼀个可⽤的对象,将其初始化为“使⽤中(in use)”状态并返回给你。当该对象不再被使⽤时,它将被置回“未使⽤(not in use)”状态。使⽤该⽅法,对象便可以在⽆需进⾏内存或其他资源分配的情况下进⾏任意的创建和销毁。
使⽤情形
此模式被⼴泛地应⽤于游戏中的可见物体,如游戏实体对象、各种视觉特效。但是它也可在⾮可见的数据结构上使⽤,⽐如当前播放的声⾳。
满⾜以下情况可以使⽤对象池:
需要频繁创建和销毁对象时。
对象⼤⼩⼀致时。
在堆上分配对象缓慢或者会导致内存碎⽚时。
每个对象都封装了很昂贵且⼜可以重⽤的资源,如数据库、⽹络的连接。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论