我们已经讨论了游戏对象和组件作为Unity引擎的两个基本构件。今天,我们将讨论它们的程序表示。
这是面向软件工程师的Unity for Software Engineers系列,面向熟悉软件开发最佳实践的人们,希望能够更快地介绍Unity作为引擎和编辑器。接下来的几周会有更多,所以考虑订阅更新。
Unity Engine运行时主要是用C++编写的,并且引擎的大部分原型(如游戏对象及其组件)都在C++环境中,您还会知道Unity Engine API是用C#编写的。API让你可以访问Unity的所有原生对象--除了我们今天要讨论的几个缺陷--感觉就像是直观的、惯用的C#。
Unity对象层次的顶部是UnityEngine.Object。因为大多数部分提供了一个名称字符串、一个int GetInstanceID()方法和一组相等比较器。
该类还提供了一个静态void销毁(Object Obj)方法(和一些重载),用于销毁UnityEngine.Object及其任何子类。当一个对象被销毁时,该对象的本机部分将从内存中释放,而较小的托管部分将在不再有对它的引用之后的某个时候被垃圾回收。
因为您对UnityEngine.Object的有效引用可以指向销毁的本机对象,所以UnityEngine.Object覆盖C#的运算符==和运算符!=,以使销毁的对象显示为空。简单地访问已销毁对象上的方法将返回NullReferenceException,尽管会有一条FriendlierError消息告诉您正在尝试访问哪个对象。
让我们从高层次开始:GameObject继承一个名称,实例ID来自父对象。否则,从概念上讲,GameObject。
让我们再深入挖掘一下。开始时,aGameObject中的大多数有趣的东西都在它的组件中。游戏对象至少有一个组件:它的变换。ATransform描述游戏对象的位置和旋转。变换包括辅助对象属性,这些属性显示对象的绝对世界位置和旋转,以及相对于其父对象的位置和旋转。在编辑器中,变换位置和旋转是从父相对变量设置的。
由于每个游戏对象都有一个变换(而且,假设变换是经常需要/访问的),因此游戏对象直接公开变换公共属性。
您可以从T GetComponent<;T>;()访问单个组件,或从T[]GetComponents<;T>;()访问组件列表等。这些方法搜索GameObject上的所有组件并返回兼容类型的组件(如果在单数情况下不存在,则返回NULL)。由于这些方法搜索组件并检查类型兼容性,因此通常建议缓存此查找。
如果您要手动构建/扩展游戏对象,则始终可以使用T AddComponent<;T>;()。然而,在大多数情况下,您最好使用编辑器。
我们在整个系列中广泛讨论了序列化:作为一个基本概念,在我们的编辑器之旅中,在描述Inspector时,以及使用Inspector作为注入框架的实践。
使用标签。每个游戏对象都可以有一个标记字符串。可以通过静态函数GameObject.FindGameObjectsWithTag和GameObject.FindGameObjectWithTag.GameObject使用该标记在场景中查找对象。GameObject还公开公共bool CompareTag(字符串标记)方法。
这是一种既快捷又肮脏的完成工作的方法,但仍然是一种很受欢迎的方法。这在野外的一个常见用法是有一个玩家的标签来找到玩家。理想情况下,不应该每帧都调用这些方法,因此如果您必须使用它们,请考虑缓存结果。
使用图层。层是介于0和31之间的整数。每个游戏对象正好在一个层中。
虽然你不能直接查找一个层中的所有对象,但是如果你已经引用了一个游戏对象(例如,在一个碰撞事件中),你可以对照一个层蒙版检查一个游戏对象。LayerMask通常用于Physics.racast()等函数中。这允许您查找具有与给定光线相交的碰撞器的对象。将LayerMask传递到Physics.racast()将只返回指定层集中的对象。
在Unity引擎内部,相机大量使用层。例如,你可以有一个相机来渲染“除了UI以外的所有东西”,然后叠加另一个相机用于游戏中的HUD,等等。
使用间接引用。上述方法可能不够充分的原因有很多:您可能不想使用标记来避免依赖复制粘贴的字符串,并且层可能不适合您的用例。如果不能引用场景中的同伴对象(例如,您正在处理动态的对象集,或者在需要此引用的上下文中无法访问当前场景对象,等等),那么您可能需要进一步查看。
为此,一个越来越流行的概念是运行时集ScriptableObjects。你可以在Unity的How-to文章中阅读更多关于使用ScriptableObjects构建游戏的文章,这篇文章基于RyanHipple的演讲。如果你有一个小时的空闲时间,你可能想看完整个过程。
GameObject还公开BroadCastMessage和SendMessage函数,这些函数将消息(在组件部分中描述)传播到它里面或下面的所有组件。
Unity通过这些eComponent类的组合定义游戏对象的行为。这是游戏引擎所称的实体组件系统(ECS)的核心原则。Ripple博客有ECS的应用概述,Robert Nystrom的游戏编程模式详细描述了实体组件系统,令人困惑的是,Unity将他们的下一代高性能游戏编程范例称为ECS,它也是基于ECS的,但通过面向数据的设计将事情带到了一个新的水平。
从外部看,Unity的基于MonoBehaviour的范例和Unity ECS都是实体组件系统,尽管您如何使用它们有很大的不同。
GameObject上的每个行为都是通过其组件驱动的,用户实现的组件通常会扩展MonoBehaviour子类(稍后将详细介绍)。
除了其GameObject之外,组件还公开速记属性和方法,如Transform Transform、T GetComponent<;T>;()等。这些只是在相应的GameObject上访问这些相同方法的方便快捷方式。
组件最重要的功能是通过UnityMessages驱动的(在引用内置消息时,有时也称为Unity事件函数)。在某些情况下,这些函数实际上是由Engine触发的回调函数。例如,每个组件将接收唤醒()、启动()、更新()和其他消息。这些消息的执行顺序上的Unity Docs是一个方便的资源。
要让您的组件接收特定消息,只需添加一个具有适当消息名称的私有void方法。如果适用,运行库将使用反射来调用这些消息。这就是为什么您在这些消息上看不到覆盖指令的原因。像Update、LateUpdate和FixedUpdate这样的消息每种类型都会检查一次,所以不用担心在每个帧中都会使用反射。有关更多信息,请参阅“10000更新()调用”Unity博客帖子中的更多详细信息。
行为是一种可以启用或禁用的组件类型。禁用行为时,不调用Start、Update、FixedUpdate、LateUpdate、OnEnable和OnDisable消息。
加载的场景中的游戏对象将存在于内存中,直到该对象被显式销毁或该场景被卸载。游戏对象可以设置为非活动状态,这将使其停止接收更新(和相关)事件。
创建对象时,组件上调用的消息取决于:(1)GameObject是否处于活动状态,以及(2)组件是否已启用:
被销毁时,Unity对象可能会显示为空。==空检查比您想象的要多。
因此,空合并运算符(??,??=)和空条件运算符(?.,?[])不能按预期工作。
不要创建不必要地声明Update或其他消息的抽象类以使覆盖更容易;这将导致引擎始终缩放这些事件。
禁用对象或组件是限制其游戏逻辑或节省CPU开销的好方法,但这些对象仍有内存开销。
您也可以订阅并保持在循环中-这将意味着很多!