第 3 章Chapter 3 脚本开发基 础 脚本是一款游戏的灵魂,Unity引擎脚本用来界定用户在游戏中的行为,是游戏制作中 不可或缺的一部分。它能实现各个文本的数据交互并监控游戏的运行状态。以往,Unity 引擎主要支持三种语言:C# 、JavaScript以及Boo。但是选择Boo作为开发语言的使用者 非常少,而Unity公司还需要投入大量资源支持它,这显然非常浪费。所以,在Unt0 iy5. 后,Unity公司放弃了对Boo语言的技术支持,在Unity2017之后又摒弃了JavaScript。本 章主要以C#语言为例讲解Unity引擎脚本的创建、链接方法及脚本编写注意事项,为后续 复杂游戏脚本开发打下基础。 3.1 脚本概述 Uniy引擎的C#和微软.t家族中的C#是同一个语言,语言本身是差不多的。但 Uniy引擎的C#是运行在Moo平台上的, Ne有一些针 tNe tn微软的C#则是运行在.t平台上, 对Windows平台的专用C#类库可能无法在Unity引擎中使用。因此,编写Unity引擎脚 本,除了要注意语言自身的语法规则外,还要注意Unity引擎开发环境的特性。 为了能运行脚本,最基本的要求是将脚本指定给一个GameObject作为它的脚本组件, 最简单的方法是将脚本直接拖到GameObject对象的Inspector视图的空白位置,或者在 Hierarchy视图中选中GameObject对象,将脚本拖向Hierarchy视图中的GameObject对 象,即可完成脚本组件的添加。 Unity引擎脚本有几个最重要的类,它们是MonoBehaviour、Transform和Rigidbody/ Rigidbody2D 。MonoBehavior是所有Unity脚本的基类,提供了大部分的Unity功能。如 果脚本不是继承自MonoBehavior,则无法将这个脚本作为组件运行。Transform类是每个 GameObject都包括的默认组件,它提供了位置变换、旋转、缩放、父物体连接等功能。 Rigidbody(2D版本为Rigidbody2D)类则主要提供物理功能。 3.2 脚本编写 2.创建脚本 3.1 选择Unity引擎菜单栏中的Asets→Create→C#Script命令,创建一个空白脚本,将 其命名为Mov如图3. e, 1所示。 65 图3.1 创建一个空白脚本 在Project视图中双击Move,打开脚本,进行脚本编写。在Update()函数中插入如下 代码。函数内的每一帧代码,系统都会去执行。 using UnityEngine; using System.Collections; public class Move : MonoBehaviour { void Update () { transform.Translate (Input.GetAxis ("Horizontal"), 0, Input.GetAxis ("Vertical")); } } 其中,Input.GetAxis()函数返回-1~1的值,在水平轴上,左方向键对应-1,右方向键 对应1。由于目前不需要向上移动摄像机,所以Y 轴的参数为0。选择菜单栏中的Edit→ ProjectSettings→InputManager命令,即可修改映射到水平方向和垂直方向的对应名称和 快捷键,如图3.2所示。 图3.2 InputManager菜单 66 3.2 链接脚本 2. 创建完脚本后,需要将其链接到游戏对象上。在Hierarchy视图中单击需要添加脚本 的MainCamera,然后将脚本直接拖到MainCamera的Inspector视图的空白位置,3所 示,Move脚本就链接到了MainCamera上。 如图3. 图3. 3 脚本链接 3.3 运行脚本 2. 在Game 视图中单击Play按钮进行测试,可以使用键盘上的方向键(水平方向、竖直方 向)移动摄像机,4和图3. 运行效果如图3.5所示。 图3.4 运行测试效果图1 图3. 5 运行测试效果图2 3.4 注意事项 2. 1. 继承自MoBhiur类 Unity所有挂载(n) 到(e) 游(a) 戏(o) 对象上的脚本中包含的类都继承自MonoBehaviour类。 MonoBehaviour类中定义了各种回调方法,例如Start、Update和FixedUpdate等。在 Unity中创建C# 脚本,系统模板已经包含了必要的定义,6所示。 如图3. 67 图3.iy中创建C# 脚本 6 在Unt 2. 使用Awke或Strt函数初始化 在Unity中,C(a) #中用于初始化的脚本代码必须置于Awake() 或Start() 方法中。 Awake() 和Start() 的不同之处在于Awake() 方法是在加载场景时运行,Start() 方法是在 第一次调用Update() 或FixedUpdate() 方法之前调用。 3. 类名字必须匹配文件名 在Unity中,C# 脚本中的类名必须和文件名相同,否则当脚本挂载到游戏对象时,控 制台会报错。 4. 只有满足特定情况变量才能显示在Inspector视图中 在Unity中,C# 脚本只有公有的成员变量才能显示在Inspector视图中,而private和 protected类型的成员变量不能显示。如果属性项要在Inspector视图中显示,必须是public 类型的。 5. 尽量避免使用构造函数 在Unity中,C# 脚本不需要在构造函数中初始化任何变量,而是使用Awake或Start 方法来实现。在单一模式下使用构造函数可能会导致严重后果,因为它把普通类构造函数 封装了,这些构造函数主要用于初始化脚本和内部变量值,这些初始化有随机性,容易引发 引用异常。因此,一般情况下尽量避免使用构造函数。 3.3 脚本开发实践项目 3.移动的立方体 3.1 1. 项目构思 在脚本环境测试的实践项目中,需要通过脚本的编写、编译、链接过程实现一个简单的 68 游戏场景中走动的效果。本项目旨在通过脚本环境编译测试结果使读者熟悉Unity引擎脚 本开发环境,为后续程序编写打下基础。 2. 项目设计 本项目计划在Unity引擎内创建一个简单的Cube模型,通过键盘的方向按键控制 Cube模型的上、下、左、右移动,并能与鼠标交互实现Cube模型复制效果,7所示。 如图3. 图3. 7 简单场景搭建 3. 项目实施 第1步:双击UnityHub图标,并设置其名称以及存储路径,单击右上角“新建”按钮即 生成一个新项目,如图3. 8所示。 图3. 8 新建项目 第2步:选择菜单栏中的GameObject→3DObject→Plane命令,在游戏场景中创建一 个Plane作为地面,9所示。 如图3. 第3步:选择菜单栏中的GameObject→3DObject→Cube命令,将它放置在plane的 中心位置,如图3. 10 所示。 69 图3.9 创建Plane 图3.10 创建Cube 第4步:接下来创建一个空脚本,选择菜单中的Assets→Create→C# Script命令,并 在项目视图中重命名为Move。 第5步:双击Move脚本,输入如下代码。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Move : MonoBehaviour {v oid Update() { transform.Translate(Input.GetAxis("Horizontal"), 0, Input.GetAxis ("Vertical")); } } 70 Update()函数在渲染一帧之前被调用,这里是大部分游戏行为代码被调用的地方。在 脚本中,为了移动一个游戏对象,需要用transform 来更改它的位置,Translate函数有x、y 和z共3个参数。 第6步:保存脚本(快捷键为Ctrl+S)。 第7步:将脚本与主摄像机相连,即将脚本拖到Hierarchy视图中的MainCamera对 象上,这时脚本与场景中的摄像机产生了关联。 第8步:单击Play按钮测试,发现通过键盘方向键可以在场景中移动摄像机,但是速 度稍快,并且速度不能改变。 第9步:更新代码,内容如下。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Move : MonoBehaviour { public float speed=5.0f; void Update() { float x=Input.GetAxis("Horizontal") * Time.deltaTime * speed; float z=Input.GetAxis("Vertical") * Time.deltaTime * speed; transform.Translate(x, 0, z); } } 位于Update()函数上面的这个速度变量speed 是一个public变量,它会显示在 Inspector视图中,可以调整它的值,便于测试。 第10步:增加新的功能,实现单击时在摄像机当前位置创建Cube游戏对象,创建C# 脚本,将其命名为Create,并将脚本链接到MainCamera上,如图3.11所示。 图3.11 脚本链接 71 第11步:输入代码,如下所示。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Create : MonoBehaviour { public Transform newObject; void Update() { if (Input.GetButtonDown("Fire1")) { Instantiate(newObject, transform.position, transform.rotation); } } } 第12步:创建预制体。 Unity2019新版本采用直接的预制体创建方式,直接将游戏对象从Hierarchy视图拖 到Project视图,即可完成预制体的创建。具体实现时,将Hierarchy视图中制作完成的立 方体Cube拖到Project视图中,重命名为MyCube即可完成预制体内容的制作,如图3.12 所示。 图3.12 关联预制体 第13步:调试。调试是发现和修正代码中人为错误的技巧,Unity提供了Debug类, Log()函数允许用户发送信息到Unity的控制台。当用户单击时,发送一个消息到Unity 控制台,修改脚本如下所示。 using System.Collections; using System.Collections.Generic; 72 using UnityEngine; public class Create : MonoBehaviour { public Transform newObject; void Update() { if (Input.GetButtonDown("Fire1")) { Instantiate(newObject, transform.position, transform.rotation); Debug.Log("Cube created"); } } } 第14步:将MyCube拖入Inspector视图中对Create脚本赋值,如图3.13所示。 4.项目测试 运行游戏并单击,创建一个新的Cube实例,控制台会出现Cubecreated字样,如图3.14 所示。同时场景中创建了新的Cube,如图3.15所示。 图3.13 对Create脚本赋值 图3.14 控制台测试效果 图3.15 运行测试效果 73 3.3.2 创建游戏对象 1.项目构思 游戏场景中对象间的交互都可以通过程序脚本控制并实现。创建游戏对象的方法有3 种:第1种是将物体模型资源由Project视图直接拖到Hierarchy视图中;第2种是在 GameObject下拉菜单中创建Unity自带的游戏对象;第3种是利用脚本编程动态地创建或 删除游戏对象。本项目计划采用第3种方法,即利用脚本编程动态地创建游戏对象。该方 法又分为2种:使用CreatePrimitive方法创建Unity系统自带的基本游戏对象和使用 Instantiate实例方法对预制体进行实例化操作。 调用Instantiate方法实例化游戏对象与调用CreatePrimative方法创建游戏对象的最 终结果是完全一样的,实例化游戏对象会将对象的脚本及所有继承关系实例化到游戏场景 中。相较于创建物体的CreatePrimative方法,Instantiate实例化方法的执行效率要高很 多。在开发过程中,通常会使用Instantiate方法执行实例化物体。调用该方法时,一般与 预制体Prefab结合使用。 2.项目设计 本项目计划通过C#脚本在Unity引擎内创建一个简单的Cube模型和Sphere模型。 单击屏幕左上方的按钮创建Cube和Sphere模型,如图3.16所示。 图3.16 创建Cube和Sphere模型 3.项目实施 第1步:选择菜单栏中的File→NewScene命令,新建场景,创建平面,搭建简单场景, 如图3.17所示。 第2 步:使用CreatePrimitive 方法创建游戏对象,创建C# 脚本,将其命名为 CreatePrimiteve,输入代码如下。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class CreatePrimiteve : MonoBehaviour { 74 图3.17 创建平面 void OnGUI() { if (GUILayout.Button("CreateCube", GUILayout.Height(50))) { GameObject m_cube=GameObject.CreatePrimitive(PrimitiveType.Cube); m_cube.AddComponent(); m_cube.GetComponent().material.color=Color.blue; m_cube.transform.position=new Vector3(0, 10, 0); } if (GUILayout.Button("CreateSphere", GUILayout.Height(50))) { GameObject m_cube=GameObject.CreatePrimitive(PrimitiveType.Sphere); m_cube.AddComponent(); m_cube.GetComponent().material.color=Color.red; m_cube.transform.position=new Vector3(0, 10, 0); } } } 第3步:将CreatePrimitive脚本链接到MainCamera对象上,如图3.18所示。 图3.18 脚本链接 75 4. 项目测试 单击Play按钮进行测试。可以看到,在Game 视图中单击CreateCube按钮或 CreateSphere按钮后,将分别调用CreatePrimitive() 方法,从而创建Cube和Sphere游戏对 象,运行效果如图3.20 所示。 19 和图3. 图3.19 运行测试前图3. 20 运行测试后 3.3 变换的立方体 3. 1. 项目构思 移动、旋转、缩放功能在脚本编写中经常遇到,可以使用transform.Translate()、 trnfrm.tte() 等方法实现。本项目通过一个立方体讲解脚本编译中的移动、旋转、缩 asoRoa 放函数的编写以及与OnGUI() 函数交互功能的实现。 2. 项目设计 本项目计划通过C# 脚本在Unity引擎内创建一个简单的Cube模型,采用OnGUI() ub2124 方法写3个交互按钮,实现与Ce模型进行移动、旋转、缩放的交互功能,如图3.~图3. 所示。 图3. 21 初始场景效果 76 图3. 22 移动立方体效果 图3.23 旋转立方体效果图3. 24 缩放立方体效果 3. 项目实施 第1步:选择菜单栏中的File→NewScene命令,新建场景。 第2步:选择菜单栏中的GameObject→3DObject→Plane命令,创建平面,搭建简单 场景,如图3. 25 所示。 图3. 25 创建平面 第3步:选择菜单栏中的GameObject→3DObject→Cube命令,创建一个盒子,如 图3. 26 所示。 第4步:在Project视图中新建一个C# 脚本,将其命名为MyScript,打开此脚本并添 77 图3.26 游戏物体场景摆放图 加代码,如下所示。 using UnityEngine; using System.Collections; public class MyScript : MonoBehaviour {//声明4 个变量 public GameObject myCube; public int transSpeed=100; public float rotaSpeed=10.5f; public float scale=3; void OnGUI() { if(GUILayout.Button ("移动立方体")) { myCube.transform.Translate (Vector3.forward* transSpeed * Time.deltaTime, Space.World); } if(GUILayout.Button ("旋转立方体")) { myCube.transform.Rotate (Vector3.up*rotaSpeed,Space.World); } if(GUILayout.Button ("缩放立方体")) { myCube.transform.localScale=new Vector3(scale,scale,scale);} } } 脚本的第5行到第8行一共声明了4个变量,且都使用public修饰,所以它们可以作为 属性出现在Inspector视图中。脚本的第9行到第17行是OnGUI()函数,用于在界面中显 示按钮,通过单击按钮实现与立方体的交互功能。 第5步:将脚本MyScript链接到MainCamera上,并将Cube拖入Inspector视图中, 如图3.27所示。 78 图3. 27 脚本链接 4. 项目测试 单击Play按钮进行测试,可以看到Game 视图的左上角会出现3个按钮:移动立方体、 旋转立方体和缩放立方体。单击相应的按钮,即可完成对立方体对象的指定操作,如图3. 28 所示。 图3. 28 运行测试效果图 3.4 脚本开发综合项目 1. 项目构思 虚拟漫游可以提升游戏玩家的沉浸感。Unity引擎中提供了第一人称及第三人称虚 拟漫游的组件。本项目通过脚本实现第一人称虚拟漫游功能,使读者深入掌握Unity脚本 编写的方法。 2. 项目设计 本项目计划在场景内摆放一些基本几何体,构建简单3D 场景,采用C# 脚本开发第一 人称虚拟漫游功能。即通过键盘的W、S、A、D键在场景内自由行走,通过鼠标实现观察者 视角的旋转功能,如图3. 29 所示。 3. 项目实施 第1步:双击UnityHub图标,启动Unity引擎,建立一个空项目。 第2步:在游戏场景中选择菜单栏中的GameObject→3D→Plane命令,创建一个平面, 如图3. 30 所示。 第3步:选择菜单栏中的GameObject→CreateEmpty命令,创建空物体,并将其标签 设为Player。 79 图3. 29 第一人称虚拟漫游测试效果 图3. 30 创建平面 一个标签是用来索引一个或一组游戏对象的词。标签是为了编程而对游戏对象的标 注,可以使用标签来书写脚本代码,通过搜索找到包含想要的标签的对象。添加标签的方法 很简单,即选中Inspector视图右上方的Tag,单击AddTag,打开标签管理器,然后在其中 输入Player,如图3.32 所示。 在Tag的下拉菜单中找到Player标签, 31 所示。然后再次选择空物体, 完成标签添加,如图3. 图3.31 打开标签管理器图3. 32 添加标签 80 第4步:在Hierarchy视图中选中Player,然后选择菜单栏中的Component→Physics→ CharacterController命令,为主角Player添加CharacterController组件,如图3.33所示。 CharacterController主要用于第三人称或第一人称游戏主角控制,并不使用刚体物理 效果。 图3.33 添加CharacterController组件 第5步:在Hierarchy视图中选中Player,然后选择菜单栏中的Component→Physics→ Rigidbody命令,为主角Player添加Rigidbody组件。在Rigidbody组件属性中取消Use Gravity,选中IsKinematic,使其不受物理影响,而受脚本控制,如图3.34所示。 图3.34 添加Rigidbody组件 第6步:在Scene视图中调整CharacterController的位置和大小,使其置于平面之上。 第7步:在Project视图的空白处右击,在弹出的快捷菜单中选择Create→C#命令,创 建一个C#脚本,将脚本命名为Player。 第8步:输入代码,如下所示。 using UnityEngine; using System.Collections; public class Player : MonoBehaviour { public Transform m_transform; //角色控制器组件 CharacterController m_ch; float m_movSpeed=3.0f; //角色移动速度 float m_gravity=2.0f; //重力 void Start () { m_transform=this.transform; m_ch=this.GetComponent(); //获取角色控制器组件 81 } void Update () {Control();} void Control() { float xm=0, ym=0, zm=0; //定义3 个值控制移动 ym -=m_gravity*Time.deltaTime; //重力运动 //上下左右移动 if (Input.GetKey(KeyCode.W)){ zm+=m_movSpeed * Time.deltaTime; } else if (Input.GetKey(KeyCode.S)){ zm -=m_movSpeed * Time.deltaTime; } if (Input.GetKey(KeyCode.A)){ xm -=m_movSpeed * Time.deltaTime; } else if (Input.GetKey(KeyCode.D)){ xm+=m_movSpeed * Time.deltaTime; } //使用角色控制器提供的Move 函数进行移动 m_ch.Move(m_transform.TransformDirection (new Vector3(xm, ym, zm))); } } 上述代码主要是控制主角前、后、左、右移动。在Start()函数中,首先获取 CharacterController组件,然后在Control函数中通过键盘操作获得X 和Y 方向上的移动 距离,最后使用CharacterController组件提供的Move移动主角。使用CharacterController 提供的功能移动,在移动的同时会自动计算移动体与场景之间的碰撞。 第9步:在Hierarchy 视图中选中Player游戏对象,将写好的Player脚本链接到 Player游戏对象上,如图3.35所示。 图3.35 Player脚本链接 第10步:此时运行测试,按W、S、A、D 键可以控制主角前、后、左、右移动。但是,在 Game视图中却观察不到主角在场景中移动的效果,这是因为摄像机还没有与主角的游戏 对象关联起来。此时需要添加摄像机代码,打开Player.cs继续添加代码,如下所示。 using UnityEngine; using System.Collections; 82 public class Player : MonoBehaviour { public Transform m_transform; CharacterController m_ch; float m_movSpeed=3.0f; float m_gravity=2.0f; Transform m_camTransform; //摄像机Transform Vector3 m_camRot; //摄像机旋转角度 float m_camHeight=1.4f; //摄像机高度 //修改Start()函数,初始化摄像机的位置和旋转角度 void Start () { m_transform=this.transform; m_ch=this.GetComponent(); //获取角色控制器组件 m_camTransform=Camera.main.transform; //获取摄像机 Vector3 pos=m_transform.position; pos.y+=m_camHeight; m_camTransform.position=pos; m_camTransform.rotation= m_transform.rotation; //设置摄像机的旋转方向与主 角一致 m_camRot=m_camTransform.eulerAngles; Screen.lockCursor=true; //锁定鼠标 }v oid Update () { Control();} void Control() { //获取鼠标移动距离 float rh=Input.GetAxis("Mouse X"); float rv=Input.GetAxis("Mouse Y"); //旋转摄像机 m_camRot.x -=rv; m_camRot.y+=rh; m_camTransform.eulerAngles=m_camRot; //使主角的面向方向与摄像机一致 Vector3 camrot=m_camTransform.eulerAngles; camrot.x=0; camrot.z=0; m_transform.eulerAngles=camrot; float xm=0, ym=0, zm=0; ym -=m_gravity*Time.deltaTime; if (Input.GetKey(KeyCode.W)){ zm+=m_movSpeed * Time.deltaTime;} else if (Input.GetKey(KeyCode.S)){ zm -=m_movSpeed * Time.deltaTime;} if (Input.GetKey(KeyCode.A)){ xm -=m_movSpeed * Time.deltaTime;} else if (Input.GetKey(KeyCode.D)){ xm+=m_movSpeed * Time.deltaTime;} m_ch.Move(m_transform.TransformDirection (new Vector3(xm, ym, zm))); //使摄像机位置与主角一致 83 Vector3 pos=m_transform.position; pos.y+=m_camHeight; m_camTransform.position=pos;} } 上述代码通过控制鼠标旋转摄像机方向,使主角跟随摄像机的Y 轴旋转方向,移动主 角时,使摄像机跟随主角运动。 4.项目测试 单击Play按钮进行测试,效果如图3.36和图3.37所示。通过鼠标可以在场景中旋转 视角,通过W、S、A、D键可以在场景中移动主角向前、向后、向左、向右移动。 图3.36 用鼠标控制旋转视角 图3.37 用键盘控制移动方向