脚本是使用 Unity 开发的所有应用程序中必不可少的组成部分。大多数应用程序都需要脚本来响应玩家的输入并安排游戏过程中应发生的事件。游戏对象的行为由附加的组件控制。虽然Unity内置了许多组件,但是我们仍然可以使用脚本来创建自定义组件。Unity支持C#编程脚本语言(不熟悉的可以单独学习一下,看看我之前发布的C#课程),开发工具使用Visual Studio。我们一般情况下,在Unity中创建C#脚本,然后在VS中编写代码,最后回到Unity中运行。
创建C#脚本非常简单,需要我们在Project工程视图下创建,可以右击Project工程视图空白区选择“Create”-> “C# Script”,也可以通过菜单栏“Assets” -> “Create” - > “C# Script”来创建。新创建的脚本文件名称将处于选中状态,提示输入新名称。最好在此时输入新脚本的名称而不是稍后编辑名称。双击新创建的脚本文件,默认将使用 Visual Studio打开。新创建的脚本文件不是一个空白文件,而是一个继承“MonoBehaviour”的C#类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
注意,类名和文件名必须相同,且该脚本必须附加到游戏对象上才能被运行。
这个MonoBehaviour 是Unity的一个内置类,该类中有两个方法:Start和Update。
我们知道游戏就是一个无限循环,每一次的循环都会接收和响应用户输入,渲染游戏对象(模型网格),播放游戏动画,特效声音或背景音乐等等。每一次的循环我们可以理解为一帧(一次渲染)。我们知道动画的原理就是通过一帧一帧的静态图片播放而实现的。为了能够让人眼感觉到这不是图片,而是动画的效果,1秒钟之内至少要播放24张连续图片才能达到最终的效果。游戏的本质也是如此,游戏引擎会在1秒钟之内循环渲染N次场景内的所有游戏对象,这个数值N就是我们所说的FPS(Frames Per Second)。FPS越大,游戏视觉效果越好。通常情况下,手机游戏的FPS=30就可以了,PC游戏的FPS=60就可以了,VR游戏的FPS=90就可以了。当然,如果我们的硬件允许的话,达到120的FPS是最棒的效果。这个话题我们不在细说了,回到MonoBehaviour中的Start和Update方法上来。Update 方法是放置代码的主要地方,用于处理游戏对象的帧更新(FPS值就是1秒钟update被执行了多少次)。这可能包括角色移动、响应用户输入,基本上涉及游戏运行过程中随时间推移而需要处理的任何事项。在处理游戏业务逻辑的时候,经常会使用附加到游戏对象身上的各种组件(游戏对象的大部分行为都是通过组件完成的),我们会通过组件来完成不同的功能,以及组件与组件之间的关联关系。为了使 Update 方法正常运行,在进行任何游戏操作之前,通常需要确保游戏对象身上的组件能够正确获取,相当于初始化操作。所以,在游戏开始之前(即第一次调用Update方法之前),Unity 将调用Start方法,此方法是进行游戏对象初始化的位置。我们可以在这个方法中来获取游戏对象上面的各种组件。
这里需要注意的,我们的C#脚本虽然是一个类,但是不能为其添加构造方法。这是因为C#脚本类对象的构造由Unity来实例化。如果尝试为脚本组件定义构造方法,将会干扰 Unity 的正常运行,并可能导致项目出现重大问题。如果想要在构造方法里面完成的代码逻辑,可以移植到Start方法内。
接下来,我们创建一个新工程(ScriptDemo),然后创建一个“Test.cs”脚本来测试一下Start和Update方法。创建文件成功后,文件就会自动处于重新命名的状态,我们只需要输出“Test”即可。输入完毕后,双击就会自动使用Visual Studio打开“Test.cs”脚本。
接下来,我们要做的就是添加如下的两行代码。
// Start is called before the first frame update
void Start()
{
Debug.Log("start method...");
}
// Update is called once per frame
void Update()
{
Debug.Log("update method...");
}
为了减少文档篇幅长度,我们省略了其他代码。
Debug.Log 是一个简单的命令,只是将消息输出到 Unity 的控制台Console面板。
由于默认创建的场景中只有“Main Camera”和“Directional Light”两个游戏对象,因此我们暂且将“Test.cs”的脚本文件附加到“Main Camera”游戏对象上面吧。这个操作非常简单,我们先在Hierarchy层次面板中选中“Main Camera”游戏对象,然后就会在Inspector检视面板中显示“Main Camera”的各种属性控件。最后我们将Project视图中的脚本文件拖动到“Main Camera”的Inspector检视视图上面即可。
接下来我们Play当前工程,应该会在 Unity Editor底部的Console 视图中看到此消息。
如果找不到Console 视图,可以在菜单栏Window -> General -> Console 重新打开。
我们发现,Start方法执行了一次,而Update方法则再不停的执行直到停止运行工程。在上面的截图中,我们会发现“Collapse”的按钮,它会重叠相同代码方式的输出。而重叠的次数则会在最右端以数字的形式展示处理。例如,上图中的update方法输出“update method...”被重复执行了94次。那么这个update方法一秒钟执行多少次呢(FPS值)?
我们可以使用代码来计算1秒钟内Update执行的次数来获取:
public class Test : MonoBehaviour
{
private int count = 0;
private float detaTime = 0.0f;
void Update(){
//Debug.Log("update method...");
// 累加帧数
count++;
// 累加时间
detaTime += Time.deltaTime;
// 累加时间超过1秒
if (detaTime >= 1.0f)
{
// 计算FPS值
float fps = count / detaTime;
Debug.Log("FPS = " + fps);
// 帧数和累加时间清零
count = 0;
detaTime = 0;
}
}
}
上面的代码逻辑非常简单,就是使用一个变量来累计Update方法执行的次数,同时累计当前执行的时间,Time.deltaTime 是Unity提供给我们的一个属性值,它代表当前帧和上一帧之间的时间差。因此,我们只需要累加这个属性值就可以获取update执行的时间长度detaTime。由于我们的代码运行都是毫秒级别的,所以当detaTime超过等于1秒钟的时候,我们就可以使用count/detaTime获取FPS值了。为什么不是detaTime等于1秒钟直接输出count值呢,因为这个detaTime不能非常非常精准的等于1,多数情况它的值可能会是1.001之类的,因此我们还需要使用count/detaTime做一个除法计算,得到一个包含小数的FPS值。
接下来,我们就Play当前工程,查看控制台视图输出。
我们发现这个FPS值并非是一个固定的数值,大概是在300-400之间(跟电脑配置有关)。
在Game窗口的右上角有一排按钮,其中有一个是“Stats”,我们点击一下就会出现一个新弹窗,在这个新弹窗上面,我们就也能看到FPS数值。
两者大致相同。既然FPS不是一个固定的数值,那么我们在上面提到过手机端(30),PC端(60)以及VR端(90)的FPS值,其实就是一个期望的平均值而已。我们工程中展示的FPS值是300-400之间,明显高于PC端的平均FPS值60啊?这么高的FPS值我们完全不需要啊(浪费电脑性能),我们如何设置其为60呢?这个问题稍微有些复杂。
首先,我们需要了解FPS值与显卡,显示器的关系?
显卡负责绘制图像再输送到显示器上,每个图像可以称为一帧。游戏帧数(FPS)就是显卡每秒绘制图像的数量。显示器则会将显卡绘制的图像显示到屏幕上。显示器刷新频率就是显示器画面每秒呈现图像的数量。一般情况下,我们电脑屏幕的刷新频率是60HZ,也就是说显示器1秒钟显示60张图像,当然我们也可以手动修改这个数值。那么,现在就存在一个问题,如果FPS值和显示器刷新频率不一样会出现什么情况?如果FPS高于显示器的刷新频率,那么多余的图像将不会被显示到屏幕上。因为动画的产生就是通过连续图像产生的,如果屏幕只显示一部分图像的话,那么这动画效果显然是不连贯的。如果FPS低于显示器的刷新频率的话,那么动画效果就会出现卡顿现象,达不到连贯效果。请注意,第一种情况是因为丢失图像而造成动画不连贯,后一种情况是显示太慢造成动画卡顿不连贯。想到这里,大家应该比较清楚了,最好的效果应该是让两个值大致相等,最好是FPS稍微大于显示器刷新频率,这样呈现的动画效果是最好的。为了解决这个问题,出现了垂直同步技术。开启垂直同步后,电脑就会等待上一张图像渲染完成后才会发出开始下一张渲染的命令,也就是说让显卡按照显示器的输出去绘制图像(强制游戏帧数最大不超过刷新率)。这样做唯一的缺点就是限制了显卡性能的发挥。但是,大家一定要明白的是,越高的FPS值越能让动画播放越流畅,给玩家非常棒的游戏体验,这一点是不变的。
在Unity中可以通过Application.targetFrameRate 指定的游戏使用的指定的FPS平均值。默认的targetFrameRate是 -1 数值,表示游戏应以平台的默认帧率渲染。
此默认速率取决于平台:
- 在独立平台上(例如PC),默认的帧率是可实现的最大帧率。
- 在移动平台上,默认帧率通常为每秒 30 帧。
- 在 WebGL 上,默认值允许浏览器选择帧率来匹配其渲染循环时序。
- 使用 VR 时,Unity 将使用 SDK 指定的目标帧率并忽略游戏指定的值。
看到上面的解释,我们就不难理解刚刚运行的工程的FPS是300-400之间了,因为PC平台的默认是能实现多大就实现多大,这个就跟个人电脑显卡档次有关系了。既然这样的话,我们就将Application.targetFrameRate 设置为 60,因为我的电脑显示器刷新频率就是60。
我们在“Test.cs”的脚本中的“Start”方法来设置FPS值,如下所示:
void Start()
{
//Debug.Log("start method...");
Application.targetFrameRate = 60;
}
再次Play我们的工程,查看控制台输出以及“Stats”弹窗。
我们发现两者大致也是相同的,都是近似等于60,这就说明我们设置起作用了。先不要着急,我们忽略了一个垂直同步技术。在Unity中我们可以通过QualitySettings.vSyncCount来设置是否开启垂直同步技术,0代表不开启,大于0代表开启。同时我们还可以点击菜单栏Edit->Project Setting->Quality,有一个VSync Count参数,该参数同样表示垂直同步。
它有三个值可以选择:Don’t Sync,Every V Blank和Every Second V Blank
Don't Sync:数值0,不设置垂直同步,通过Application.targetFrameRate来指定帧率
Every V Blank:数值1,帧率为60,Application.targetFrameRate无效
Every Second V Blank:数值2,帧率为30,Application.targetFrameRate无效
默认值是Every V Blank(数值1)。按照官方的解释,如果设置了 QualitySettings.vSyncCount 属性,将忽略targetFrameRate,而游戏将使用 vSyncCount 和平台的默认渲染率来确定目标帧率。上文中也给出了vSyncCount设置值与帧率的关系。另外,移动平台下这个vSyncCount不会起作用,而是使用targetFrameRate来设置帧率。还有VR平台下,vSyncCount也不会起作用,而是通过VR SDK来设置。这一点对于Unity开发游戏来讲,差距确实很大。但是当我们将VSync Count设置为Every Second V Blank的时候,FPS值没有改变。不知道到底是哪里的问题,貌似vSyncCount从开始就没有起作用,不讨论了。