Unity 3D脚本编程与游戏开发
上QQ阅读APP看书,第一时间看更新

1.1 控制物体的运动

仅通过控制物体的位置,就能做出好玩的小游戏。本节将详细讲解创建脚本、改变物体位置和处理用户输入等基本操作,并对容易产生误解的地方做出提示。

1.1.1 新建脚本

首先在场景中新建一个球体,接着新建脚本并挂载到该球体上。

新建脚本有两种方法。第一种方法是在Project(工程)窗口的某个文件夹中,如在Assets(资源根目录)中单击鼠标右键,选择Create→C# Script选项,如图1-1所示。这时Unity会在该文件夹下建立一个扩展名为.cs的脚本文件(扩展名在Unity编辑器中是隐藏的),并立即让用户为其指定一个名称,默认为NewScript。

55875-00-011-1

图1-1 创建C#脚本

第二种方法是选中球体,在Inspector(检视)窗口中单击Add Component(添加组件)按钮,在菜单中选择New script(新建脚本)选项,再输入文件名,最后单击Create and Add(创建并添加)按钮,则会在Assets下新建指定名称的脚本文件,如图1-2所示。

55875-00-011-2

图1-2 直接创建脚本Ball的方法

如果采用第一种方法,那么创建的脚本文件就还未挂载到球体上。如果采用第二种方法,则在创建完成的同时脚本文件也挂载完成了。

将脚本文件挂载到球体上也有两种方法:一是拖曳Project窗口中的脚本文件到场景中的球体上,二是拖曳Project窗口中的脚本文件到Inspector窗口里所有项目的后面。推荐使用第二种方法,因为它更为直观。

将脚本文件拖曳到Inspector窗口时会出现一条蓝色标记线,代表插入的位置,如图1-3所示。一般来说脚本组件的顺序不是很重要,可以插入到任意两个组件之间或者最后。

55875-00-012-1

图1-3 插入脚本组件的操作

当不小心挂载了多个同样的脚本组件时,在组件名称上单击鼠标右键,并选择Remove Component(删除组件)选项即可删除多余的组件,如图1-4所示。

55875-00-012-2

图1-4 删除组件的操作

小提示

创建脚本组件的注意事项

Unity规定,能够挂载到物体上的脚本文件必须是“脚本组件”(另有一种不是组件的脚本文件),脚本组件要继承自MonoBehaviour,且脚本代码中的class名称必须与文件名一致。一般脚本创建时会自动生成这部分内容,但是如果修改了脚本文件名,那么在挂载时就会报错。这时就必须修改文件名或class名称,让它们一致,这样才能正确挂载。

Unity支持一个物体挂载多个同样的脚本组件,但一般来说只需要一个。如果由于操作失误挂载了多个脚本组件,就要删除多余的,这也是建议把脚本文件拖曳到Inspector窗口内的原因,这样易于确认是否挂载了多个脚本组件。

1.1.2 Start和Update事件

双击脚本文件,或者在脚本组件上单击鼠标右键,选择Edit Script(编辑脚本),就可以打开脚本文件进行编辑。本书建议使用Visual Studio 2017或Visual Studio 2019作为代码编辑器。在初次安装Unity时会默认安装Visual Studio社区版,该版本可免费使用。

打开脚本文件,可以看到如下内容。

using UnityEngine;

public class Ball : MonoBehaviour {
  // Use this for initialization
  void Start () {
 }

  // Update is called once per frame
  void Update () {
 }
}

这个脚本文件名为Ball,引用了UnityEngine这个命名空间,组件类型为Ball,继承了MonoBehaviour,这些都是自动生成的部分。这里需要关注的是其中的Start()函数和Update()函数。

Start()函数在游戏开始运行的时候执行一次,特别适合进行组件初始化。而Update()函数每帧都会执行,在不同设备上更新的频率有所区别,特别是当系统硬件资源不足时,帧率就会降低,因此Update()函数实际执行的频率是变化的。

Start和Update又被称为“事件”,因为它们分别是在“该组件开始运行”和“更新该组件”这两个事件发生时被调用的。第2章会详细讲解脚本组件的各种事件函数。

这里介绍一个非常常用的方法:Debug.Log()用于向Console(控制台)窗口输出一串信息。下面改动脚本,加上两句输出信息的代码。

