通过学习:
1.将了解到如何新建一个游戏对象(game object)
2.为这些游戏对象添加组件(components)
3.为他们的属性(properties)赋值
4.把这些对象放置在场景(scene)中来创建一个游戏
在游戏中,玩家将控制一个求在场地中滚动,我们将利用物理过程和力让小球移动,我们要接收用户的键盘输入,并利用这些输入内容对小球施力,让它在场景中移动。
我们会学习如何检测小球和捡拾物的碰撞,并使用这些事件来收集捡拾物。结束时,我们将完成一个很简单的滚球游戏:玩家通过键盘控制小球收集特定的物体,并计算分数,当所有的捡拾物收集完成后,展示分数并结束游戏。本次项目不需要导入任何资源,也不会用到模型、材质、音频和动画,我们只用unity提供的原始形状,例如方块、球体和平面。
首先,让我们从课程1开始,设置游戏和初始的游戏对象。
让我们从新建项目(project)开始,我们可以通过File-New Project 新建项目,这会打开一个界面,命名为Roll a Ball,然后指定项目路径,选择3D,然后Create Project。然后有了有一个空白场景的新项目(project),在为新场景添加东西时,我们要先保存场景(save scene)。
新建游戏场地:
会用到unity自带的平面(plane),
新建平面:
GameObject-3D Object-Plane或者
通过层级(Hierarchy)视图的创建(create)菜单,
把这个游戏对象重命名为Ground,选中游戏对象并按回车键或者双击游戏对象进入名字编辑状态,输入新名字后回车确认。
使用Transform(变换)的下拉齿轮菜单重设(reset)变换组件,这样会把游戏对象置于场景的坐标(0,0,0)处,这个位置是游戏世界的原点,也是场景中所有坐标计算的基础。
选中游戏对象,鼠标放在场景视图中按F键,或者选择编辑(Edit)菜单中的Frame Selected(frame:框架,结构),可以在场景视图中看到整个游戏对象。
观察当前的场景,可以看到网格线标识了平面的原点,不过在本项目中,要把它们关掉,在场景视图中选择Gizmos(线框)菜单,取消选择show grid(grid:网格)。
改变地平面的大小:
1.使用缩放(Scale)工具,选中想改变的坐标轴,然后拖拽它。
2.点击并拖拽要改动的字段标题。
3.直接输入数字,可以按Tab在字段间自由切换,然后按回车确认选择。
一个平面(plane)是没有体积的,因此scale属性的Y轴的缩放是无效的,除非输入负数,输入负数时,平面这样的单面物体将会反向。
如果你在场景中放置了一个平面,但看不见它,可以检查平面和摄像机(camera)的朝向,并确保平面Scale属性的Y值有正确的值,正常情况下该值为1。
实现比例,输入,拖拽。Plane是没有体积的,Y轴对它无效,确保为 Scale Y轴为1,其他轴的值为2。
新建玩家(player)对象:
本次任务中玩家是一个小球,通过Herarchy(层级视图)-Create菜单选择球体(Sphere),重命名为Player,重置(reset)变换(transform)确保它在原点,选择Edit-Frame Selected,以便场景视图聚焦在球体上。
为什么球体被埋在平面下了呢,这是因为它们在场景中的坐标完全相同,都是原点X,Y,Z(0,0,0)。
要把玩家的球上移,直到它被放置在平面上。
像方块(cubes)、球体(spheres)、容器/胶囊(capsules)等,Unity中所有的初始对象都有标准尺寸,如1:1:1或1:2:1的Unity单位,因此只需把球的Y轴上移半个unity单位,这样它就会刚好落在平面上,如果从游戏视角(Game view)看,我们会发现球体是明亮的,并且在平面上投下了阴影,所有的新场景都带有默认的天空盒(sky box)和模拟太阳的平行光源(directional light),所以我们不用费心去设置任何光源。
给球体和背景添加颜色
但是现在球体和背景都是白色,让我们给背景加点颜色,这样就好区分了,要想给模型(model)添加颜色或纹理(texture),就要用到材质(material)。我们这里不使用纹理,就选一个标准的材质给对象添加颜色。
1.首先,在项目下新建一个文件夹,用于放置材质。我们用项目(project)的新建(create)菜单,然后选择文件夹(Folder),重命名为Materials,选中这个文件夹后,再次用项目的新建(Create)菜单,这次选择Material选项,注意看,材质被创建在Materials文件夹里了,这是因为我们创建新的材质时,选中了该文件夹,把材质重命名为Background。
选中该材质,然后看Main Maps下面,第一个属性是Albedo(反照率,漫反射系数),点击它的颜色区域,打开拾色器,把颜色改成一个漂亮的暗影蓝,这里我直接使用RGGB(0,32,64),要看效果的话,需要确保预览窗口是开着的。
下面给平面弄上这个贴图(texture),只需要选中项目(project)视图中的材质,然后把它拖拽到场景(scene)视图的平面上,现在玩家已经站在深蓝色场地上了,我想再加点变化,这会在后面帮到我们,我想旋转(rotate)主光源,让我们的玩家有更好的光照效果,选中Directional Light,然后把Transform组件Rotation属性的Y值改成60,这样会让我们的玩家球体产生更好的轮廓,现在我们有一个玩家对象和场地了。
现在我们要移动玩家对象,首先要想想,我们想让球怎么动。我们想让球在地上滚,撞上墙然后停下,不要飞出去,我们希望能收集一些可收集物,这就需要物理系统了。
要使用物理系统,需要游戏对象和刚体组件(rigidbody component)关联,要关联一个新的组件,需要先选中要关联的游戏对象。在这里我们要选择玩家对象,接着我们1.既可以选择component菜单,选择physics-Rigid Body,这样就能关联我们所选中的游戏对象了。2.或者用Inspector里的Add Component(添加组件)按钮,选择Physics-Rigid Body。这两种方法都可以给游戏对象关联上刚体组件。
如果想给游戏对象的组件重新排序,可以用组件右下角的下拉齿轮菜单,做这些对我们的游戏效果不会有影响。
不过,通过保持项目和层级有个稳定的组件顺序,可能会提升开发效率。
别忘了,你可以点击组件的标题栏,折叠或展开组件视图。要注意,当你这样做的时候,会切换场景视图中相关组件的辅助线(gizmos)。
现在,我们要让玩家对象在控制下移动,因此我们要获取玩家的键盘输入,并且把这些输入转换成对球的作用力来让球移动。我们要写一个与游戏对象关联的脚本来实现这个功能。
首先在project视图中新建一个文件夹来存放脚本文件,选择Project视图的Create菜单,然后选择Create Folder,重命名为Scripts。下面我们来新建一个C#脚本,我们有几种方式新建脚本,我们1.可以选择Assets-Create新建脚本2.或者用project视图的Create菜单,但这里有更效率的方式,就是选中玩家对象,用Inspector中的Add Component按钮,Add Component菜单包含New Script选项,这样可以一步完成新建和关联。
1.首先,我们把脚本命名为PlayerController,我们可以选择脚本的语言C#,然后点击Create and Add或者直接敲回车键确认选择。
Unity会新建、编译脚本并与游戏对象关联。
要注意的是,用这种方式新建脚本,会把脚本生成在项目视图的根目录下,需要我们把它移动到Scripts文件中以保持项目视图的组织关系。
如果我们选择了这个脚本,可以在Inspector中看到预览,但这时候是不可编辑的,我们打开它,依然有几种方式,当我们检查关联了脚本的游戏对象时,可以用对应的下拉齿轮菜单,我们可以双击project视图下的脚本文件,或者我们可以选中脚本后点Inspector的Open按钮,脚本会在我们常用的编译器中打开。
1.首先我们删掉原有的示例代码。
下面让我们想想,我们想用脚本干嘛,我们想每一帧都检测一下用户的输入,然后每一帧都把输入应用到游戏对象上,我们在那检测并应用输入呢,有两种方式:Update和Fixed Update
Update在每一帧展示中之前被调用,我们的大多数代码就是在这里执行的。
而Fixed Update是在进行任何物理计算之前被调用,我们的物理代码就在这里执行。
我们对刚体施加力的作用让小球移动,这就是物理系统,所以我们要把代码放进Fixed Update。
在使用unity引擎开发时,很多程序员都弃之不用unity引擎自带的MonoDevelop编辑器,而选择VS。
我们需要写什么呢,我们知道需要Input,但如何知道更多呢,Monodevelop中有个快捷键,可以搜索Unity API,在PC上是Ctrl+单引号,选择你想搜索的项,在这里是input,然后按住Ctrl键输入单引号,搜索结果会给出文档中与input有关的所有链接。
点击链接,会打开input的页面,这就是input类的页面。
我们用这个类读取输入管理器中的坐标轴或者移动设备的多点触控或传感器数据,我们阅读上面的文字来理解如何使用这个类,在这里就是获取全平台的用户输入,包括移动设备,在说明的下面是一系列变量和方法类变量持有信息,例如表示触摸点数量的touchCount或者表示默认陀螺仪的gyro。
类方法可以实现某些功能,在我们的代码中,要用到Input.GetAxis(axis:轴),当我们找到要找的东西后,点击链接,就能打开方法或变量对应的说明页面。让我们看看这个方法,这个页面包含方法的声明
和说明
以及展示用法的代码片段
包括JavaScript和C#,我们用C#。
想得到InputManager和Input.GetAxis的更多信息,可以看下面链接的课程。我们要用的Input.GetAxis的方式和代码片段差不多,让我们回到脚本编写代码。
horizontal(水平的)
vertical(垂直的)
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
这样就能抓取用户的键盘输入了。
浮点型变量moveHorizontal和moveVertical,记录的是横纵坐标,是由键盘控制的。我们的游戏对象是一个刚体,并且依赖物理引擎互动,我们将用这些输入对刚体施加力,以此在游戏中移动游戏对象。想了解更多关于力对刚体作用的信息,就要查看文档了,输入Rigidbody,选中rigidbody并按住Ctrl输入单引号,搜索项rigidbody的结果页面出来了,我们点击Rigidbody。
如果我们想对游戏对象施加力,我们需要使用AddForce
该方法可对刚体施力,这样刚体就会移动了。
方法的声明,这个声明告诉我们,需要一个Vector3变量和一个可选的ForceMode来给刚体施力。Vetcor3是啥,简单来说,Vector3是一个容器,包含三个小数值,它能让四处移动变得容易,并且,在需要一个值代表X,Y,Z三轴力的三维空间中,这些值就相当于力或者表示一个旋转,这旋转需要用一个变量描述三个轴向。
Vector3(x,y,z)
本次示例,我们既可以用一个Vector3,也可以用三个浮点型的X,Y,Z值。
下一个要说明的概念,是如何让游戏对象得到,或者说引用不同的组件,我们在写的脚本叫PlayerController,它通过一个脚本组件关联着小球,在这个脚本中,我们需要刚体组件来调用AddForce,我们想要脚本取得刚体组件的引用。
有几种方式,但本次只用一种获取其它组件的方式,我们新建一个变量持有刚体组件的引用,并且我们在Start方法内设置这个引用,我们看代码片段,public Rigidbody rb
这句代码创建了一个公共变量,是一个叫rb的Rigidbody类型的变量,它将持有我们想要的刚体的引用,在Start方法内用代码设置了引用rb = GetComponent
,这将返回一个已关联的刚体的引用。
如果存在的话,所有Start中的代码都会在脚本激活的第一帧执行。通常也是游戏里最早的一帧。
最后,在FixedUpdate方法中,通过变量rb获取了关联的刚体组件,调用了AddForce。因此,在脚本中我们要这样写。
private Rigidbody rb;//创建变量来持有引用
在新的Start方法内我们要这样写, rb = GetComponent
在FixedUpdate方法中,让我们用AddForce最简单的版本,即只传一个Vector3参数,省略ForceMode让它保持默认,所以在脚本中要这样写,rb.AddForce和某个Vector3。现在我们如何将两个浮点数值用在一个Vector3里面呢。让我们创建一个新的Vector3变量叫Movement,让它等于一个新的Vector3,这个Vector3由X,Y,X组成,这些X,Y,Z值将决定给小球施的力的方向。X值是什么呢,就是moveHorizontal,通过它我们的左右键就能施力,让小球左右移动了,Y是多少呢,0,因为我们不需要向上移动,Z值呢,就是moveVertical了。现在我们把movement这个Vector3变量 用在rb.AddForce中,即为rb.AddForce(movement);
让我们保存脚本,回到unity中。
我们看看底部或控制台的错误信息,发现一切正常,很good。我们来测试测试刚写的东西,点击Play,然后按方向键,小球移动了,不过太慢了,慢的没法玩,不过基本想法完全行得通,退出游戏模式。
●我们回到代码中,新建一个工具,以便我们可以控制球的速度,我们要给movement加点数值,我们可以简单地在脚本中输入值,但如果要调整或改动,就不得不返回到脚本编辑器中,改变这个值再重新编译,浪费时间。解决办法是在脚本中新建一个公共变量,我们新建一个浮点数,叫做Speed
,在脚本中新建公共变量后,这个变量会出现在Inspector中,作为可编辑属性。使用公共变量时,可以在编辑器中作任何变更。我们接下来给movement乘上Speed。我们现在可以在编辑器中控制movement的值了,让我们保存并回到unity中。
当我们回到编辑器可以看到,PlayerController脚本现在有了个Speed属性,把它调为100,进入游戏模式。这样太快了,再调成10。现在我们可以移动角色了。
现在摄像机不会动,从它当前的位置看不到太多东西,所以我们要把摄像机和玩家角色绑定。
首先设置摄像机的位置,把它上移10个单位,然后倾斜45度,接下来把摄像机当做玩家对象的子对象。
这样就是典型的第三人称视角了,当我们移动玩家角色时,摄影机就会跟着移动。玩家角色旋转时摄影机也会跟着旋转。
让我们从一个可以同时看到玩家和摄影机的角度看看,移动玩家角色,旋转玩家角色,摄影机都会跟着动,现在重置(reset)玩家角色来测试一下。我们进入游戏状态,按住向上键。
好吧,看到现在的这个情况,不关联的时候反而更好一点,玩家角色疯狂旋转,摄像机焦点就跟着一起旋转。
我们先退出游戏模式。
不想普通的第三人称视角游戏,我们的玩家对象是三个轴都会旋转的,而不仅仅是一个轴。
在经典第三人称视角中,摄影机作为玩家跟随物,会始终与它当前父对象的坐标绑定,这个坐标是父对象在游戏中的坐标,会随着子对象的位移改变。所以我们不能把摄影机当做玩家对象的子对象,所以把它们分开。
我们的偏移量(offset value)就是玩家和摄影机之间的距离,现在我们需要关联摄影机和玩家对象,但不是通过设置子对象的方法。而是通过编写脚本。
Unity中,子对象的Transform属性会跟随父对象一起进行变化
我们想要的效果只需要摄像机的位置跟随球体移动,角度不需要,所以我们只能用脚本来实现这一效果,首先我们给摄像机添加一个脚本(大家应该还记得如何添加吧,不记得可以回去看第一节的内容,这里就不再重复介绍了),我们给脚本命名为CameraController,脚本语言选择C#。
用Add Component按钮选择New Script,我们用C#语言来写,把脚本命名为CameraController,然后点击Create and Add或者敲击回车键。
要注意的是,用这种方式新建脚本,会把脚本文件生成在项目视图的根目录下。
我们把它放在Scripts目录下,然后打开它。
我们需要两个变量:
一个公共变量GameObject代表玩家,
一个私有变量vector3持有偏移量,
偏移量是私有的,因为我们可以在脚本里设置它的值。
为了计算偏移值:
我们要取当前摄像机的transform position减去玩家的transform position。
所以,我们在一开始,让偏移值等于摄像机的transform position减去玩家的transform position,
然后后面的每一帧,都把摄像机的transform position设成玩家的transform position加上偏移值。
意思是:一旦我们使用键盘来控制角色移动,在每一帧摄像机展示拍摄画面之前,摄像机都会先移动到和角色相关的新位置。
就像摄像机依然是玩家角色的一个子对象,但是它不再满屏滚了。不过,Update不是最好的方式,Update确实每一帧都会执行,并且每一帧执行时,我们都可以跟踪到角色的坐标,然后重设摄像机的坐标。
但是,为了镜头、动画以及游戏状态的更新,最好使用LateUpdate。
LateUpdate每帧都会执行,跟Update一样,但它执行在所有的东西都Update之后。
这样,当我们设定摄像机的位置时,我们就能确定玩家角色在这一帧内已经完成移动。
那么我们再试一次,保存脚本后回到unity。
首先我们需要给玩家对象角色创建一个引用,只要把玩家角色对象拖拽进摄像机控制组件的Player一栏即可。
进入游戏模式,现在我们得到了想要的结果:摄像机跟随者旋转的球,但自己不再旋转了,即便是球滚出了边界也正常。
下一个任务我们要设定游戏区域,并创建特殊的捡拾物了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
public GameObject player;
private Vector3 offset;//一个公共变量GameObject代表玩家,一个私有变量vector3持有偏移量,
// Start is called before the first frame update
void Start()
{
offset = transform.position - player.transform.position;//开始,让偏移值等于摄像机的transform position减去玩家的transform position
}
// Update is called once per frame
void LateUpdate()//LateUpdate每帧都会执行,跟Update一样,但它执行在所有的东西都Update之后
{
transform.position = player.transform.position + offset;//后面的每一帧,都把摄像机的transform position设成玩家的transform position加上偏移值。
}
}
现在我们来设置游戏场地,我们的游戏场地也很简单,我们要在边缘围上围墙,避免球体掉出去,然后放置一些可收集物让玩家来收集。
首先新建一个游戏对象,命名为Walls,这将是我们所有墙体对象的父对象。
让我们看看层级(Hierarchy)视图的结构,项目结构和层级关系非常重要,我们必须了解它们内部的结构。
●我们直接创建文件夹以便于组织项目(project),配合Create菜单使用即可。
●我们通过使用游戏对象来组织层级(Hierarchy)关系。
在场景中,游戏对象可以包含游戏对象,不要害怕使用空游戏对象(empty game object)。
我们可以把它们当成是Hierarchy中的文件夹,
把它重置到原点,这一点非常重要。
使用前,要确保这个容器对象在原点。
现在要建墙了。
让我们先新建一个方块,这是我们的第一面墙,重命名为West Wall(west:西),把它重置到原点。
现在把West Wall作为Walls的子对象,让我们把场景视图的焦点放在墙体对象上,我们可以把光标放在场景视图内,并按F键。或者选择Edit-Frame Selected。
我们需要改变方块的尺寸,以便适应我们场地的边缘,设置方块的X,Y,Z变换尺寸宽.5(或0.5)高2 长20.5,现在我们可以用Translate工具把墙推到边上,或者可以在transform组件中输入值。
在这里我们可以把transform组件的position X值设为-10,这会把墙放在游戏场地的边缘,刚刚好。
要新建下一面墙,我们可以如法炮制,但我们又要设置它们的尺寸,我们的WestWall已经有完美的尺寸了。所以,让我们复制WestWall(Edit->duplicate)(duplicate:复制,重复的,副本,复制品),重命名为East Wall(east:东边)。
要把它放到位置只要去掉负号即可,现在它出现在东边了,现在我们复制EastWall,重命名为NorthWall,重置position X,以便它回到中心。
现在我们有两种做法,我们可以让它旋转90度。或者,既然它是立方体,我们直接重设,X为20.5,Z为0.5。现在它的方向正确了。我们可以直接拖动它,或者我们可以简单点,把transform组件的position Z设为10。复制North Wall然后命名为South Wall,然后把Z值设为-10,进入游戏模式试一下。
很好,墙做的很好。
记住,要尽早并且经常测试,不要等到最后才测试。我们退出游戏模式。选中/标记Player对象,然后把编辑器(editor)设为Local模式,再试一遍,注意观察Local模式中transform rotate如何变化,现在可以退出了。下一集我们会创建收集物。
下面我们来创建可收集物,新建一个方块,重命名为PickUp,重置它的transform组件,把场景视图的摄像机焦点对着这个捡拾对象,玩家对象挡着了。我们选择玩家对象,然后取消选择Name属性,这是游戏对象的Active勾选框,取消选择会让该游戏对象在场景中不可见,这会给我们一个清晰的工作空间来新建捡拾物对象。
方块埋进平面了,就像一开始的球体一样,方块也是标准形状1:1:1,所以我们把它上移半个单元,现在它落在平面上了,这个方块就是我们的捡拾物。
作为捡拾物,就应该吸引玩家,所以我们让它更有吸引力,我们先把它改小一点(0.5),这会让它看起来像漂浮在地面上一样,这些改动会提升它的特殊性。
还不够,让我们倾斜它,transform组件里的rotation属性中,每个坐标都给予45度倾斜,看起来更好了,但依然不够。我觉得足够吸引用户的一点是。它要是移动的,所以我们要旋转它。
为了做到这一点,我们需要用到脚本。
选中捡拾物对象,用Inspector(检测)中的Add Component,创建一个新的脚本,名为Rotator(旋转器),点击Create and Add确定,把它扔进scripts文件夹中,然后打开它。
我们希望方块旋转,并且想用脚本来实现,让我们把示例代码删掉,我们不需要用到力(forces),因此我们可以使用update而非fixed update。
我们已经在变换组件中设置了X,Y,Z轴的变换角度(45,45,45),但这些值不会自己改变,我们希望它们每帧都自己变,要让它们旋转,我们不想设置transform rotation,而是希望旋转transform。
下面我们在update方法内输入Transform,选择它并且按住Ctrl键或者command键,输入单引号,这回打开一个带transform搜索项的页面,选择transform,这样就打开了文档中的Transform页。
主要有两个方法来实现变换:一个是位移Translate一个是Rotate
Translate移动物体,Rotate旋转物体。
我们要用旋转Rotate,所以点击链接,这样就到了Transform Rotate页,注意两个声明和两个重写方法,一个用vector3,另一个用三个浮点型(float)值对应X,Y,Z,它们都有可选的参数Space,不过本期课程我们保持默认即可,并且我们会选择最简单的方式,仅仅使用vector3,让我们回到代码中。
让我们回到代码中,输入transform确保transform开头是小写的T,写Rotate(new Vector3(15,30,45))现在这个动作需要平滑一些,并且与帧率无关,所以我们要让vector3的值乘上Time.deltaTime,保存脚本,回到unity中。
进入游戏模式,可以看到我们的捡拾物在旋转了。
退出游戏模式。很好,我们现在有一个初始捡拾对象了。
接下来我们想把这些东西放满整个场地,但在这之前还有一个很重要的步骤:要把我们的捡拾物弄成一个预设(prefab)。
记住,一个预设(prefab)是一个资源,它包含一个游戏对象或对象组的模板或原型。我们用现有的游戏对象或对象组新建预设,一旦新建完成,我们就可以在当前项目的任何场景中使用这个预设。
有了一个捡拾物对象的预设后,我们就可以对某一个预设的实例(instance)做变更,或者改变预设资源本身。然后场景中所有的捡拾物对象都会做同样的变更。
所以,先新建一个文件夹放预设资源。
我们希望该文件夹在项目的根目录下,所以选择项目视图(project view),并确保没有其它任何项是被选中状态。接下来选择Create-Folder,重命名为Prefabs。
现在把捡拾物对象,从分层视图(hierarchy view)拖进Prefabs文件夹。
每当我们把东西从分层视图拖进项目视图时,就创建了一个新的预设资源。这些预设包含一个对象或对象组的模板或原型。
在散布收集物之前,我们要先创建一个新对象来持有这些捡拾物,并帮助我们组织分层视图。
我们新建一个游戏对象,叫它PickUps,检查并确保这个父对象在原点。然后把我们的捡拾物对象拖进去。
现在,我们想散布一大批捡拾物对象。
首先,确保我们是选中了这个捡拾物对象,而非选中了它的父对象。
现在,让我们点击场景视图右上角的gizmo,切换到俯视视角(top-down view),让我们回退一点点这样就能看到整个游戏场地了。
拉动捡拾物对象,但它不像我们希望的那样移动。
注意看这个方块如何跟着gizom移动的,它倾斜了。我们看到的是游戏对象在Local模式(本地模式)下移动。
我们希望PickUp对象移动时与地面平行。
把编辑器改为Global模式(全局模式),现在看看gizom的方向如何改变。
现在我们可以四处拖动游戏对象,并且它们始终与世界坐标轴关联。
那么我们多放一些,取第一个捡拾物。把它放在游戏场地的随便什么位置。我要放在最上面,选中该游戏对象,复制它。
你可以选择Edit-Duplicate或者用组合快捷键mac是command+D,pc是Ctrl+D,现在我们放置第二个预设实例,用快捷键创建更多的实例,然后放入场地。
注意,我的移动是与地面平行的。因为gizmo已经选为X/Z轴所在平面了。
放置完毕后,我们进入游戏模式,棒极了,它们一切正常。
还剩最后一件事,就是让它们更突出。
让我们改变它们的颜色,因此需要一个新的材质(material)。
想要简单点的话,我们直接选择现有的材质,复制它,重命名新的材质为PickUp,我们选中这个新材质,把它的albido颜色属性改为黄色(rgb:255,255,0),现在我们只需要把预设的材质替换掉,就可以改变颜色了。
我们有两种方法,我们可以改变其中某一个已经实例化的预设的材质,要这样做的话必须记住,override(覆盖)使用Apply all按钮,把这些改变应用到预设资源上,否则我们改变的仅仅是这一个实例。
另一种方式会更简单,我们直接改变预设资源的材质,让我们运行测试一下,看起来更好了。
下一节课我们将学习如何收集它们并统计分数。
我们希望玩家碰到可捡拾物时,把它们收集起来。
所以我们需要检测玩家对象和PickUp对象的碰撞。
我们需要这些碰撞触发一个新的行为,并且要检测这些碰撞事件,以确保我们拿到的是正确的捡拾物。
PickUp对象,玩家小球、地板和墙体,都有碰撞器,碰撞器会告知我们发生了碰撞。如果我们不检测碰撞的对象是否正确,我们可能会收集到错误的东西,我们可能会收集到地板或者墙体。
实际上,如果不尽早检测碰撞是否正确,我们可能会碰到地面,然后收集地面,最后掉入虚空,游戏就over了。
首先,我们需要玩家显示出来,所以勾选Active勾选框。
接下来,选择PlayerController脚本,打开编辑它。
注意一下,我们可以直接编辑脚本,无需在意游戏对象是否可见(active),现在我们打开它了,那么写什么呢?
我们输入collider(碰撞器),然后用组合快捷键搜索文档,但这里有另一种方式,我们回到unity,仔细看看玩家对象,需要关心的是球体的碰撞组件(collider component)。
每个组件的左上角,都有个翻折箭头,有个图标,有个是否可用的勾选框,还有个组件类型。右边就是对应的下拉齿轮菜单以及一个带问号的小书图标
这就是我们需要的,这是组件说明的快捷入口。
如果我们选中这个图标,就会被带到组件的说明,而不是脚本的说明。
我们可以通过阅读文档来学习如何使用组件。
当然,我们想知道的是,如何在代码中编写相关组件,所以直接切换到scripting,就到球体碰撞器的脚本说明了。
我们想检测碰撞并测试,本项目将使用OnTriggerEnter
想象一下,如果我们是个水管工(超级玛丽),我们起跳,收集一串拱形的硬币,当收集到第一个时弹起,然后落回地面,不是太简洁。
这些代码能让我们检测玩家和PickUp对象的接触,并且无需创建物理碰撞,示例代码不是很符合我们想要的,不过不要紧,我们可以改。
先拷贝这些代码,然后回到脚本编辑界面。
我们粘贴示例代码,因为是从网页拷贝的,所以需要修正缩进,这里我要确认缩进是tab缩进,并且所有的tab空格都对齐。
接下来看看这段代码,我们在用的是OnTriggerEnter方法,OnTriggerEnter会在玩家对象第一次碰到触发碰撞器(trigger collider)时被调用,我们会得到一个参数,一个我们碰到的触发碰撞器的引用,这个就是碰撞器,叫做Other。
这个引用让我们可以持有接触到的碰撞器,根据代码,当我们碰到另一个触发碰撞器时,会调用Destory(other.gameObject)
,销毁碰撞触发器关联的游戏对象。
一旦游戏对象被销毁,它和它的子对象以及它们所有的组件都会被一并移除。为了本次教学目的,我们不销毁这些对象,让它们不可见(deactive)即可。
就像创建PickUp对象时,让玩家对象不可见一样。
首先,让我们剪切 Destory(other.gameObject)
,然后粘贴在下方,稍后再用。
那么这么让PickUp对象不可见呢,我们有什么线索呢,我们可以通过other.gameObject定位到碰撞对立面的游戏对象,这些可以从示例代码中看到,我们要检测这个游戏对象是否是一个要使之不可见的PickUp对象,让我们用组合键看看GameObject类,看看能找到什么有用的信息。这就是GameObject的页面。
有两个重要的项是我们想要的,一个是tag(标签)
通过对比tag字符串,可以唯一标识游戏对象。
另一个是SetActive
这是让我们设置游戏对象可见的代码。
Compare Tag允许我们高效地比较任意游戏对象的tag值。
我们分别打开这三项,Tag允许我们辨识游戏对象
使用前,必须在Tags and Layers Panel(layer:层,层次 ;panel:面板)中声明tag,要判断一个tag的字符串值跟某个值是否相等,使用gameObject.tag是简单的,if(gameObject.tag == "Some String Value")
。
但还有更高效的内建方法,就是CompareTag。
使用CompareTag即可高效比较任意对象的tag值
我们把示例代码粘贴到代码备选区。
现在看看GameObject.SetActive,这是控制游戏对象是否可见的方法。
这个方法等效于在Inspector中,勾选Name属性旁的Active选框。
本例中,就像代码片段一样,我们要调用GameObject.SetActive(false)
来让PickUp对象不可见。
我们拷贝这部分代码,返回到脚本编辑器中,把它粘到备用区中,我感觉代码片段已经集齐了。那么开始写。
if(other.gameObject.CompareTag("PickUp"))
我们待会儿得定义这个tag,
other.gameObject.SetActive(false);
现在,每次碰撞到一个触发碰撞器,这段代码就会被调用,我们就能得到该碰撞器的引用,我们检测一下它的tag值。
如果tag值等于PickUp,我们就取other游戏对象并且调用SetActive(false),让该游戏对象不再可见。
现在我们不再需要备用区的代码了,留着会影响编译,所以我们删掉。
我们保存脚本返回Unity,检查错误。
第一件事情是为我们的PickUp对象设置tag值。
选择PickUp的预设资源,看下tag列表,找不到任何叫PickUp的tag。
所以我们需要添加一条,选择Add Tag,打开Tags and Layers面板,在这里自定义tags和layers。
注意这个列表是空的,要新建一个自定义tag,选择+按钮新增一行。
在新的空项内,在这里是tag0,输入PickUp,注意大小写要和代码里的值保持一致,如果担心输错,可以直接从代码里拷贝过来。
当我们再看预设资源时,会发现预设依然是Untagged(无tag)状态,这是因为选择Add Tag时,焦点由预设资源移动到Tag Manager,在Tag Manager中创建了tag。
现在我们要应用这个tag到预设上,再次选择Tag下拉,看看现在列表已经有PickUp了,选择这个tag,预设现在已经被标记为PickUp了。
因为是预设,现在所有的实例的tag值都是PickUp了。
现在测试下游戏,保存场景,进入游戏。
很好,tag已经设置为PickUp了,但仍然会反弹捡拾物方块,就像撞墙反弹一样。
退出游戏模式。
在探讨为何会反弹而不是拾起之前,我们需要搞清楚unity的物理系统,我将进入游戏模式来说明。
我们看看玩家对象和某个方块,题外话,我们可以同时选择多个游戏对象并观测它们,要这样做可以按住command/Ctrl键,然后选择游戏对象,选择多个游戏对象时,注意Inspector的变化,看它是如何展示通用组件和属性值的。
Inspector也允许多对象编辑,通过多对象编辑,我将一键禁用所有选择的对象的mesh renderer(网格渲染器),这样就能看见两个绿色的碰撞体积。
unity物理引擎中的碰撞时如何工作的呢,物理引擎不允许两个碰撞体积重叠,当物理引擎检测到任意两个或更多的碰撞器即将重叠,就会查看相应的对象,分析它们的速度、旋转和形状,计算出碰撞事件。
在碰撞中的一个关键因素是,碰撞器是静态还是动态的。
●静态碰撞器通常是场景中不会动的部分,例如墙体、地板或者院子里的喷泉。
●动态碰撞器是会动的部分,如玩家球体或者一辆车。
在计算碰撞时,静态的外观将不会被碰撞影响,但动态的对象会被影响。在我们的例子中,玩家球体是动态的或者说外观可移动,因此会被静态的方块弹开,就像被墙体弹开一样。
物理引擎是可以允许碰撞体积重叠的,这种情况下,物理引擎仍然会计算碰撞体积并记录碰撞重叠,但这样就不再是物理碰撞了,不会造成碰撞现象。
我们可以把碰撞器做成触发器,或触发碰撞器。
当我们把碰撞器做成触发器或触发碰撞器时,就可以利用触发器的OnTrigger事件信息,检测到接触行为。
当碰撞器是触发器时,你就能做很多有意思的事情,例如冒险游戏中,把触发器放在门口,玩家进入时,小地图更新,弹出消息“你发现了一个新的房间”;或者每次玩家走到角落,天花板都掉下来一只蜘蛛,这是因为玩家经过了触发器。
想知道更多关于OnCollision和trigger的信息,请看下方的相关课程。
我们的代码中要用OnTriggerEnter而不是OnCollisionEnter,因此我们需要把碰撞器体积改成触发器体积。
要这样做,必须先退出游戏模式。
让我们选择预设资源,然后看看盒碰撞器组件(box collider component),这里我们选择 Is Trigger,预设再一次生效。
现在所有PickUp对象都用上了触发碰撞器。
让我们保存场景,进入游戏模式。
一旦进入触发器,就捡起了物品,棒极了!
退出游戏模式,一切正常。
我们只剩一个问题:现在有个小错误,这和unity如何优化它的物理系统有关。为了性能优化,unity计算出场景中所有静态碰撞器的体积,然后存入缓存中。因为静态碰撞器不应该移动,这样就能避免每一帧的重复计算。
我们犯的小错误就是旋转方块,每次我们移动、旋转或者缩放一个静态碰撞器。unity都会重算所有的静态碰撞器,然后更新这些静态碰撞器的缓存,重算缓存消耗了太多资源。
我们可以随意移动、旋转或缩放动态碰撞器,并且unity不会重新缓存任何碰撞器体积,unity以为我们是要移动触发器。
我们只需要在移动前告诉unity,哪些碰撞器时动态的。我们要用到刚体组件(rigid body component),
任何带有碰撞器和刚体的游戏对象,都被认为是动态的。
任何带有碰撞器但没有刚体的游戏对象都被认为是静态的。
现在我们的PickUp游戏对象有一个盒碰撞器,但没有刚体,因此unity每一帧都会重算这些静态碰撞器。
解决方案就是给PickUp对象添加刚体,这样就能让方块变成动态碰撞器了。
让我们保存测试,方块穿过地板了,重力把它们往下拉。由于是触发器,也不会跟地面碰撞。
我们退出游戏模式,我们看看刚体组件,可以禁用Use Gravity,这样就能避免方块下落,然而这只是种不太好的方案,如果我们这样做,尽管重力会失效,但物理力仍然有效。
还有更好的方法,就是选择Is Kinematic,这样可以让刚体组件成为一个kinematic(运动)刚体。
kinematic刚体不会对物理力有反应,但可以活动,或者通过transform移动。这对很多带碰撞器的对象来说非常棒。
例如电梯、可移动的平台,还有带触发器的对象,例如我们的可收集物,需要利用transform来实现动画和移动。
想了解更多有关刚体和Is Kinematic的信息,请看下方的相关课程。
我们保存场景,进入游戏模式。
现在我们的行为一样并且高性能了。因此,静态碰撞器不该移动,例如墙体和地板,动态碰撞器可以移动,并且与刚体关联。
标准(Standard)刚体依赖物理力移动,运动(Kinematic)刚体依赖transform组件移动。
下一期课程,我们将统计捡拾物的数量,并制作一个简单的UI用来展示分数和信息。
计数,展示文本和结束游戏。
我们需要一个工具保存收集物的数量,
另一个工具用于在收集时增长这个值,
让我们把这个工具添加至PlayerController脚本,选中Player对象,打开PlayerController脚本编辑。
让我们新增一个私有变量来保存计数,它是个整型,因为我们的计数是整数,我们不会只收集对象的某一部分,我们叫它count。
在游戏开始时,它的值是0,然后每当我们收集到一个新的对象,就让它加1。
首先我们要把它设置成0,因为是私有变量,我们没办法在Inspector中设置它,这个变量只能在这个脚本内使用,因此我们需要在脚本中设置它的初始值。
要设置初始值有好几种方式,但这次课程中,我们将在Start方法中设置。
在Start中设置count=0,接下来当我们捡到新物品时,需要增加count,在OnTriggerEnter中,如果Other对象的tag值为PickUp就捡起它,而这就是增加count的代码,在设置Other对象为不可见状态后,我们让count的值等于count的旧值加1。
在unity的代码中,还有很多方式计数或增值,但这是最简单易懂的,并且本次课程就将使用这种方式,保存脚本,返回unity。
现在,我们可以保存并增长计数了,但没法展示它,如果游戏结束时能显示一个信息就更好了,为了显示文本,我们将用到unity的UI工具箱(Toolset)来展示文本和数值。
首先,通过Hierarchy的Create菜单,创建一个新的UI文本元素,我们看看新增了什么,似乎不只有我们想要的东西,我们不止有了一个UI文本元素,还创建了一个父画布(canvas)元素以及一个EventSystem游戏对象,它们都是UI工具箱所需的。
关于这些新增的东西,唯一重要的是要知道,所有的UI元素必须是一个画布的子元素,想了解更多关于UI工具,画布以及事件系统的信息,请看下面的相关资料,重命名为CountText,让我们改动一点点,默认的文本有点暗,我们把它改成白色(255,255,255),这样明显点,尺寸刚好。
让我们加点占位文本,CountText,我们希望游戏进行的时候,文字显示在屏幕左上角。我们可以看到当前的游戏视图中,UI文本显示在屏幕中心位置,这是因为文本元素,被固定在它的父元素:即画布的中间,值得注意的是UI元素的transform组件和其它游戏对象的不一样,UI元素的transform组件被替换成了rect transform(rect:矩形;矩形(Rectangular);)。
这是因为考虑到多功能的UI系统需要很多定制化的特性,包括锚定和定位,想了解更多关于rect transform的信息,请看下方相关资料。
有个最简单的方法,把计数文本元素移动到左上角,就是把它固定到画布的左上角,而非中心。
只需要点击显示在当前锚设置上的按钮
打开Anchors and Presets菜单(anchors:锚 presets:预设;预调;预置),当我们重定位文本元素时,还想设置轴心和基于新锚的坐标,所以我们同时按住Shift和Alt,选择左上角的按钮。
Shift-also set pivot:同时设置轴心
Alt-also set position:同时设置位置
我们给它点边距,在我们锚定至画布左上角时,我们同时把轴心设置在了左上角,想要留点间隔,最简单的方式就是改变rect transform的Pos X和Y值,我们设置成10和-10应该就好了,这样就不至于太高太偏了
下面我们用UI文本元素来显示计数值,打开PlayerController脚本编辑,在写关于UI元素的任何代码前,我们需要让脚本知道更多东西,UI工具箱的详细信息都在一个叫namespace(命名空间)的东西中,我们需要用到这个命名空间,就像用UnityEngine和System.Collections一样。
所以要在脚本最上面写:using UnityEngine.UI;
,有这个就能继续往后写了,首先新建个公共文本变量,起名countText,用它保存UI文本游戏对象上UI文本组件的引用。
我们需要给UI文本的Text属性设初值,我们同样在Start中写,countText.text = "Count: + count.ToString()";
,这行代码必须写在设置count值之后,Count必须有某个值给我们设置文本。
现在,我们每捡起一个新的对象,还需要更新text属性,因此在OnTriggerEnter中,在我们增加coount后要再次写,countText.text = "Count: + count.ToString()";
噢,在一个脚本中,同样的代码写了两次。这通常不是好的方式,一个让它变得更简洁的方式,是创建一个方法来做这件事,然后只需要简单地调用它即可,我们新建一个方法 void SetCountText,然后用圆括号和方括号,现在把其中一行代码复制粘贴进来,在原先的地方,我们调用这个方法,最后我们把另一行也替换成这个方法。
棒极了,保存然后切回unity。
现在我们可以看到PlayController脚本有个新的text属性,要关联Count文本的引用,只需要拖拽CountText游戏对象至槽内即可,unity会找到游戏对象的text组件并正确关联引用。
现在保存,进入游戏模式。啊哈,现在不仅能收集,还能统计分数了。我们退出游戏模式。
我们要在收集完所有的方块后弹出一条消息,我们需要一个新的UI文本对象来做这件事。
再次使用Hierarchy视图的Create菜单,创建新的UI文本游戏对象,重命名为WinText,注意新的UI文本元素是如何被自动加到canvas中的,再像之前一样,我们自定义组件的值,把颜色设为白色以提高辨识度,我们把文本放大一点,试试24,最后调整到中间位置,再添加一个占位符文本:WinText,我们希望文本显示在屏幕中心,但再高一点,避免挡住玩家对象,我们只需要调整rect transform的Pos Y值,因为UI文本元素默认就在画布的中心,改成75看起来就很好。
保存场景,切换至脚本编辑器。
我们要给UI文本元素添加一个引用,创建一个新的公共文本变量,命名为winText,现在让我们设置UI文本text属性的初值,这是设置为空字符串或两个无内容的双引号,这个文本属性一开始是空的,在SetCountText方法内,判断如果count大于或等于待/可收集物的总数,就让winText.text等于You Win,保存脚本并返回unity。
再次看玩家对象,PlayerController脚本有了个新的UI文本属性,我们还是拖拽WinText游戏对象到槽内来关联组件。
保存场景,进入游戏。
现在我们在捡拾游戏对象同时计数,最后,赢了,就像看到的那样,我们收集了所有的可收集物,然后显示了You Win文字。
下一期,也是本系列最后一期课程。
我们将编译游戏并让它独立运行。
现在,游戏已经完成了。
我们想把它发布出去,unity的一个超棒的特性就是支持时下许多主流平台,想了解更详细的编译(build)信息,请看下面的相关课程。
在编译游戏前,我们要先保存场景。
要编译游戏,必须先打开Build Settings窗口,我们可以选择File-Build Settings或使用组合快捷键,
Shift + command/Ctrl+B,这样就打开了编译设置窗口,我们当前的编译目标用Unity的logo表示,蓝色高亮是我们的焦点,右侧展示了当前所选平台的编译设置,我们想编译一个独立的应用,这是我们当前的编译目标,PC Mac和Linux,我们不用改变编译目标。
如果确实想改变编译目标,可以从列表里选择想要的平台,并点击窗口底部的Switch Platforms按钮,现在把编译目标置回独立播放器。
一旦我们选择了新的编译目标,需要把要编译的场景添加至Build Settings 窗口,我们可以点击Add Current 按钮来添加当前场景,或者可以从项目视图中拖拽任意场景到Build Settings窗口的顶部区域。
注意,我们无需包含项目中的每个场景,只包含我们需要的场景即可,甚至Build Settings窗口中没有任何场景也能编译,如果这样做unity会选择当前正在打开编辑的场景。
现在,我们准备好编译了。
我们回到Build Settings窗口,点击Build按钮,这会弹出一个弹窗,询问编译路径。
我喜欢把编译路径跟项目关联,因此我在项目下新建一个文件夹,命名为Builds。
这个文件夹应该被放在项目的根目录下,与资源和库文件夹同级。选中文件夹,给编译命名Roll A Ball,然后点击保存。
unity将会编译程序,并存至Builds文件夹中,unity会为mac编译app文件包,它包含了所有相关的数据和文件。
当编译Windows程序时,编译会得到.exe文件和一个数据文件夹,包含了所有的必要资源。要打开游戏,运行可执行程序即可。
现在我们已经在运行游戏了,然后,赢了。
那么本次系列课程中,我们学到了如何新建游戏对象、在场景中放置它们,添加新组件、通过编写简单的脚本自定义行为,我们了解了如何使用光源、摄像机、碰撞器、触发器、刚体,我们可以收集并统计游戏对象了,尽管这是个非常简单的例子,它依然涵盖了大部分基础主体,这对我们了解如何使用unity来说非常重要!