游戏中的脚本

思考并回答以下问题:

  • 什么是协程?怎么创建协程?
  • 协程在什么情况下会自动停止运行?如何手动关闭协程?
  • 大部分函数在完成工作后就立即返回怎么理解?
  • IEnumerator是什么?
  • 写一个让cube一直运行的协程
  • 实例化一个对象意味着复制该对象是什么意思?使用什么函数?
  • 什么是特性?可以附加到什么上?
  • 特性可改变类的行为和类在编辑器中呈现的方式怎么理解?
  • RequireComponent怎么理解?如果没有该组件呢?
  • Header和Space是什么作用?
  • SerializeField和HideInInspector是为了解决什么问题?
  • ExecuteInEditMode是干嘛用的?

本章涵盖:

  • 1.协程
  • 2.创建和销毁对象
  • 3.特性

Unity提供了必要的基础功能,例如渲染图形、从玩家那里获取输入,以及播放音频。你需要做的就是添加自己的游戏所需要的功能。怎么做呢?编写脚本,并将其添加到游戏的对象中。

协程

大部分函数在完成工作后就立即返回。然而,有些时候需要让一些工作用些时间逐渐完成。例如,如果想让一个对象从一个位置滑动到另一个位置,就需要让这种移动发生在多个帧中。

在多个帧中运行的函数称为协程。要创建协程,首先需要创建一个返回类型为IEnumerator的方法:

1
2
3
4
IEnumerator MoveObject()
{

}

接下来,使用yield return语句让协程临时停止运行,使游戏的其余部分能够继续执行。例如,要使一个对象在每一帧中向前移动一定距离,可以使用下面的代码:

1
2
3
4
5
6
7
8
9
IEnumerator MoveObject()
{
// 无限循环下去
while(true)
{
transform.Translate(0, 1, 0); // 每一帧在y轴上移动一个单位
yield return null; // 等待进入下一帧
}
}

如果包含一个无限循环(例如上例中的while(true)),那么在循环期间必须使用yield让出执行权。否则,循环将一直执行,其他代码将没有机会执行其他工作。由于游戏的代码运行在Unity内,如果进入无限循环,可能导致Unity不能响应。如果发生这种情况,需要强行关闭Unity,而这可能导致丢失未保存的工作。

从一个协程yield return时,将暂时停止执行该函数。Unity将在以后恢复执行该函数,具体何时恢复执行取决于使用什么值yield return。

例如:

1
2
3
4
5
6
7
8
9
// 等到下一帧恢复执行
yield return null

// 等待3秒后恢复执行
yield return new WaitForSeconds(3)


// 等待someVariable等于true时执行;在这里可以使用任何计算结果为true或false变量的表达式
yield return new WaitUntil(()=>this.someVariable == true)

要停止执行协程,需要使用yield break语句:

1
2
// 立即停止此协程
yield break;

当执行到方法末尾时,协程也会自动停止执行。

有了协程函数后,就可以启动该函数。要启动一个协程,不能单独进行调用,而要使用StartCoroutine函数进行调用:

1
StartCoroutine(MoveObject());

启动协程后,它将开始执行,直到遇到yield break语句,或者到达函数末尾。

除了刚刚提到的yield return示例,还可以yield return另一个协程。这意味着该协程将等待另一个协程结束。

在协程外部也可以停止一个协程。要使用这种方法,需要保留对StartCoroutine方法的返回值的一个引用,并将其传递给StopCoroutine方法:

1
2
3
Coroutine myCoroutine = StartCoroutine(MyCoroutine());
// ...后面...
StopCoroutine(myCoroutine);

创建和销毁对象

在游戏运行期间,有两种方法可创建对象。第一种方法是创建一个空的GameObject,然后使用代码给该GameObject附件组件;第二种方法可在一行代码中完成设置。

在Play Mode中创建新对象时,这些对象将在停止游戏后消失。如果想要这些对象保留下来,需要执行以下步骤。

  • (1)选择想要保存的对象。
  • (2)按Ctrl+C键复制这些对象。
  • (3)退出Play Mode。对象将从场景中消失。
  • (4)按Ctrl+V键,粘贴前面复制的对象。对象将重新出现,现在就可以在Edit Mode中操作这些对象了。

实例化

在Unity中,实例化一个对象意味着复制该对象,以及该对象的所有组件、子对象以及子对象的组件。当实例化的对象是一个预设(prefab)时,这一点尤为强大。预设是作为资源保存的预构建对象。这意味着你可以创建对象的一个模板,然后在许多不同的场景中实例化该模板的多个副本。

要实例化一个对象,需要使用Instantiate方法:

1
2
3
4
5
6
7
8
9
public GameObject myPrefab;

void Start()
{
// 创建myPrefab的一个新副本,将其置于与此对象相同的位置
var newObject = (GameObject)Instantiate(myPrefab);

newObject.transform.position = this.transform.position;
}

Instantiate方法的返回类型是Object,而不是GameObject。需要执行转换来将其作为GameObject处理。

从头创建对象

