1.5 Unity脚本基础
编写Unity的脚本,无论是使用C#或Java Script,除了要注意语言自身的语法规则,还需要注意Unity开发环境的特性。
1.5.1 Script(脚本)组件
在Unity中,最基础的游戏单位称为Game Object(游戏体),一个基本的Game Object仅包括一个Transform组件,能够对其进行位移、旋转和缩放,此外没有任何其他功能。每个Game Object上可以加载不同的Component(组件),这个组件可能是一个贴图,一个模型或一个脚本,当加载了不同的组件后,Game Object将根据组件的组成产生不同的功能。
为了能运行脚本,最基本的要求是将脚本指定给一个Game Object作为它的Script(脚本)组件,最简单的方式是直接将脚本拖动到Game Object的Inspector窗口的空白位置,或者在菜单栏中选择【Component】,然后选择需要的脚本。
所有的Unity运行时,脚本默认必须继承自基类MonoBehavior,如果脚本不是继承自MonoBehavior,则无法将这个脚本作为组件运行,但允许在其他脚本中调用。
1.5.2 脚本的执行顺序
在Unity中编写脚本,没有一个默认的Main函数作为程序入口。所有继承自MonoBehavior的脚本都可以独立运行。另外,继承自MonoBehavior的脚本也没有构造函数,但MonoBehavior提供了系统事件函数来响应不同的事件,比如脚本开始运行、更新等。一些常用的函数如下:
// 实例化后最先执行的函数,只会执行一次 void Awake() { Debug.Log("Awake"); } // 当前脚本可用后会执行一次,因为可以关闭脚本,因此可以反复执行,在Awake之后执行 void OnEnable() { Debug.Log("OnEnable"); } // 在进入Update函数之前执行的函数,只会执行一次,在OnEnable之后执行 void Start () { Debug.Log("Start"); } // 程序主循环,每帧执行 void Update () { }
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-18所示。
图1-18 序列化脚本
默认只有继承自MonoBehaviour的脚本才能序列化。如果是一个普通的C#类,需要使用添加System.Serializable属性才能序列化,如下所示。
[System.Serializable] public class PlayerData { public int id = 0; public string m_name = "default"; public float m_speed = 0; public Camera m_camera; public List<string> items; } public class Player : MonoBehaviour { public PlayerData m_data; }
现在,所有的序列化变量都放到了m_data中,如图1-19所示
图1-19 序列化脚本
Unity只能序列化public类型的变量,且不能序列化属性。Unity中的很多功能都可以通过序列化在场景中直接编辑,甚至不用编写代码即能实现,优点是节省代码,便于修改,缺点是依赖场景中的设置,比较难以整体控制。
1.5.4 组件式的编程
在Unity中编写程序,无论是使用C#或JavaScript语言,它们都支持面向对象编程,Unity的脚本是基于组件形式加载到游戏对象上,当我们需要重载函数时,继承并不是唯一的解决方案。在下面的示例代码中,TestScript定义了一个虚函数DoSomething,传统的方式,我们可以使用继承重载DoSomething函数实现不同的功能。
public class TestScript : MonoBehaviour { void Start () { this.DoSomething(); } public virtual void DoSomething() { Debug.Log("DoSomething"); }
如果有很多对象,且每个对象都需要一个不同版本的DoSomething函数,我们也可以使用组件的方式实现函数的重载。
在下面的示例中,创建了两个类,将这两个类都加载到同一个Game Object上。TestScript函数使用SendMessage函数调用了游戏对象上的DoSomething函数,注意,这个函数是定义在另一个类中,如果需要不同版本的DoSomething函数,只需要创建多个不同版本的DoSomething函数即可。使用这种方式的好处是可以使功能更加模块化。
public class TestScript : MonoBehaviour { void Start () { this.gameObject.SendMessage("DoSomething"); } } public class DoSomethingScript : MonoBehaviour { public void DoSomething() { Debug.Log("DoSomething"); } }
需要注意的是,SendMessage函数的效率较低,追踪错误也比较困难。在下面的示例中,我们混合了继承和组件的特性,首先创建了一个名为DoSomethingBase的抽象类,DoSomethingScript继承DoSomethingBase, TestScript可以直接引用DoSomethingScript中的DoSomething函数。
public class TestScript : MonoBehaviour { void Start () { // GetComponent获取其他组件 this.gameObject.GetComponent<DoSomethingBase>().DoSomething(); } } public abstract class DoSomethingBase : MonoBehaviour{ public abstract void DoSomething(); } public class DoSomethingScript : DoSomethingBase{ public override void DoSomething(){ Debug.Log("DoSomething"); } }
1.5.5 协程编程
在游戏的逻辑中,经常会遇到先完成某个任务,等待一定时间后,再去做某个任务,又或者在提交一个请求后,需要等待请求返回结果再执行后面的代码。Unity提供了一种称为协程的编程方式,它允许在不堵塞主线程的情况下,在协程函数中堵塞或循环,听起来有些类似多线程,但注意协程不是多线程,只是方便了程序编写。
协程函数需要使用关键字IEnumerator定义,并一定要使用关键字yield返回。协程函数不能直接调用,需要使用函数StartCoroutine将协程函数作为参数传入,下面是一段示例代码:
using System.Collections; // 使用协程需要这个名称域 using UnityEngine; public class CoroTest: MonoBehaviour// 协程必须运行在MonoBehaviour对象中 { void Start () { Coroutine coro=StartCoroutine(DoSomethingDelay(1.5f)); // 必须使用StartCoroutine执行协程 //Destroy(this.gameObject); 如果删除了当前游戏体,协程也会随着消失 //StopAllCoroutines(); // 停止所有运行于当前MonoBehaviour的协程 //StopCoroutine(coro); // 停止指定的协程 StartCoroutine(RunLoop()); } // 定义协程的函数体 IEnumerator DoSomethingDelay(float sec) { yield return new WaitForSeconds(sec); // 等待 Debug.Log("运行"); //if () // yield break; // 满足条件中断协程 //yield return new WaitForSeconds(sec); // 可以多次等待 //StartCoroutine(DoSomethingDelay(1.5f)); // 可以一直循环执行下去 } IEnumerator RunLoop() // 在协程中创建循环 { while(true) // 可以在循环中设置中断条件 { Debug.Log("循环中"); yield return 0; // 没有等待,立即返回 } } }