原文地址https://theliquidfire.wordpress.com/2015/07/20/custom-component-based-system/ 我很喜欢的一个老外大神的博客,翻译出来以供自己学习,也共享出来。
我自己的总结就是,在对model层编码 而没有继承MonoBehaviour的情况下,使用组件的方式取代继承的方式实现编码
在这一节里我将介绍模仿unity组件结构实现的自定义的组件结构。它其实并不像人们想象的那么难。那我究竟是为什么要做这件事呢?我想我可以给出大量的合理的理由,但是我将让你们来决定是否值得这么做,像往常一样,欢迎大家来评判!
什么是基于组件的结构
简而言之这就是一个让系统更加健壮的保存组件的通用容器。在unity里面,GameObject 是一个可以添加组件的容器,就像继承至Monobehaviour的脚本。
我的观点是,使用这种结构能够帮助你支持对象组合,而非类的继承,这是一件好事。If I have lost you, then imagine the following scenario. 我们正在创建一个RPG游戏,并且你的任务是实现 物品和物品清单 系统(item and inventory system). 你或许会通过继承实现这个系统就像下面这样:
· BaseItem.cs (或许会持有像购买和销售价格的信息)
BaseConsumable.cs (消耗类别的子类)
HealthPotion.cs (生命服剂)
BaseEquippable.cs (装备类别的子类)
BaseArmor.cs (盔甲子类)
LightArmor.cs (光明铠甲)
HeavyArmor.cs (大地铠甲)
这个列表展示了一个通过深层次的继承来试图最小化代码的方案,这个列表是一个非常简单的列子,并且一个滥用继承的链会比这个更深。在你的设计结构里需要一些异常规则的时候继承的问题就出现了():
- 如果我们需要一些你可以装备或者消耗的物品,但是又不能销售,或许是因为他们是故事情节的的关键性的物品,或者因为它们被诅咒了?
- 如果一个物品既能被消耗又能被装备呢?在一些最终幻想的游戏里你可以允许一个角色去使用一个不能装备的物品去完成一些特殊的事情像铸件一张符咒。
- 你如何在两个不同的继承分支中来分配特殊的属性呢?
或许有办法克服这些问题,但是经常所采用的方法不会很高雅,并且额外的属性被添加到基类中,除了期望的那个类外对于其他类来说都未被使用并且浪费。 如果你通过基于组件的模式来完成这样的任务,那么代码将会看起来完全不同。例如,你 甚至不用一个Item的基类。Item本身就会是非常可复用的一个容器类(就像GameObject) .为了让 Item可销售,你只需要简单的添加一个适当的组件,表明一个Merchandise 组件,任何对象,没有包含这个主键的都不能添加到商店的目录,并且你也不能销售他——问题解决了。 为了让物品可装备 或者 可消耗,再添加一个适当的组件,如果你想它既能被装备又能消耗,只需要添加这两种组件就行了。如果你想的话,你依旧可以在这个模式中使用继承,但是这样继承链就该比较浅。
为什么我们要创建自己的组件取代unity的实现呢?
那么为什么我要做这些事情呢?很高兴你问这个问题,假设你在做一个RPG类型的游戏(就像我现在做的这样). 你将会需要对(逻辑意义上的) 你的(characters,items,equiment)角色,物品,装备等进行建模 。这些物品 会在大量的游戏世界场景里可见的,如一个战斗场景,商店,目录里面。这让你为了达到可重用性的目的而将你的的model从view 层中分离出来:
Tips:
如果你还不清楚MVC的话建议你读这篇Model—View—Controller结构的这篇文章
我想这是再自然而然的去实现model模式的代码在没有继承MonoBehaviour的情况下去保持最大的灵活性和 最少的累赘。这就是我为什么这么做的缘由了。在一些像RPG这样复杂的游戏里,你需要规则和特殊处理,甚至需要对特殊处理进行特殊处理。你不会意识到这是多么复杂的系统直到你自己去写。
我发现我的models 变的 “smarter” —— 它们视图去做更多的事情,并且甚至发送 和 回应事件。 在我与类的耦合斗争的时候,我发现很难去 想出一个高雅 而 紧凑的方式去 实现一些事情 像从事件中注销事件那样。
为什么那样很艰难?
如果你不知道为什么一个好的架构对于事件的注册和注销是困难的,那么看下面的例子,新建一个unity场景并且附加一个叫Demo的脚本给camera。
using UnityEngine; using System; using System.Collections; public class Demo : MonoBehaviour { public static event EventHandler apocolypseEvent; void Start () { CreateLocalScopedInstances(); GC.Collect(); if (apocolypseEvent != null) apocolypseEvent(this, EventArgs.Empty); } void CreateLocalScopedInstances () { new Mortal(); new Immortal(); } } public class Mortal { public Mortal () { Debug.Log("A mortal has been born"); } ~ Mortal () { Debug.Log("A mortal has perished"); } } public class Immortal { public Immortal () { Debug.Log("An immortal has been born"); Demo.apocolypseEvent += OnApocolypticEvent; } ~ Immortal () { // This won't ever be reached Debug.Log("An immortal has perished"); Demo.apocolypseEvent -= OnApocolypticEvent; } void OnApocolypticEvent (object sender, EventArgs e) { Debug.Log("I'm alive!!!"); } }
在这个演示里面我使用了一个方法去示例 一个本地对象实例化一个Mortal 和一个 Immortal。正常情况下,在局部范围类创建一个对象,你希望在方法结束的时候这个对象能被释放。由于垃圾回收系统在这种情况下Mortal对象会被销毁,为什么在Mortal没有的情况下Immortal对象还存活下来了呢?这并不是因为名字的原因。在这个例子里,我展现了一个常见的错误——我试图在一个构造器里做为一个注册事件的时机(就像我或许会在Awake 或者 OnEnable)并且我也试图使用构析函数作为移除事件的时机(就像在 OnDisable 和 OnDestroy )。
问题是Immortal对象依然存活,因为静态事件对它保存这一个强引用,更糟的是静态事件永远也不会被销毁(至少在app运行期间). 我不能使用构析函数作为注销的时机因为在一个强引用被保持的期间对象无法被销毁(CG原理 )。
让事情更加透彻,垃圾回收系统 作为语言的特性被添加是因为程序员经常在内存管理上犯错误。随着你系统的复杂度的增长管理的对象的生命周期将会很狡诈。希望这个能阐明为什么一些事情不总是向表面上显看起来而易见的,就像在你应该添加和移除事件监听器的时候。
Exit the Rabbit Trail
在我努力写我复杂的代码的时候,我开始强烈的怀念unity提供的一些优美的特性:
- 我喜欢模型对象在检视面板中的显示层次结构的功能
- 我喜欢这个基于组件的良好的灵活的结构,允许我在它共享的容器里获取组件。更不用说在相同的层次面板中得到父子容器
- 我喜欢它所有的组件它生命周期的方法都能接受响应像 Awake,OnEnable,OnDisable 和 OnDestroy
所有的这些特性让代码保持独立性变动更加容易,给清零像对事件的注销 提供的很大的方便,并且通常让一个复杂的数据模型变的更加容易理解。同时,这种简单的方法(就像unity提供的)伴随的是大量的额外的包袱,就像不幸的限制,没有后台线程。
所以我开始想。。。去写我自己的拥有从MonoBehaviour的所有特性的组件系统真的很难吗
On to the Code!
最难的一部分,哎,就是起一个好的名字。我不想在另一个命名空间中重复使用GameObject 和 Component 而让人们感到困惑。我花了大量的事件去查看同义词并且最后选定使用Whole 作为容器, Part 作为组件。此外,我决定指定一个接口,以防我或许想整合基于Part的MonoBehaviour的系统,我需要避免属性和方法的同名。
Interfaces
第一步是 Part(模式的组件分配),任何 继承这个接口的必须显示它的容器的引用—IWhole . 我使用两个额外的属性:allowed ,running ,和 GameObject的 activeSelf 和 activeInHierarchy 相似. 例如,一个part(组件) 可以允许允许,但是如果容器不允许运行,那么它的parts 也不允许运行。只有一个part和它的容器和所有的父容器都允许,运行的标签才会被标记为true。
Check()方法 是用来告述一个Part 它因该检查他自己的状态(不管他是运行还是没有,)——就像修饰了一个组件和容器允许的属性一个父的层级 或 切换 such as after modifying a parent hierarchy or toggling the allowed property of a component or container,
Assemble 方法基本上等价于Awake方法,它只会被唤醒一次,在part第一次被 启用和运行的时候。 Disassemble方法 是它的副本 也和 OnDestroy相似。 他也只允许一次,在part 回收前。
Resume 和 Suspend 和 OnEnable OnDisable 相似,它们能被唤醒多次。 任何时候part 在从 不运行到 运行,就是Resumed会执行。从运行到不运行就触发 Suspend。
using UnityEngine; using System.Collections; public interface IPart { IWhole whole { get; set; } bool allowed { get; set; } bool running { get; } void Check (); void Assemble (); void Resume (); void Suspend (); void Disassemble (); }
下一步就是Whole(模式的容器部分)。这个接口有点像Part 例如伴随者Check方法的 allowed , running 属性 。
另外,这个接口看起来更像介于Gameobject 和 Transform 之间的混合物。它有能力添加和移除Part, GetPart(从它自己或者 层次面板) 和 管理
它的层次(例如 通过添加一个子对象 或者编辑它的父对象)
using UnityEngine; using System.Collections; using System.Collections.Generic; public interface IWhole { string name { get; set; } bool allowed { get; set; } bool running { get; } IWhole parent { get; set; } IList<IWhole> children { get; } IList<IPart> parts { get; } void AddChild (IWhole whole); void RemoveChild (IWhole whole); void RemoveChildren (); T AddPart<T> () where T : IPart, new(); void RemovePart (IPart p); T GetPart<T>() where T : class, IPart; T GetPartInChildren<T>() where T : class, IPart; T GetPartInParent<T>() where T : class, IPart; List<T> GetParts<T>() where T : class, IPart; List<T> GetPartsInChildren<T>() where T : class, IPart; List<T> GetPartsInParent<T>() where T : class, IPart; void Check (); void Destroy (); }
Implementation
这是实现IPart接口的类。 注意组件仅仅只有对它的容器的一个弱引用。这种方法,一个对象容器就可以被销毁即使它的组件有一个强引用(This way, an object container can go out of scope even if there are strong references to its components.)。最初我计划设计这个引用在一个构造器里,但是我不喜欢任何允许使用泛型创建和添加一个part的解决方法。
using UnityEngine; using System; using System.Collections; public class Part : IPart { #region Fields / Properties public IWhole whole { get { return owner != null ? owner.Target as IWhole : null; } set { owner = (value != null) ? new WeakReference(value) : null; Check(); } } WeakReference owner = null; public bool allowed { get { return _allowed; } set { if (_allowed == value) return; _allowed = value; Check(); } } bool _allowed = true; public bool running { get { return _running; } private set { if (_running == value) return; _running = value; if (_running) { if (!_didAssemble) { _didAssemble = true; Assemble(); } Resume(); } else { Suspend(); } } } bool _running = false; bool _didAssemble = false; #endregion #region Public public void Check () { running = ( allowed && whole != null && whole.running ); } public virtual void Assemble () { } public virtual void Resume () { } public virtual void Suspend () { } public virtual void Disassemble () { } #endregion }
这是IWhole接口的实现
using UnityEngine; using System.Collections; using System.Collections.Generic; public sealed class Whole : IWhole { #region Fields / Properties public string name { get; set; } public bool allowed { get { return _allowed; } set { if (_allowed == value) return; _allowed = value; Check(); } } bool _allowed = true; public bool running { get { return _running; } private set { if (_running == value) return; _running = value; for (int i = _parts.Count - 1; i >= 0; --i) _parts[i].Check(); } } bool _running = true; public IWhole parent { get { return _parent; } set { if (_parent == value) return; if (_parent != null) _parent.RemoveChild(this); _parent = value; if (_parent != null) _parent.AddChild(this); Check (); } } IWhole _parent = null; public IList<IWhole> children { get { return _children.AsReadOnly(); }} public IList<IPart> parts { get { return _parts.AsReadOnly(); }} List<IWhole> _children = new List<IWhole>(); List<IPart> _parts = new List<IPart>(); bool _didDestroy; #endregion #region Constructor & Destructor public Whole () { } public Whole (string name) : this () { this.name = name; } ~ Whole () { Destroy(); } #endregion #region Public public void Check () { CheckEnabledInParent(); CheckEnabledInChildren(); } public void AddChild (IWhole whole) { if (_children.Contains(whole)) return; _children.Add(whole); whole.parent = this; } public void RemoveChild (IWhole whole) { int index = _children.IndexOf(whole); if (index != -1) { _children.RemoveAt(index); whole.parent = null; } } public void RemoveChildren () { for (int i = _children.Count - 1; i >= 0; --i) _children[i].parent = null; } public T AddPart<T> () where T : IPart, new() { T t = new T(); t.whole = this; _parts.Add(t); return t; } public void RemovePart (IPart p) { int index = _parts.IndexOf(p); if (index != -1) { _parts.RemoveAt(index); p.whole = null; p.Disassemble(); } } public T GetPart<T>() where T : class, IPart { for (int i = 0; i < _parts.Count; ++i) if (_parts[i] is T) return _parts[i] as T; return null; } public T GetPartInChildren<T>() where T : class, IPart { T retValue = GetPart<T>(); if (retValue == null) { for (int i = 0; i < _children.Count; ++i) { retValue = _children[i].GetPartInChildren<T>(); if (retValue != null) break; } } return retValue; } public T GetPartInParent<T>() where T : class, IPart { T retValue = GetPart<T>(); if (retValue == null && parent != null) retValue = parent.GetPartInParent<T>(); return retValue; } public List<T> GetParts<T>() where T : class, IPart { List<T> list = new List<T>(); AppendParts<T>(this, list); return list; } public List<T> GetPartsInChildren<T>() where T : class, IPart { List<T> list = GetParts<T>(); AppendPartsInChildren<T>(this, list); return list; } public List<T> GetPartsInParent<T>() where T : class, IPart { List<T> list = new List<T>(); AppendPartsInParent<T>(this, list); return list; } public void Destroy () { if (_didDestroy) return; _didDestroy = true; allowed = false; parent = null; for (int i = _parts.Count - 1; i >= 0; --i) _parts[i].Disassemble(); for (int i = _children.Count - 1; i >= 0; --i) _children[i].Destroy(); } #endregion #region Private void CheckEnabledInParent () { bool shouldEnable = allowed; IWhole next = parent; while (shouldEnable && next != null) { shouldEnable = next.allowed; next = next.parent; } running = shouldEnable; } void CheckEnabledInChildren () { for (int i = _children.Count - 1; i >= 0; --i) _children[i].Check(); } void AppendParts<T> (IWhole target, List<T> list) where T : class, IPart { for (int i = 0; i < target.parts.Count; ++i) if (target.parts[i] is T) list.Add(target.parts[i] as T); } void AppendPartsInChildren<T>( IWhole target, List<T> list ) where T : class, IPart { AppendParts<T>(target, list); for (int i = 0; i < target.children.Count; ++i) AppendPartsInChildren<T>(target.children[i], list); } void AppendPartsInParent<T>( IWhole target, List<T> list ) where T : class, IPart { AppendParts<T>(target, list); if (target.parent != null) AppendPartsInParent<T>(target.parent, list); } #endregion }
Another Demo
你看了第一个演示么? Immortal 对象 没有被垃圾回收因为它注册了一个静态事件。 接下来这个demo看起来很相似。由于我I的新结构,这次我有了一个能为静态呢事件安全注册的对象,但是也能注销 且被垃圾回收
using UnityEngine; using System; using System.Collections; public class Demo : MonoBehaviour { public static event EventHandler apocolypseEvent; void Start () { CreateLocalScopedInstances(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); if (apocolypseEvent != null) apocolypseEvent(this, EventArgs.Empty); } void CreateLocalScopedInstances () { IWhole whole = new Whole("Mortal"); whole.AddPart<MyPart>(); } } public class MyPart : Part { public override void Resume () { base.Resume (); Debug.Log("MyPart is now Enabled on " + whole.name); Demo.apocolypseEvent += OnApocolypticEvent; } public override void Suspend () { base.Suspend (); Debug.Log("MyPart is now Disabled"); Demo.apocolypseEvent -= OnApocolypticEvent; } ~ MyPart () { Debug.Log("MyPart has perished"); } void OnApocolypticEvent (object sender, EventArgs e) { // This won't actually be reached Debug.Log("I'm alive!!!"); } }
总结
In this post, I mentioned some of the difficulties of writing an RPG architecture which doesn’t suck. Once I got passed that, I helped demonstrate what difficulties I had personally encountered to help explain why in the world I would ever want to write my own Component-Based Architecture along the lines of what Unity had already provided. I showed my implementation and a demo which overcame one of the challenges I brought up, and hopefully now you understand both the why and the how behind this post.
The code I provided here is very fresh (read – not battle tested) so it is very possible there is room for improvement. If you have anything good, bad, or indifferent to say feel free to comment below.