using UnityEngine;

public class Ball : MonoBehaviour {
  // Use this for initialization
  void Start () {
        Debug.Log("组件执行开始函数!"); 
 }

  // Update is called once per frame
  void Update () {
        Debug.Log("当前游戏进行时间:" + Time.time);
 }
}

运行游戏,找到Console窗口,如果没有打开,则选择主菜单中的Window→General→Console选项打开,如图1-5所示。

55875-00-013-1

图1-5 让Console窗口显示出来

执行游戏,Console窗口内会产生大量信息,如图1-6所示。

55875-00-013-2

图1-6 Console窗口输出的信息

暂停游戏,查看Console窗口中的信息,发现开始函数只执行了一次,后面所有的“当前游戏进行时间:秒数”都是Update()函数被调用时输出的。如果仔细观察,可以发现时间的流逝不太稳定,特别是在游戏刚启动的时候偏差较大。

Debug.Log()函数是写代码、查bug的好帮手,越是编程高手,越是使用得多。Debug.Log()函数也是最简单、最可靠的一种调试手段,未来还会反复使用这个方法,建议从现在开始就多多应用它。

1.1.3 修改物体位置

在Unity里修改物体位置,实际上就是修改Transform(变换)组件的数据。在Inspector窗口里可以方便地查看和修改Transform组件的位置(Position)、旋转(Rotation)和缩放(Scale)参数,如图1-7所示。

55875-00-014-1

图1-7 查看和修改物体位置

在界面中可以修改的参数在脚本中也能修改,而在脚本中可以修改的参数就不一定会在界面上出现了,因为脚本的功能比界面上展示的功能要多得多。

修改Transform组件中的Position有两种常用方法。一种是使用Translate()函数。

// 物体将沿着自身的右侧方向(X轴正方向也称为向右)前进1.5个单位
transform.Translate(1.5f, 0, 0);

另一种是直接指定新的位置。如果在游戏一开始就把物体放在(1, 2.5, 3)的位置,则只需要修改Start()函数。

void  Start () {  
      transform.position = new Vector3(1, 2.5f, 3);
}

这里可能有两个让人觉得奇怪的地方:“2.5”要写作“2.5f”,设置位置时要用new Vector3(x, y, z)这种写法。

这些主要源于C#语法的规定。直接写2.5会被认为是一个double类型的数,而这里需要的是float类型的数,所以必须加上f后缀。而写“new”的原因是Vector3是一个值类型,而position是一个属性,由于C#中引用和值的原理,因此不能使用“transform.position.y = 2.5f”这种写法直接修改物体的位置。这里的解释可能对初学者来说有点模糊,不过没有关系,可以先记住并使用这个语法,随着C#编程学习的深入自然会明白。

如果要做一个连续的位移动画,只需要让物体每帧都移动一段很小的距离就可以了。也就是说,可以把改变位置的代码写在Update()函数里,但是每帧都要比前一帧多改变一些位置。例如,以下两种方法都可以让物体沿z轴前进。

void Update () {
        transform.Translate(0, 0, 0.1f);
        // 在这个例子中等价于:
        transform.position += new Vector3(0, 0, 0.1f);
 }

以上两句代码只选择写其中一句即可,另一句可以注释掉。运行游戏,观察小球是否持续运动。

小提示

物体位置的两种写法有本质区别

如果深究,这里有两个要点:一是向量的使用和向量加法,二是局部坐标系与世界坐标系之间的区别和联系。

Translate()函数默认为局部坐标系,而修改position的方法是世界坐标系。3D数学基础知识会在后续章节详细说明,这里先把关注点放在位移逻辑上。

前面提到,由于系统繁忙时无法保证稳定的帧率,因此对于上面的写法,如果帧率高,小球移动就快;帧率低,小球移动就慢。我们需要在“每帧移动同样的距离”和“每秒移动同样的距离”之间做出选择。

按游戏开发的常规方法,应当选择“每秒移动同样的距离”。举个例子,如果帧率为60帧/秒时物体每帧移动0.01米,那么帧率只有30帧/秒时就应该每帧移动0.02米,这样才能保证物体移动1秒的距离都是0.6米。这里虽然原理复杂,但实际只需要略作修改即可。

