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

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;  // 没有等待,立即返回
              }
            }
        }