Unity 3D\2D手机游戏开发:从学习到产品(第4版)
上QQ阅读APP看书,第一时间看更新

1.5 Unity脚本基础

编写Unity的脚本,除了要注意语言自身的语法规则,还需要注意Unity开发环境的特性。这里再次强调,如果读者没有编程基础或从未接触过C#语言,建议先参考本书附录C#部分进行学习,不过阅读本章可以对Unity有一个大体了解。

1.5.1 Script(脚本)组件

在Unity中,最基础的游戏单位称为Game Object(游戏体),一个基本的Game Object仅包括一个Transform组件,用于对其进行位移、旋转和缩放。每个Game Object上可以加载不同的Component(组件),这个组件可能是一张图片,一个模型或一个脚本,当加载了不同的组件后,Game Object将根据相应组件产生不同的功能,如图1-21所示。

图1-21 Unity的组件系统

为了能运行脚本,最基本的要求是将脚本指定给一个Game Object作为它的Script(脚本)组件,最简单的方式是直接将脚本拖动到Game Object的Inspector窗口的空白位置,或者在菜单栏中选择【Component】,然后选择需要的脚本。

Unity脚本有几个最重要的类,它们是MonoBehaviour、Transform和Rigidbody/Rigidbody2D。

MonoBehavior是所有Unity脚本的基类,MonoBehaviour提供了大部分的Unity功能。如果脚本不是继承自MonoBehavior,则无法将这个脚本作为组件运行。

Transform类是每个Game Object都包括的默认组件,它提供了位置变换、旋转、缩放、子父物体连接等功能。

Rigidbody类(或2D版本Rigidbody2D)提供所有的物理功能。

1.5.2 脚本的执行顺序

在Unity中编写脚本,没有一个默认的Main函数作为程序入口,所有继承自MonoBehavior的脚本都可以独立运行。另外,继承自MonoBehavior的脚本通常也不需要构造函数,MonoBehavior提供了回调函数来响应不同的事件,比如脚本开始运行、更新等。一些常用的函数如下:

Unity的其他事件函数还有很多,详细可以查阅文档。

1.5.3 脚本的序列化

大部分游戏都会有一套“配置”系统,比如使用配置文件定义游戏中的角色生命值、移动速度数值。使用Unity,一些简单的配置数值可以直接在脚本中暴露出来,然后在编辑器中进行设置。在下面的代码中,我们创建了一个名为Player的类,它的成员都是public类型的:

     public class Player : MonoBehaviour {
     public int id = 0;
        public string m_name = "default";
        public float m_speed = 0;
        public Camera m_camera;
        public List<string> items;
// ...

将这个脚本指定给场景中的任意一个游戏体,然后就可以在Inspector窗口中配置Player实例的public成员变量初始值了,为了显示方便,Unity会在编辑器中自动去掉前缀m_,如图1-22所示。

图1-22 序列化脚本

默认只有继承自MonoBehaviour的脚本才能序列化。如果是一个普通的C#类,需要使用添加System.Serializable属性才能序列化,如下所示。

现在,所有的序列化变量都放到了m_data中,如图1-23所示

Unity只能序列化public类型的变量,且不能序列化属性。Unity中的很多功能都可以通过序列化在编辑器中直接编辑,优点是节省代码,便于修改,缺点是依赖场景中的设置,比较难以整体控制。

图1-23 序列化脚本

1.5.4 组件式编程

在Unity中编写程序,主要是采取面向对象编程方式进行编程,传统的面向对象编程有三要素,即封装、继承和多态。在Unity中,因为脚本是基于组件形式加载到游戏对象上,当我们需要为一个类扩展功能时,继承可能并不是唯一的解决方案。

我们可以将不同的功能看作是不同的组件,通过组合的方式实现功能的扩展和变化。举个例子,比如我们有一个脚本执行A任务,另一个脚本执行B任务,同时将两个脚本加载到同一个游戏对象上,该游戏对象即可以同时执行A和B任务。如果我们将来还需要同时执行C任务,只需要再添加一个执行C任务的脚本即可。

1.组件的获取

在Unity中编写代码的过程中,通常会有需求在一个脚本中调用另一个组件的功能,下面的代码示例演示了如何在脚本中获取、添加组件。

2.使用Unity消息机制在组件间通信

在本例中,创建了两个类TestScript和DoSomethingScript,将这两个类都作为脚本组件添加到同一个Game Object上。TestScript类在Start中调用SendMessage函数,通过字符串名称调用另一个类的DoSomething函数。使用这种方式的好处是TestScript可以不知道DoSomethingScript,很明显,任何一个带有DoSomething函数的类都可以与TestScript类组合使用。

需要注意的是,SendMessage函数的效率较低,使用字符串调用函数也不利于追踪错误。

3.继承和组合

在下面的另一个示例中,我们混合了继承和组件的特性,首先创建了一个名为DoSomethingBase的抽象类,DoSomethingScript继承DoSomethingBase,TestScript可以通过GetComponent函数取得DoSomethingScript组件,然后再执行它的DoSomething函数。

1.5.5 协程编程

在游戏的逻辑中,经常会遇到先完成某个任务,等待一定时间后,再去做另一个任务的情况,Unity提供了一种称为协程的编程方式,它允许在不堵塞主线程运行的情况下,异步等待执行某些特定代码,听起来有些类似多线程,但协程不是多线程。

协程函数需要使用关键字IEnumerator定义,并一定要使用关键字yield返回。协程函数不能直接调用,需要使用函数StartCoroutine将协程函数作为参数传入,下面是一段示例代码,包括了大部分协程的应用方法:

在Unity中,协程是一种重要的编程技巧,但对于初学者来说,协程代码可能难以理解,在后面的章节,我们会通过一些具体实例演示如何使用协程。实际上,协程是通过C#迭代器特性实现的一种编程技巧,读者可以参考本书附录C#的迭代器部分了解更多关于协程实现的原理。