代码改变世界

解构Unity的腳本物件模型

2010-02-26 00:11 by Milo Yip, ... 阅读, ... 评论, 收藏, 编辑

Unity 是一个以 Mono 为基础的游戏开发环境,能同时支持三种脚本语言,包括 C#、Javascript 和 Boo (类似 Python)。 由于 Unity 的开发工具暂时只有 Mac 的版本 (2010年2月25日更新: 现时已有Windows版本,而且有免费授权版,另外因为Unity iPhone版的出现使Unity的使用者大增),所以暂时未能测试。但是它有很详细的文档,看上来很易用,所以就从文字上学习它的 Script 使用方式。 跟据一些 Tutorial参考手册,我用 Graphviz 画了一个 (我认为) 最核心的 UML 类图:

从这个类图我们可以理解它的结构,及如何把一些常用功能映射至这系统里,以下分节讨论。

GameObject 和 Component

Unity 的执行环境里,会有一个场境 (Scene)。这个场境包含一个 GameObject 对象的层阶 (Hierarchy)。 这个 GameObject 类只是一个容器,本身没有其他功能。使用者需要为 GameObject 加入各种 Component 对象来定义它的行为,而不是透过继承 (inherit) GameObject 来加入 行为。 一个对象可拥有多个 Component 对象,但有一些 Component 类别只可以在一个 GameObject 中有一个 实例 (instance)。

MonoBehavior

我最感兴趣的,是使用者如何自行定义行为来做出不同的 Gameplay。在 Unity 中,程式员编写的 Script,其实也是 Component 的一种,所有的 Script 都会继承自 MonoBehavior 类别。以下是一个简单例子:

var speed = 5.0;

function Update () {
    var x = Input.GetAxis("Horizontal") * Time.deltaTime * speed;
    var z = Input.GetAxis("Vertical") * Time.deltaTime * speed;
    transform.Translate(x, 0, z);
}

把这个 Script 加进一个 GameObject 的话 (成为该 GameObject 的一个 Component),Runtime 会在每帧呼叫 Update(),玩家就可以用上下左右键控制那个 GameObject 在水平方向移动。。

Transform

每个能在三维空间里的 GameObject 都会有 Transform Component (未有详细看是否有一些 GameObject 可以省郤 Transform,例如一个用来定义一个游戏任务的 GameObject)。Transform 包括平移、旋转及缩放。 之前的例子已用了 Transform Component,不过它其实是 Object 类别的一个简写,这简写其实等同:

GetComponent(Transform).Translate(x, 0, z)

Component 的连结

在 Script Tutorial 里的例子是写一个 Follow 的行为,拥有这个 Component 的 GameObject 会自动追踪 (面对着) 一个目标对象:

var target : Transform;

function Update () {
    transform.LookAt(target);
}

这个 Script 暴露了一个 target 变量 (应当作成员变量吧),使用者可以把其他对象的变 assign 至这个变量。这 assignment 有两种方法实现,其一是利用 Unity 的 GUI 工具把一个 Component 实例的变量 (如Transform) drag-and-drop 至这个 Component 实例的 target 变量,而另一个方法是写代码:

var newTarget = GameObject.Find("Cube").transform;
GetComponent(Follow).target = newTarget;

用代码就可以这样动态改变这些 Component 之间的联结方式。或者另一个说法是,GUI 工具是可以设定起始的联结,而 Script 可以在执行期改变这些联结。

渲染

一个可被渲染的 GameObject 需要有以几个 Components,以 Mesh 为例:

  1. MeshFilter: 用来找出现时的 Mesh 对象
  2. MeshRenderer: 用来渲染 Mesh 的 Component,会参考一个 Material 对象

要注要 Mesh 和 Material 对象并非 Component,它们是继承自 Object 的。你可以动态改变它们。但由于它们不是 Component ,所以可以被分享,例如多个 GameObject 的 MeshRenderer 都参考到同一个 Material。一个 Component 实例只属于一个 GameObject (所以在 UML 中我用黑色钻石表示 Composition)。 而 Light 和 Camera 则是 Component,这意未着可以简单的设定联结。

分析

Unity 的 Script 对象模型是以 Component 为基础的。透过把 Component 实例加入 GameObject 实例来组合不同功能的对象,而 Component 实例之间可以建立联结。 这种方式不需要透过继承 (inheritance),而是透过聚合 (aggregation)加入对象的功能和行为。使用聚合的好处是不会产生复杂的继承层阶,亦可以动态改变聚合的结构 (例如在执行期加入或移除 Component)。 有一些细节我暂时未清楚,例如多个 Component 在一个 GameObject 中的执行次序如何设定;联结会否有 cylic 的问题等等。可能要拿到软件再试用才可以知道。

结语

Unity 的脚本系统给我的感觉是使用非常简单。透过很少的代码就能写一些行为,甚至把行为组合到对象中。但是,通常容易的东西都会有相对的缺点,例如在效能上或是 Scalability 上。后者可能是一个很大的问题,当游戏规模扩大,Component 和联结就会变成一个很复杂的 graph,由于连结是发生于执行期 (而非静态),可能要作改动会变得困难。换句话说,就是改几十个类别容易,改它们的几千个 实例就会很困难。 软件设计世界里当然没有银子弹,每个方案都适合不同的情况。我认为 Unity 的一个设计目标是容易使用,就是像 Virtools 之流,可以给没有程式底子的人做游戏,相对来说做比较复杂的项目可能会遇到许多问题。但参考一下总可以给予对事物新的观点,或分析另一个科案的优越之处。


之后还有一篇关于 CryEngine 的脚本分析,但现时我在家里开发的 Mil 引擎主要是采用 Unity 的物件模型。

本文原来是繁体中文,在2008-02-29发表于http://miloyip.seezone.net/?p=15,本文經過修正。