(翻译)构建自定义组件

   原文地址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 reachedDebug.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 / Propertiespublic 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 Publicpublic 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 / Propertiespublic 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 & Destructorpublic Whole (){}public Whole (string name) : this (){this.name = name;}~ Whole (){Destroy();}#endregion#region Publicpublic 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 Privatevoid 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 reachedDebug.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. 

          

转载于:https://www.cnblogs.com/bambomtan/p/5018917.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/491521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python matplotlib画图设置坐标轴刻度的字体大小

import matplotlib.pyplot as pltplt.xticks([0, 100, 200, 300, 400, 500, 600, 700]) plt.tick_params(labelsize13) #刻度字体大小13

JavaSE——类集(下)(Set、Comparable、Collections、Comparator、Map)

第2节 集合&#xff08;下&#xff09; 一、Set接口 java.util.Set 接口和 java.util.List接口一样&#xff0c;同样继承自 Collection接口&#xff0c;它与Collection接口中的方法基本一致&#xff0c;并没有对 Collection接口进行功能上的扩充&#xff0c;只是比Collection…

美国国家科学院发布《材料研究前沿:十年调查》

来源&#xff1a;中国科学院科技战略咨询研究院2月8日&#xff0c;美国国家科学院发布了针对材料研究的第三次十年调查《材料研究前沿&#xff1a;十年调查》报告。这次的调查主要评估了过去十年中材料研究领域的进展和成就&#xff0c;确定了2020-2030年材料研究的机遇、挑战和…

useContext 和 useReducer语法讲解

useContext 和 useReducer useContext 和 useReducer 传递state dispatch, 模拟redux useContext 用法 // App.tsx const UserContext React.createContext({ name: }) function App() {return (<UserContext.Provider value{{ name: jack }}><div><p>欢…

Openfire on Centos7

学习一下linux&#xff0c;装备 1&#xff09;centos 最小安装。&#xff08;找抽的节奏&#xff09; 2&#xff09;必备 oepnssh yum install openssh-server.x86_64 3&#xff09;配置网络。打开 /etc/sysconfig/network-scripts/ifcfg-* 网卡配置。 TYPEEthernet #改为s…

python matplotlib画图改变图标题和坐标轴标题的字体大小

import matplotlib.pyplot as pltplt.title(Input,fontdict{weight:normal,size: 20}) #改变图标题字体 plt.xlabel(Time, fontdict{weight: normal, size: 13})#改变坐标轴标题字体

JavaSE——IO(上)(File、字节流、字符流、转换流、打印流、缓存流)

第3节 IO&#xff08;上&#xff09; 一、File类与文件基本操作 在程序中经常需要用到文件的操作&#xff0c;Java有专门的类来进行文件的操作——File类。 1.1 File类概述 它是对文件和目录路径名的抽象表示。 即它本身不是一个文件&#xff0c;只是一个抽象表示&#xff…

学习新技能时,大脑在如何发生改变?

来源&#xff1a;中国生物技术网众所周知&#xff0c;无论是一项运动、一种乐器还是一门手艺&#xff0c;掌握一项新技能都是需要花费时间并进行训练的。虽然我们都知道健康的大脑能够应付的来&#xff0c;但是为了开发出新行为大脑如何发生改变科学家们对此仍知之甚少。近日&a…

1-4-14:计算邮资

描述 根据邮件的重量和用户是否选择加急计算邮费。计算规则&#xff1a;重量在1000克以内(包括1000克), 基本费8元。超过1000克的部分&#xff0c;每500克加收超重费4元&#xff0c;不足500克部分按500克计算&#xff1b;如果用户选择加急&#xff0c;多收5元。 输入输入一行&a…

JavaSE——IO(下)(Properties类、序列化与反序列化)

第3节 IO&#xff08;下&#xff09; 一、.properties文件与Properties类 1.1 .properties文件介绍 .properties文件一种属性文件&#xff0c;以键值对 的格式存储内容&#xff0c;在Java中可以使用Properties类来读取这个文件&#xff0c;一般来说它作为一些参数的存储&…

MATLAB获得子图位置

a1subplot(9,11,1) get(a1,position) %[0.1300 0.8577 0.0547 0.0673] %前面两个数值分别代表子图离左边框和下边框的距离&#xff0c;后面两个数值是子图的长和宽

VS调试dll详细过程记录

VS调试dll详细过程记录 qianghaohao(孤狼) 前言&#xff1a;在我们写的程序中有时候调用dll&#xff0c;并且需要跟踪dll中的函数&#xff0c;此时直接调试调用dll的工程是无法跳进dll的函数的&#xff0c;此时我们可以启动dll工程 来跟踪程序的走向。注意&#xff1a;要有…

谁在真正领跑 5G:技术创新和标准

来源&#xff1a;云头条5G是包括美国总统特朗普在内的所有人都在谈论的新技术。所以&#xff0c;每家公司自然都想谈论5G以及如何领跑这个领域。然而现实情况是&#xff0c;移动5G是一项涵盖甚广的无线标准&#xff0c;它改变了我们对蜂窝通信的认识&#xff0c;并前所未有地拓…

JavaSE——XML与JSON(语法格式、解析内容)

第6节 XML与JSON 一、XML 1.1 XML简介 XML全称为可扩展标记语言&#xff08;extensible Markup Language&#xff09; 。 特性&#xff1a; xml具有平台无关性&#xff0c;是一门独立的标记语言&#xff1b; xml具有自我描述性。 用途&#xff1a; 网络数据传输 数据存…

matlab画图设置

设置坐标轴显示范围&#xff1a; xlim([0,150]) 设置坐标轴显示的刻度&#xff1a; set(gca, XTicklabel,[0,50,100,150] ); 不显示坐标轴刻度&#xff1a; set(gca, YTicklabel,[] ); 设置坐标轴标题及旋转角度&#xff1a; ylabel(yl,Rotation,0) 设置字体大小 se…

root 链接ftp

could not change active booleans:Inalid boolean 2014-11-02 16:05:34分类&#xff1a; Linux could not change active booleans:Inalid boolean [rootumboyserver vsftpd]# setsebool allow_ftpd_full_access 1[rootumboyserver vsftpd]# setsebool allow_ftpd_use_cifs 1…

三大阶段,四大领域,详解你不知道的AIoT!

AIoT即AIIoT&#xff0c;指的是人工智能技术与物联网在实际应用中的落地融合。目前&#xff0c;越来越多的行业及应用将AI与IoT结合到了一起&#xff0c;AIoT已经成为各大传统行业智能化升级的最佳通道&#xff0c;也是未来物联网发展的重要方向。来源&#xff1a;物联网智库AI…

Spring的@Resource注解报java.lang.NoSuchMethodError

见&#xff1a;https://www.cnblogs.com/xiaoguoniu/p/13504601.html 少了javax的包

word修改公式中的部分字体

有时候只想修改word公式中的部分字体大小&#xff0c;方法是先将公式变成普通文本格式再进行修改。