Event函数

unity的脚本不像传统的代码,会一直在一个循环中运行直到完成自己的工作。与此相反,unity通过调用脚本内特定的函数将控制权间歇地交给脚本。一旦一个函数执行结束,控制权会回到 unity那。这些函数就是 event函数,因为它们是在 unity为响应 gameplay期间发生的 events而被激活的。Unity使用命名机制来识别哪个函数是用来相应 event的。比如说,你已经见过 Update函数和 Start函数了。unity内有更多的 event函数。下面我将介绍一些最常见且重要的 events。

Update函数

一个游戏就像是一个动画,动画帧是动态生成的。游戏中关键的概念就是在帧被渲染时改变物体在游戏里的位置、状态和行为。Update函数就是做这个的。Update函数在帧被渲染和动画被计算前被调用。

void Update() {
    float distance = speed * Time.deltaTime * Input.GetAxis("Horizontal");
    transform.Translate(Vector3.right * distance);
}

物理引擎也会像帧渲染那样在离散时间内更新。在物理更新时,FixedUpdate会被调用。因为物理和帧更新不会以相同的频率更新,所以如果你把它放在 FixingedUpdate函数会获得获得更精确的结果。

void FixedUpdate() {
    Vector3 force = transform.forward * driveForce * Input.GetAxis("Vertical");
    rigidbody.AddForce(force);
}

GUI events

Unity有一个系统可以在场景中的主要动作上渲染GUI控件以及回应这些控件的点击。这个代码的处理方式与正常的帧更新不同,因此它应该被放在 OnGUI函数里,这个函数会定期执行。

void OnGUI() {
    GUI.Label(labelRect, "Game Over");
}

创建和销毁 GameObject

一些游戏在场景中拥有固定数量的物体,但是对于文字、宝物和其他的物体来说,在 gameplay时产生和移除是十分常见的。在 unity中,一个 GameObject能使用 Instantiate函数生成,这个函数能copy一个已经存在的物体。

public GameObject enemy;

void Start() {
    for (int i = 0; i < 5; i++) {
        Instantiate(enemy);
    }
}

实例化一个 GameObject会 copy原始物体的所有组件。

Destroy函数将在帧更新结束或在短暂时间(可选地)销毁一个物体。

void OnCollisionEnter(Collision otherObj) {
    if (otherObj.gameObject.tag == "Missile") {
        Destroy(gameObject,.5f);
    }
}

注意 Destroy函数可以摧毁单个组件而不影响到 GameObject本身。一个常见的错误如下:

Destroy(this);

这样做实际上只会销毁这个调用这个函数的脚本组件而不是与这个脚本关联的 GameObject。

Coroutines

当你调用函数时,它会在返回前运行结束。这意味着在一帧更新内,函数里的动作都必须要发生;一个函数调用不能被用来包含一个程序式动画或一段时间的一系列 events。比如说,有个逐步减少物体alpha(不透明度)值直到它变得完全不可见的任务。

void Fade() 
{
    for (float ft = 1f; ft >= 0; ft -= 0.1f) 
    {
        Color c = renderer.material.color;
        c.a = ft;
        renderer.material.color = c;
    }
}

就像上面写的,Fade函数并不会带来你想要的效果。为了让物体褪色的物体变得可见,alpha值必须在一系列帧上降低,以使中间值能被渲染出来。然而,这个函数将会在一个帧更新中执行结束。中间值并不会被看到且物体会立即消失。
为了处理这样的情形,你可以在 Update函数中添加代码来执行帧与帧之间物体的褪色。然而,对于这种任务,使用 coroutine是最方便的。
coroutine就想一个能暂停执行的函数,且能将控制权返回给 unity,之后会继续在接下来的帧中继续执行直到结束。在 C#中,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;
    }
}

Coroutine函数的返回类型为 IEnumerator,且带有一个 yeild返回语句。yield return null这一行是暂停执行且在接下来的帧重新开始的地方。想让 coroutine运行,你可以使用 StartCoroutine函数。

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

可以看到在 Fade函数的循环计数在 coroutine生存周期中保持着一个正确的值。事实上,任何变量和参数将会正确地保存在 yields。
Coroutine默认在它的 yield后面的帧重新开始,但是你仍可以使用 WaitForSeconds来延迟。

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 new WaitForSeconds(.1f);
    }
}

这可以作为在一段时间内传播影响的方式,且是一种有效的优化方式。一个游戏里的许多任务需要定期执行,最常见的是在 Update函数里添加这些任务。然而,这个函数将会在一秒内被调用许多次。当一个任务不需要被频繁重复时,你可以把它放在 coroutine中,来获得周期性更新而不是每帧更新。比如说你需要警告玩家敌人就在附近,代码可以这样写:

function ProximityCheck() 
{
    for (int i = 0; i < enemies.Length; i++)
    {
        if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
                return true;
        }
    }
    
    return false;
}

如果附近有许多敌人,那么每帧都会调用这个函数会有许多开销。因此,你可以使用 coroutine来每秒调用十次此函数。

IEnumerator DoCheck() 
{
    for(;;) 
    {
        ProximityCheck();
        yield return new WaitForSeconds(.1f);
    }
}

这将会显著减少检查的次数,且不会对 gameplay有任何明显的影响。
注:你可以使用 StopCoroutine和 StopAllCoroutines来停止 Coroutine。Coroutines也会在与它关联的 GameObject不可用(SetActive(false))时停止。调用 Destroy(example) 会马上触发 OnDisable并处理 coroutine,从而有效地终止它。最后,OnDestroy会在帧结束时被唤醒。