0%

Unity Script 文档阅读

阅读一下这部分文档了解一下 Unity 引擎的运作方式。

Scripting Overview

Creating and Using Scripts

GameObject 的行为被从属于它的若干 Components 所控制。为了实现更强大的功能,我们需要为 GameObject 附加 script component

每个 script 是一个 MonoBehaviour 的派生类,是一个 Component 的蓝图。

其中:

  • Start 函数将在 gameplay 开始之前被调用,用于初始化;
  • Update 函数用来为 GameObject 处理每一帧的变化,但一般来说会将其分成多个不同的阶段,每一阶段进行不同的维护。

Variables and the Inspector

可以在 script 中声明一个 public 的成员,从而可以在 Unity Editor 中的 Inspector 中进行修改,甚至可以在 Play Mode 中动态修改。

Controlling GameObjects using components

可以通过

1
Rigidbody rb = GetComponent<Rigidbody>();

获取 GameObject 自身的其他 Component 实例;可以给一个 GameObject 附加多个 Script Component ,并可以使用这种方式互相获取实例。

此外,我们还可以在某个 GameObject 中获取到其他 GameObject 实例或者它的 Component :

首先是静态方法:

  • 在 Script 中开一个 public 的 GameObject 或者对应的 Component 成员,然后在 Unity Editor 中 drag 过去完成引用。这样在 Script 中就可以直接使用了。

其次是动态方法:

  • 常常用一个 GameObject 管理一组逻辑相同的 GameObject ,如若干个仅仅位置不同的重生点。此时我们将这些重生点作为一个控制 GameObject 的 children 。

    可以看代码感性理解一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class WaypointManager : MonoBehaviour {
    public Transform[] waypoints;

    void Start()
    {
    waypoints = new Transform[transform.childCount];
    int i = 0;

    foreach (Transform t in transform)
    {
    waypoints[i++] = t;
    }
    }
    }
  • 可以通过 Name 或者 Tag 获取任意一个 Scene 中的 GameObject 实例:

    详情参考下面两段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // by name
    GameObject player;
    void Start()
    {
    player = GameObject.Find("MainHeroCharacter");
    }
    // by tag
    GameObject player;
    GameObject[] enemies;
    void Start()
    {
    player = GameObject.FindWithTag("Player");
    enemies = GameObject.FindGameObjectsWithTag("Enemy");
    }

Event Functions

Unity 通过 event functions 与 Script 进行交互。当对应的 event 发生时,Unity 调用 script 中的 event function ,并在返回后将控制权收回。

常用、重要的事件有以下几种:

  • 周期更新事件

    在每一帧绘制之前修改 GameObject 的状态、行为;

    1. Update

      最常用的是 Update . 在每一帧被绘制之前,以及动画被计算之前被调用。

    2. FixedUpdate

      与帧绘制一样,物理引擎也是每隔一段时间更新一下状态,而每次其更新之前会调用 FixedUpdate 函数。

      物理引擎更新与帧绘制的频率并不相同。物理引擎的更新频率会更高一下。

    3. LateUpdate

      已经调用了所有 Script 的 Update, FixedUpdate 更新了对应的 GameObject 的状态,同时所有的动画计算已经完成,在开始正式绘制之前会调用 LateUpdate 。

      一个典型应用场景是更新跟随物体的摄像头,或覆盖动画计算。

  • 初始化事件

    1. Awake

      将在 Scene 载入时对于每个 Script 进行调用

    2. Start

      将在绘制第一帧、或进行第一次物理引擎更新之前对于每个 Script 进行调用

      注意,所有 Script 的 Awake 调用返回后,才会开始调用 Start

  • GUI 事件

    OnGUI ,用来更新 GUI ,用到的时候再说

  • 物理引擎事件

    OnCollisionEnter/Stay/Exit ,OnTriggerEnter/Stay/Exit 这些。

Time and Framerate Management

绘制每一帧的时间取决于 CPU 负载,因此并不是恒定的。我们可以用 Time.deltaTime 来获得上一帧绘制所需的时间。(仔细想一想,这还蛮有意思的,当涉及到多人同步的时候就更有趣了)

然而,Unity 的物理引擎则是以恒定的时间间隔更新的。你可以通过 Time.fixedDeltaTime 来查看并修改它,但默认值通常不必修改。

下面提到有可能物理引擎更新比帧绘制快会导致问题,从而可以让物理引擎跑的慢一点…没太看懂。

我们可以通过设定 Time.timeScale 来改变游戏时间流逝的速度(加急名单警告)。

同时 Unity 还提供了方便的定期自动截图功能。

Creating and Destroying GameObjects

使用 Instantiate 新建 GameObject 。通常我们用 public 的成员将一个 prefab 拖进来用来后续的新建,这样它的各个 Component 也能复制进来。

我们还可以使用 Destroy 来回收一个 GameObject 或 Component 。

Coroutine

即协程。

首先,我们可以将在 Update 中每绘制一帧之前做的更新放在协程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Coroutine
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return null;
}
}

// Start it
void Update()
{
if (Input.GetKeyDown("f"))
{
StartCoroutine("Fade");
}
}

这里 yield 就会将控制权交还给 Unity 。但是下一帧中还会从上一帧中离开的地方继续执行。这样就跟放在 Update 中一样每帧更新一次了。

对于那些定期更新但不能过于频繁如每帧更新一次的任务而言,我们也可以放在协程中,将 yield 语句变为:

1
yield return new WaitForSeconds(.1f);

Namespace

当类的名字容易产生冲突时,我们可以用名字空间 namespace 进行管理。这样添加脚本的方式似乎也与之前略有差别。等到项目真到了那种大小再说吧。

Attributes

与 C# 一样可以加一些 Attributes 。不过现在还没有什么值得注意的地方。

后面的部分都有些过于复杂了,目前可能还用不到,就先不看了。

C# Job System

如果我们仅仅使用线程池来维护,给每个任务分配一个新线程,在 Unity 中同时 Ready 的线程会轻易超出 CPU 的硬件线程数,从而导致频繁的切换线程,带来不菲的上下文切换开销。

因此使用 Job System ,一个 Job System 管理若干 worker threads ,每个 worker thread 对应 CPU 的一个硬件线程,从而不会频繁切换线程。同时,我们使用 job queue 来维护所有的 job ,而 worker thread 从 job queue 中取出 job 来执行。

我们可以使用 NativeContainer 来存放主线程与 Job 共享的数据,然而文档中并没有说它是线程安全的。