创建对象的另外一种方法是通过代码自己创建。为此,需要使用new关键字构造一个新的GameObject,然后调用该GameObject的AddComponent方法来添加新组件:

1
2
3
4
5
6
7
8
9
// 创建一个新的游戏对象
// 在Hierarchy中,新游戏对象将显示为My New GameObject
var newObject = new GameObject("My New GameObject");

// 向游戏对象添加一个新的SpriteRenderer
var render = newObject.AddComponent<SpriteRender>();

// 告诉新的SpriteRender显示一个精灵
renderer.sprite = myAwesomeSprite;

AddComponent方法接受一个泛型参数,即想要添加的组件类型。在这里可以指定Component类的任意子类,该组件将被添加到GameObject中。

销毁对象

Destroy方法从场景中移除对象。注意,这里的用词是“对象”(object),而不是“游戏对象”(game object)。Destroy方法既可以移除游戏对象,又可以移除组件。

要从场景中移除游戏对象,需要对该对象调用Destroy方法:

1
2
// 销毁此脚本关联到的游戏对象
Destroy(this.gameObject);

Destroy方法可操作组件和游戏对象。
如果在调用Destroy方法时传入this(代表当前的脚本组件),并不会移除游戏对象;相反,脚本将从所附的游戏对象上移除。游戏对象仍将存在,但是不再附有你的脚本。

特性

特性(attribute)是可以附加到类、变量或方法上的一条信息。Unity定义了几种有用的特性,可改变类的行为或者类在编辑器中呈现的方式。

1.RequireComponent

当附加到类时,RequireComponent特性允许告知Unity,此脚本要求另外一种类型的组件必须存在。当脚本只有附加了该类型的组件才有意义时,这个特性会很有用。例如,如果脚本只是做一件事,比如修改Animator的设置,那么类要求必须存在一个Animator是很合理的。为了指定某个组件要求必须存在的组件类型,只需要提供该组件类型作为参数,例如:

1
2
3
4
5
[RequireComponent(typeof(Animator))]
class ClassThatRequireAnimator : MonoBehaviour
{
// 此类要求GameObject上关联一个Animator
}

如果添加的脚本要求GameObject有特定的组件,但是GameObject还没有该组件,那么Unity将自动把该组件添加到GameObject中。

2.Header和Space

把Header特性添加到一个字段时,Unity会在Inspector中在该字段的上方绘制一个标签。Space的工作方式与此类似,只不过是添加一个空行。二者对于组织Inspector的内容都很有用。

1
2
3
4
5
6
7
8
9
10
11
public class Spaceship : MonoBehaviour
{
[Header("Spaceship Info")]
public string name;

public Color color;

[Space]

public int missileCount;
}

显示标签和空行的Inspector

3.SerializeField和HideInInspector

正常情况下,只有公共字段才会显示在Inspector中。但是,将变量声明为public,意味着其他对象能够直接访问它们;这样一来,对象就很难完全控制自己的数据。然而,如果将变量声明为private,Unity就不会在Inspector显示该变量。

为了处理这个问题,当想要在Inspector中显示一个私有变量的时候,可以向该变量添加SerializeField特性。

如果想要获得相反的行为(即,变量是public变量,但是不显示在Inspector中),那么可以使用HideInInspector特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Monster : MonoBehaviour
{
// 由于是公有的,所以会显示在Inspector中
// 可在其他脚本中访问
public int hitPoints;

// 由于是私有的,所以不会显示在Inspector中
// 在其他脚本中无法访问
private bool isAlive;

// 由于设置了SerializeField,所以会显示在Inspector中
// 在其他脚本中无法访问
[SerializeField]
private int magicPoints;

// 由于设置了HideInInspector,所以不会显示在Inspector中
// 可在其他脚本中访问
[HideInInspector]
public bool isHostileToPlayer;
}

4.ExecuteInEditMode

默认情况下,脚本只在Play Mode中运行自己的代码。也就是说,只有当游戏实际运行的时候,Update方法才会运行。

但是,有时候让代码一直运行会很方便。这种情况下,可以向类添加ExecuteInEditMode特性。

组件的生命周期在Edit Mode下和在Play Mode下有所不同。在Edit Mode下,Unity只在必要的时候重绘,通常是为了响应用户输入事件(如鼠标单击)。这意味着Update方法只是间或运行,而不是连续运行。另外,协程的行为会与你的预期不同。

另外,在Edit Mode下不能调用Destroy,因为Unity的行为是推迟到下一帧才实际移除对象。在Edit Mode下,应该调用DestroyImmediate,该方法将立即移除对象。

例如,下面的脚本使一个对象始终面向其目标,即使当前不再Play Mode下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ExecuteInEditMode]
class LookAtTarget : MonoBehaviour
{
public Transform target;

void Update()
{
// 如果没有目标,就不继续执行
if (target != null)
{
return;
}
//转动以面对目标
transform.LookAt(target);
}
}

如果将这个脚本附加到一个对象,并将其target变量设为另外一个对象,那么不管是在Play Mode还是Edit Mode下,第一个对象将转动自身来面向其目标。

0%