void Update () {
        transform.Translate(0, 0, 5 * Time.deltaTime);
        // 或
        transform.position += new Vector3(0, 0, 5 * Time.deltaTime);
 }

Time.deltaTime表示两帧之间的间隔,如帧率为60帧/秒时这个值为0.0167秒,帧率只有10帧/秒时这个值为0.1秒,用它乘以移动速度就可以抵消帧率变化的影响。由于Time.deltaTime是一个比较小的数,因此速度的数值应适当放大一些。

1.1.4 读取和处理输入

Unity支持多种多样的输入设备,如键盘、鼠标、手柄、摇杆、触摸屏等。很多输入设备有着类似的控制方式,如按键盘上的W键或上箭头键,将手柄的左摇杆向前推,都代表“向上”,用Unity的术语表述则是“沿纵轴(Vertical)向上”。以下代码就可以获取用户当前的纵轴输入和横轴输入。

void Update () {
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");
        Debug.Log("当前输入纵向:"+v + "    " + "横向 :"+h); 
 }

Input.GetAxis()函数的返回值是一个float类型的值,取值范围为-1~1。Unity用这种方法将各种不同的输入方式统一在了一起。

以上代码会输出v和h的数值。运行游戏,然后按键盘上的W、A、S、D键或方向键,查看Console窗口是否正确读取到了输入值。

通过简单的乘法就可以将输入的幅度与物体运动的速度联系起来。

void Update () {
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");
        Debug.Log("当前输入纵向:"+v + "    " + "横向 :"+h);
        transform.Translate(h*10*Time.deltaTime, 0, v*10*Time.deltaTime);
 }

代码解释:先取得当前的纵向、横向输入(取值范围为-1~1),然后将输入值与速度(代码中的数字10)和Time.deltaTime相乘,以控制物体的移动。每帧最小移动距离为0,在60帧时x轴的每帧最大移动幅度是1*10*0.0167,即0.167米,z轴同理。

运行游戏,按W、A、S、D键或方向键,看看效果。

1.1.5 实例:实现一个移动的小球

下面利用上面所讲解的内容,实现一个移动小球的案例,具体操作如下。

01 新建一个球体。

02 调整摄像机的朝向和位置,可以设置为俯视、平视或斜上方视角。

03 为球体创建脚本Ball并挂载,完整代码如下。

using UnityEngine;

public class Ball : MonoBehaviour {

   public float speed = 10;  
void Start () {  
}

  void Update () {
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");
        transform.Translate(h*speed*Time.deltaTime, 0, v*speed*Time.deltaTime);
 }
}

04 运行游戏。单击Game(游戏)窗口,然后按W、A、S、D键或方向键就可以看到球体以一定的速度移动了。运行游戏的效果如图1-8所示。

55875-00-016-1

图1-8 控制小球移动

通过修改Translate()函数的参数,可以使球体沿不同方向移动,如修改为以下代码就能实现沿xy平面移动,而不是沿xz平面移动。

transform.Translate(h*speed*Time.deltaTime, v*speed*Time.deltaTime, 0);

用这种写法修改速度非常方便,不需要反复修改代码。因为speed是一个public字段(C#中类的成员变量称为字段,两种术语叫法,不影响理解),Unity会为这种字段提供一个快捷的修改方法。在场景中选中球体,在Inspector窗口中查看它的脚本组件,如图1-9所示。

55875-00-016-2

图1-9 脚本中的速度变量显示在面板中

读者会发现这里出现了一个Speed变量,值为10,这就是脚本中的public float speed = 10所带来的效果。由于在真实的游戏开发过程中有很多参数需要设计师反复细致调节,因此Unity默认将public字段直接放在界面上,用户可以随时修改速度,并且被修改的参数会在下一次Update()函数执行时立即生效,也就是说在游戏运行时也可以随时修改参数并生效。

唯一需要注意的是,如果游戏运行前Speed的值设定为20,运行时改为了30,那么停止运行游戏后,Speed的值会回到20。运行时的改动都不会被自动保存,需要在调整时记下合适的数字,停止运行后手动修改。将参数显示到编辑器中还有更多技巧,将在第2章继续讨论。