3.3 Unity 3D脚本编写
3.3.1 创建脚本
首先执行Assets→Create→C#Script或JavaScript菜单命令创建一个空白脚本,将其命名为Move,如图3.3所示。
图3.3 创建脚本
在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→Project Settings→Input(输入)菜单命令,即可修改映射到水平方向和垂直方向的名称和快捷键,如图3.4所示。
图3.4 Input命令
3.3.2 链接脚本
脚本创建完成后,需要将其添加到物体上。在Hierarchy视图中,单击需要添加脚本的游戏物体Main Camera(主摄像机),然后执行Component→Script→Move菜单命令,如图3.5所示,Move脚本就链接到了Main Camera上。
摄像机(Camera)是向玩家捕获和显示世界的设备。通过自定义和操纵摄像机,可以自由旋转游戏视角。场景中摄像机的数量不受限制,它们可以以任何顺序放置在屏幕上的任何地方,或者只捕获屏幕的某些部分。摄像机参数如图3.6和表3.6所示。
图3.5 链接脚本
图3.6 摄像机参数设置
表3.6 摄像机参数
3.3.3 运行测试
单击播放按钮,在Scene视图中,使用键盘上的W(前)、S(后)、A(左)、D(右)键移动摄像机,运行效果如图3.7和图3.8所示。
3.3.4 C#脚本编写注意事项
在Unity 3D中,C#脚本的运行环境使用了Mono技术,Mono是指Novell公司致力于.NET开源的工程,利用Mono技术可以在Unity 3D脚本中使用.NET所有的相关类。但Unity 3D中C#的使用与传统的C#有一些不同。
(1)脚本中的类都继承自MonoBehaviour类。Unity 3D中所有挂载到游戏对象上的脚本中包含的类都继承自MonoBehaviour类。MonoBehaviour类中定义了各种回调方法,例如Start、Update和FixedUpdate等。通过在Unity中创建C#脚本,系统模板已经包含了必要的定义,如图3.9所示。
图3.7 运行测试效果图1
图3.8 运行测试效果图2
图3.9 在Unity 3D中创建C#脚本
(2)使用Awake或Start方法初始化。用于初始化的C#脚本代码必须置于Awake或Start方法中。Awake和Start的不同之处在于:Awake方法是在加载场景时运行,Start方法是在第一次调用Update或FixedUpdate方法之前调用,Awake方法在所有Start方法之前运行。
(3)类名必须匹配文件名。C#脚本中类名必须和文件名相同,否则当脚本挂载到游戏对象时,控制台会报错。
(4)只有满足特定情况时变量才能显示在属性查看器中。只有公有的成员变量才能显示在属性查看器中,而private和protected类型的成员变量不能显示,如果要使属性项在属性查看器中显示,它必须是public类型的。
(5)尽量避免使用构造函数。不要在构造函数中初始化任何变量,而应使用Awake或Start方法来实现。在单一模式下使用构造函数可能会导致严重后果,因为它把普通类构造函数封装了,主要用于初始化脚本和内部变量值,这种初始化具有随机性,容易引发引用异常。因此,一般情况下尽量避免使用构造函数。
实践案例:脚本环境测试
案例构思
在脚本环境测试实践项目中,需要通过脚本的编写、编译、链接过程实现玩家在游戏场景中走动的效果。本案例旨在通过脚本环境编译测试结果让读者熟悉Unity 3D脚本开发环境,为后续程序编写打下基础。
案例设计
本案例通过JavaScript脚本创建一个简单的Cube模型,通过键盘的方向键控制Cube模型的上下左右移动,并能通过鼠标交互实现Cube模型复制效果,如图3.10所示。
图3.10 运行测试效果图
案例实施
步骤1:设置场景。首先在游戏场景中创建一个Cube作为地面,缩放Cube的Scale值为(5,0.1,5),使Cube在场景中看起来是一个大平板。
步骤2:在Hierarchy视图中将Cube重命名为Plane。
步骤3:创建第二个Cube,将它放置在这个大平板的中心位置,将其重命名为Cube1。
步骤4:创建一个空脚本,执行Assets→Create→Javascript命令并在Project面板中将脚本重命名为Move。
步骤5:双击打开Move脚本,写入以下代码:
function Update(){ transform.Translate(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));}
Update函数在渲染一帧之前被调用,这里是大部分游戏行为代码被调用的地方。在脚本中移动一个游戏对象时,需要用transform来更改它的位置,Translate函数有x、y和z 3个参数。Input.GetAxis()函数返回-1或1,横轴和竖轴是在输入设置(Input Settings)中预先定义好的,执行Edit→Project Settings→Input命令可以重新定义按键映射。
步骤6:保存脚本(Ctrl+S键)。
步骤7:将脚本与主摄像机相连,即拖动脚本到Hierarchy面板中的Main Camera对象上,这时脚本与场景中的摄像机产生了关联。
步骤8:单击播放按钮测试一下,发现通过键盘W、S、A、D键可以在场景中移动摄像机,但是移动速度稍快,不可调。
步骤9:更新代码。
var speed=5.0; function Update(){ var x=Input.GetAxis("Horizontal")*Time.deltaTime*speed; var z=Input.GetAxis("Vertical")*Time.deltaTime*speed; transform.Translate(x, 0, z); }
Update()上面的速度变量speed是一个public变量,这个变量会在Inspector面板中看到,可以在Inspector面板中方便地调整它的值以便于测试。
步骤10:增加新的功能,实现当按下开火按钮时在用户(主相机)当前位置创建新的游戏对象。
创建脚本Create.js,并且将脚本链接到Main Camera上,如图3.11所示。
图3.11 链接脚本
代码如下:
var newObject:Transform; function Update(){ if(Input.GetButtonDown("Fire1")) {Instantiate(newObject, transform.position, transform.rotation);} }
步骤11:调试。调试是发现和修正代码中人为错误的过程。Unity 3D中提供了Debug类,Log()函数允许用户发送信息到Unity 3D的控制台,当用户按下开火按钮时发送一个消息到Unity控制台。修改脚本如下:
var newObject:Transform; function Update(){ if(Input.GetButtonDown("Fire1")){ Instantiate(newObject, transform.position, transform.rotation); Debug.Log("Cube created"); } }
运行游戏并按下开火按钮创建一个新的Cube实例后,控制台会出现Cube created字样,这些字符是灰色的,意味着它是只读的(不能编辑)。创建了新的Cube后的场景如图3.12和图3.13所示。
实践案例:创建游戏对象
案例构思
游戏场景中出现的所有物体都属于游戏对象,游戏对象之间的交互都可以通过脚本来控制并实现。
创建游戏对象的方法有3种:第一种是将物体模型资源由Project视图直接拖曳到Hierarchy面板中;第二种是在Unity 3D菜单GameObject中创建Unity 3D自带的游戏对象,如Cube、Camera、Light等;第三种是利用脚本编程,动态创建或删除游戏对象,本实践案例采用第三种方法,即利用脚本动态创建游戏对象。利用脚本动态创建游戏对象的方法又分为两种:使用CreatePrimitive方法创建Unity 3D系统自带的基本游戏对象,使用Instantiate实例化方法将预制体实例化为对象。
图3.12 初始场景效果
图3.13 创建Cube后的效果
案例设计
本案例通过C#脚本在Unity 3D内创建一个Cube模型和一个Sphere模型,通过屏幕左上方按钮控制Cube和Sphere模型创建,如图3.14所示。
案例实施
步骤1:使用CreatePrimitive方法创建Unity 3D系统自带的基本游戏对象,创建脚本输入代码。
using UnityEngine; using System.Collections; public class CreatePrimitive:MonoBehaviour{ OnGUI() { if(GUILayout.Button("CreateCube", GUILayout.Height(50))){ GameObject m_cube=GameObject.CreatePrimitive(PrimitiveType.Cube); m_cube.AddComponent<Rigidbody>(); m_cube.GetComponent<Renderer>().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<Rigidbody>(); m_cube.GetComponent<Renderer>().material.color=Color.red; m_cube.transform.position=new Vector3(0, 10, 0); } } }
图3.14 运行测试效果
在上述OnGUI()函数中,单击CreateCube按钮或CreateSphere按钮后,将分别调用CreatePrimitive方法,从而创建Cube和Sphere游戏对象,并且为这两个游戏对象添加刚体、颜色以及位置属性,运行效果如图3.15和图3.16所示。
图3.15 运行测试前
图3.16 运行测试后
步骤2:使用Instantiate实例化方法将预制体实例化为对象。
当制作好了游戏组件(场景中的任意一个GameObject)后,将它制作成一个组件模板,用于批量的套用工作,例如场景中“重复”的东西——“敌人”“士兵”“子弹”……称为预制体,默认生成的预制体其实就像克隆体,其内储存着一个游戏对象,包括游戏对象的所有组件及其下的所有子游戏对象。
(1)创建预制体。首先在场景中创建一个立方体,然后在Project面板中单击Create菜单的下拉三角,在出现的对话框中选择Prefab创建一个预制体,并为其命名为MyCube,如图3.17所示。
然后在Hierarchy视图中将立方体拖曳至Project视图中的MyCube,完成预制体的制作并与立方体Cube关联。在Hierarchy视图中与预制体关联的游戏对象为蓝色,如图3.18所示。
图3.17 创建预制体
图3.18 关联预制体
(2)预制体实例化。将预制体复制一份到场景里的过程叫实例化,在Project面板中将预制体拖曳到Inspector面板中可以实例化一个对象。预制体的实例化不是普通的复制,预制体实例化后产生的新游戏对象依然保持着与预制体的关联,也就是预制体进行添加组件、修改组件属性等改变,预制体实例化产生的游戏对象也会发生相应的改变。
if(Input.GetButtonDown("Fire1")){ Instantiate(newObject, transform.position, transform.rotation); }
上述代码中调用Instantiate方法实例化游戏对象与调用CreatePrimitive方法创建游戏对象的最终结果是完全一样的,实例化游戏对象会将对象的脚本及所有继承关系实例化到游戏场景中。Instantiate实例化方法比创建物体的CreatePrimitive方法执行效率要高很多。在开发过程中通常会使用Instantiate方法实例化对象,Instantiate方法调用时一般与预制体Prefab结合使用。
实践案例:旋转的立方体
案例构思
在脚本编写中经常用到移动、旋转、缩放功能,可以使用transform.Translate()、transform.Rotate()和transform.localScale方法实现,本案例旨在通过一个立方体让读者掌握脚本编译中移动、旋转、缩放的函数编写以及与OnGUI函数交互功能的实现。
案例设计
本案例计划通过C#脚本在Unity内创建一个简单的Cube模型,采用OnGUI方法写3个交互按钮,实现与Cube模型进行移动、旋转、缩放的交互功能,如图3.19至图3.22所示。
图3.19 初始场景效果
图3.20 移动立方体效果
图3.21 旋转立方体效果
图3.22 缩放立方体效果
案例实施
步骤1:为游戏项目里的游戏场景添加两个游戏对象:Cube和Directional,前者是脚本要操作的游戏对象,后者是负责游戏场景照明的游戏对象。然后创建一个Plane位于Cube下方。调整游戏场景中3个游戏对象的位置,使得Game视图达到最佳的效果,如图3.23所示。
步骤2:在Project视图里,新建一个C#脚本,命名为MyScript,打开此脚本并添加下面的代码:
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); } } }
图3.23 游戏场景物体摆放图
脚本声明了4个变量,且都使用public修饰,所以它们可以作为属性出现在组件下。OnGUI()函数用于在界面中显示按钮,玩家可以通过单击按钮实现与立方体的交互功能。
步骤3:将脚本MyScript赋予Main Camera。
步骤4:运行游戏,在Game视图的左上角会出现3个按钮:“移动立方体”“旋转立方体”和“缩放立方体”。单击按钮,即可完成对立方体对象的指定操作,如图3.24所示。
综合案例:第一人称漫游
案例构思
虚拟漫游可以提升游戏玩家的沉浸感,Unity 3D中提供了第一人称以及第三人称虚拟漫游的组件,如何编写一段脚本实现虚拟漫游功能呢?本案例旨在通过脚本实现第一人称虚拟漫游功能,让读者深入掌握编写Unity 3D脚本实现游戏功能的方法。
案例设计
本案例在场景内摆放一些基本几何体,构建简单的3D场景,采用C#脚本开发第一人称虚拟漫游功能,通过键盘W、S、A、D键在场景内自由行走,通过鼠标实现观察者视角的旋转功能,如图3.25所示。
图3.24 运行测试效果
图3.25 第一人称虚拟漫游测试效果
案例实施
步骤1:执行GameObject→3D Object→Plane命令创建一个平面。
步骤2:执行GameObject→Create Empty命令创建空物体,并将标签设为Player。
图3.26 打开标签管理器
一个标签是用来索引一个或一组游戏对象的词。标签是为了编程的目的而对游戏对象的标注,游戏开发人员可以使用标签来书写脚本代码,通过搜索找到包含想要的标签的对象。添加标签方法很简单,选中Inspector面板右上方的Tag,点击Add Tag将在检视面板打开标签管理器,然后在里面输入Player,如图3.26所示。然后再次选择空物体,在Tag的下拉列表中找到Player标签,完成添加标签,如图3.27所示。
步骤3:为主角添加角色控制器组件。执行Component→Physics→Character Controller命令,如图3.28所示。角色控制器主要用于第三人称或第一人称游戏主角控制,并不使用刚体物理效果,具体参数如表3.7所示。
图3.27 添加标签
图3.28 添加Character Controller组件
表3.7 Character Controller组件的参数
图3.29 添加刚体组件
步骤4:添加Rigidbody组件,取消选中Use Gravity复选框,选中Is Kinematic复选框使其不受物理影响,而是受脚本控制,如图3.29所示。
步骤5:调整Character Controller的位置和大小,使其置于平面之上。
步骤6:创建C#脚本,将其命名为Player。
步骤7:输入如下代码:
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<CharacterController>(); } void Update(){ Control(); } void Control(){ //定义3个值控制移动 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; } //使用角色控制器提供的Move函数进行移动 m_ch.Move(m_transform.TransformDirection(new Vector3(xm, ym, zm))); } }
上述代码主要是控制角色前后左右移动。在Start函数中,首先获取CharacterController组件,然后在Control函数中通过键盘操作获得X和Y方向上的移动距离,最后使用CharacterController组件提供的Move函数移动角色。使用CharacterController提供的功能进行移动时,会自动计算移动体与场景之间的碰撞。
步骤8:在Hierarchy视图中选中Player游戏对象,在其Inspector属性面板中选择Component→Script,选择Player脚本将其链接到Player游戏对象上,如图3.30所示。
步骤9:此时运行测试,按W、S、A、D键可以控制主角前后左右移动,但是在Game视图中却观察不到主角在场景中移动的效果,这是因为摄像机还没有与主角的游戏对象关联起来,需要添加摄像机代码。打开Player.cs,添加如下代码:
//摄像机Transform Transform m_camTransform; //摄像机旋转角度 Vector3 m_camRot; //摄像机高度 float m_camHeight=1.4f; //修改Start函数, 初始化摄像机的位置和旋转角度 void Start(){ m_transform=this.transform; //获取角色控制器组件 m_ch=this.GetComponent<CharacterController>(); //获取摄像机 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; } void 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; //操作角色移动代码 //使摄像机位置与角色一致 Vector3 pos=m_transform.position; pos.y+=m_camHeight; m_camTransform.position=pos; }
图3.30 Player脚本链接
上述代码通过控制鼠标旋转摄像机方向,使角色跟随摄像机的Y轴旋转方向,在移动角色时,使摄像机跟随角色运动。
步骤10:单击Play按钮进行测试,效果如图3.31和图3.32所示,通过鼠标操作可以在场景中旋转视角,通过W、S、A、D键可以在场景中向前、向后、向左、向右移动。
图3.31 利用鼠标旋转视角
图3.32 用键盘按键控制移动