安卓-Unity-游戏开发入门手册-全-

安卓 Unity 游戏开发入门手册(全)

原文:Beginning Unity Android Game Development

协议:CC BY-NC-SA 4.0

一、编程概念

编程就是解决一个问题并为其定义一个解决方案。每一个细节都是精心制作的,试图将解决方案传达给计算机。对于一些,特定的指令被给予计算机系统,以执行导致期望的解决方案的任务。

幸运的是,有高级编程语言来帮助我们用更接近英语而不是 0 和 1 的语言编写这些解决方案。确切地说,编程语言是一套规则,它提供了一种指示计算机执行什么操作的方法。

让我们考虑一个类比。(参见图 1-1 。)假设我们有几个水果,必须用它们做沙拉。第一步是分析和定义问题。具体来说,必须识别输入数据(水果)并将其转化为预期输出数据(沙拉)。

第二步是规划。程序员使用的一种技术是流程图,它是一个问题的逐步解决方案的图形表示。它们帮助我们关注程序逻辑,而不是我们将要使用的编程语言的适当语法。

第三步是实际编写程序。第二步中的逻辑现在必须转换成计算机可以理解的东西。存在各种集成开发环境(ide)来帮助程序员用他们选择的编程语言编码。IDE 就像一个文本编辑器,但是有几个附加的特性来帮助程序的开发,比如自动完成或者调试器。自动完成是一种功能,通过这种功能,句子会自动用 IDE 下一步期望的关键字来完成,同时调试器会帮助运行程序并可能找到错误。

第四步也是最后一步是测试程序。可能存在一些错误,为了检测它们,必须输入不同类型的测试数据,并且输出必须与预期的结果一致。调试是指检测、定位和纠正错误。这些错误可能是语法错误或逻辑错误,例如,前者是计算机无法理解的拼写错误的指令,后者就像告诉计算机重复操作,但不告诉它如何停止重复。

本章将引导你了解许多流行的编程概念,但是为了简单起见,很多内容将被搁置。

img/491558_1_En_1_Fig1_HTML.jpg

图 1-1

做沙拉

Note

您还不能运行下面的代码片段,但是不要担心。在我们进入后续章节的 Unity 编码之前,它们只是给你一些基本的理论。

1.1 变量、常数和类型

在编程中,数据可以有许多不同的类型。由于我们将使用 Unity 游戏引擎,代码片段(部分)将用 C#来表达。在这一章中,我们将只考虑四种类型的数据:整型、浮点型、布尔型和字符串型。

1.1.1 整数数据类型

整数(没有小数部分)的数值可以用整数形式表示。整数值也可以是负值。

10, 2667, -50, 0

1.1.2 浮点数类型

有小数部分的数值可以用浮点形式表示。浮点值也可以是负值。

3.9874, 1.245, -112.245, 0.0932

布尔数据类型

布尔值具有值truefalse

1.1.4 字符串数据类型

字符集可以用字符串形式表示。

"Unity", "192.168.100.0", "The big brown fox"

变量

可以对各种数据执行不同的操作。从长远来看,利用变量可能是个好主意。变量是一种保存特定类型数据的特殊容器。打个比方,一个变量可能是一个衣柜,用来装衣服,而不是其他东西——比如说,不是厨房用具。在 C#中,声明变量的经典方式如下:

<variableType> <variableName> = <value>;

在 C#中,特定数据类型的变量不能保存另一种数据类型的值。为了清楚起见,要声明上述数据类型的变量并为其赋值,必须设置类似于以下内容的内容:

int myInteger = 10;
float myFloat = 3.9874f;
bool myBool = false;
string myString = "Unity";

注意,对于浮点变量,f必须附加在 float 值的末尾,以确保它被解释为 float(而不是 double 数据类型)。在 double 数据类型中,值以 64 位(最多 16 位)存储,这对于我们将要处理的值的类型来说不是很有用。使用 float 数据类型还可以提供更高的整体性能,因为数据是以 32 位(7 位数)存储的。

如果变量在初始化时没有值,例如bool condition,它们所使用的数据类型的默认值将被赋给它们,在本例中为false。对于整数和浮点数,默认值将分别等于00.0f。对于字符串,这将是""(一个空白和空字符串)。

如果一个变量已经包含一个值,并且给它赋了一个新值,那么这个变量的内容将被覆盖,并且它将包含新赋的值,直到程序结束,除非给它赋了另一个值。

int pin = 1234;
pin = 4321;
// pin now holds a value of 4321.

注意,如果一个变量已经被声明了,就没有必要用它的数据类型来引用它。这将在 1.7 节中详细讨论。

常数

常数就像变量一样,只是在声明后不能修改,并且将保持它们初始化时的值。声明常量的过程与声明变量的过程类似,除了关键字const必须放在数据类型字段之前。

const <variableType> <variableName> = <value>;

评论

注释是脚本中程序员可读的注释或解释,通常使用户更容易理解代码的某个部分是做什么的。注释通常会被编译器/解释器忽略。在 C#中,注释可以用单行或多行的方式编写。

// This is a single-line comment.
/* This is a
multiline comment.*/

1.2 阵列

数组是一种类似于变量的数据存储结构形式,因为它被声明为保存特定的数据类型。然而,与变量不同,数组可以保存多个数据值。创建数组时,会为其设置一个预定义的大小。因此,数组将保存与其大小相等的数据值的数量。

与变量一样,数组中索引(位置)处的数据值可以被相同类型的其他数据值读取、修改或替换。数组的索引从 0(第一个数据值)开始,最后一个索引将等于数组的大小减 1,因为第一个索引不是 1。

1.2.1 声明和创建数组

如果声明的数组没有等号后面的部分,那么它的大小将为零。以后可以修改数组的大小,但是存储在每个索引中的数据值将被重置为数组使用的数据类型的默认值。

<arrayType>[] <arrayName> = new <arrayType>[<size>];

// create an integer array of a size of 5 named firstArray
int[] firstArray = new int[5];
// create a string array with a size of 0 named secondArray
string[] secondArray;

// creating a new string array with a size of 10 and assigning it to secondArray
secondArray = new string[10];

声明数组的另一种方式是在开始时用值初始化它们。

<arrayType>[] <arrayName> = {<value0>, <value1>};
int[] firstArray = {5, 10, 20, 35, 45};
string[] secondArray;

secondArray = {"abc", "def", "ghi"};

要获得数组的长度,即它可以存储多少个值,可以调用Length方法。

int[] firstArray = {5, 10, 20, 35, 45};
int arraySize = firstArray.Length; // 5

1.2.2 设置、获取和修改数组中的值

在这一节中,我们将看看如何设置和修改数组中的索引值。

<arrayName>[<index>] = <value>;
<variable> = <arrayName>[<index>];

在下面的例子中,值13将被存储在正在使用的整数数组(firstArray)的第三个位置(索引 2)。通过将存储在索引 0 ( 7)和索引 4 ( 6)的数据值相加来获得值13,其中大小- 1指的是索引 4。

int[] firstArray;
int size = 5;

firstArray = new int[size];
firstArray[0] = 7;
firstArray[4] = 6;

firstArray[2] = firstArray[0] + firstArray[size - 1];

1.3 算术运算符

在编程中执行算术运算是很常见的。基本的算术运算是加、减、乘和除。可以使用这些运算符来处理数值。这也适用于保存数值的变量和常量(intfloat)。算术运算符必须有两个操作数,并且它们的倍数可以链接成一个等式。操作数是与操作符一起使用的任何东西。例如,加法语句中的数字是操作数,加号是运算符。遵循运算顺序:首先计算括号中的运算,然后是除法和乘法运算,最后是加法和减法运算。

1.3.1 加减乘除

这些可以像你在数学课上学到的一样使用。

1 + 1; // 2
9 -6; // 3
4 * 2; // 8
60 / 12; // 5
6 * 2 + 3; // 15
5 * (3 - 1) + 1; // 11

变量和常数

如前所述,算术运算可以通过使用保存数值的变量或常数来完成。

int number = 1;

number + 1; // 2
number + 8 - 6; // 3

算术运算的结果也可以存储在数字类型的变量中。

int number = 2;

number = number * 2; // 4
number * 4; // 16

number = number + 56; // 60
number / 12; // 5

模量

这是另一个有用的算术运算符。它返回整数除法的余数。正如前面的操作符一样,它也可以用于变量和常量。

11 % 4; // 3

复合赋值运算符

有一种简单的方法可以将一个操作的值赋给该操作中涉及的变量。

|

例子

|

相等的

|
| --- | --- |
| 绵羊+= 5; | 羊=羊+5; |
| 绵羊-= 5; | 羊=羊-5; |
| 绵羊-= 5; | 羊=羊 5; |
| 绵羊/= 5; | 羊=羊/5; |
| 绵羊% = 5; | 绵羊=绵羊% 5; |

快速递增和递减

变量可以快速递增或递减 1。

int count = 5;

count++; // 6
count++; // 7
count--; // 6

一元运算

一元运算是只有一个操作数的运算。由于一元运算只有一个操作数,因此它们在包含它们的其他运算之前被求值。这通常适用于正运算符和负运算符。例如,+8 + -2 和+2 - 3 分别相当于 8 - 2 和 2 + 5。

铸造

例如,当执行算术运算时,可能会获得不期望类型的输出。例如,如果程序员将一个变量声明为一个整数,他们将在其中存储 5 * 1.61f 的结果,将获得一个不能存储在该变量中的浮点结果。

这是选角出现的情况之一。基本上,强制转换将特定数据类型的值转换为相同的值,但属于指定的数据类型。将浮点数转换为整数不会对值进行舍入。只返回整数部分。

int perfectInt;
float badFloat;

badFloat = 5 * 1.61f; // 7.55f

perfectInt = (int)badFloat; // 7

在前面的例子中,我们将变量badFloat的值转换为perfectInt。然而,我们可以直接执行算术运算,并在perfectInt变量中直接将其转换为整数。对于需要浮点结果的算术运算,不需要将涉及的整数(无论是变量还是原始值)转换为浮点数据类型

铸造在某些情况下也可能不适用。例如,由字母组成的 stringvalue 不能转换为整数或浮点数。

1.4 逻辑运算符

C#中的逻辑运算符是用于连接表达式的符号,因此生成的复合表达式的值取决于原始表达式的值以及所用逻辑运算符的含义。

简单的布尔表达式

布尔表达式总是产生truefalse。例如,问一个问题,比如 5 大于 2 吗?会导致yes。在编程中,这将被写成 5 > 2,返回的结果布尔值将是true

|

标志

|

解释

|
| --- | --- |
| == | 等于 |
| != | 不等于 |
| > | 大于 |
| < | 小于 |
| >= | 大于或等于 |
| <= | 小于或等于 |

4 > 5 // false
180 < 450 // true

60 >= 70 // false
567 <= 550 // false
330 != 80 // true

45 > 45 // false
40 < 40.1f // true
90 >= 90 // true
1 == 2 // false

"Cat" == "Dog" // false
"cat" == "Cat" // false
"Shoes" == "Shoes" // true
"Car" != "Plane" // true

1.4.2 和(&&)

AND逻辑运算符有两个操作数。如果操作数的结果是true,它将返回一个值true

(1 == 1) && (5 > 4) // true
(3 > 4) && (80 >= 79) && (50 > 40) // false

1.4.3 或(||)

OR逻辑运算符有两个操作数。如果它的操作数中至少有一个导致true,它将返回一个值true

(3 < 4) || ( 255 == 256) // true
(4 > 5) || (40 < 50) || ("abc" == "def") // false

1.4.4 不是(!)

NOT逻辑运算符只接受一个操作数。它将一个布尔值转换成它的对应物。即如果其操作数的布尔值为true,则返回false

!(1 == 2) // true
!((4 <= 8) && (6.1 >= 6)) // false

1.5 选择

通常,脚本由许多行代码组成。指令序列被一个接一个地执行。有时,我们希望只根据特定的条件运行特定的指令。这就是选择的来源。通过问一些问题,我们可以让计算机系统根据答案做一些事情。控制结构帮助我们改变编程中的流程。每个控件结构都需要缩进放在其中的代码。缩进实际上只是空格或制表符。

1.5.1 如果是,则控制结构

在这个选择控制结构中,将在代码运行的正常流程中检查所有指定子句的条件,直到找到导致true的第一个条件,或者如果所有子句都导致false。如果某个子句的条件导致true,那么它的代码块将会运行,而所有其他子句将会被跳过,因为只有一个指定的子句可以运行。

使用一个非常简单的if then else,一些代码只有在指定条件导致true时才能执行。

if (condition) {
 // Do something
}
string apple = "fruit";
int numberOfFruits = 0;

if (apple == "fruit") {
 numberOfFruits++;
}

如果if子句中的条件导致false,也可以指定else关键字来运行另一个代码块。

if (condition) {
 // Do something
} else {
 // Do something else instead
}

string apple = "fruit";
int numberOfFruits = 0;
int numberOfVegetables = 0;

if (apple == "fruit") {
 numberOfFruits++;
} else {
 numberOfVegetables++;
}

此外,ifelse子句可以一起使用,仅在一个块中创建多个条件。注意最后一个else从句可以省略。

if (condition) {
 // Do something
} else if (another condition) {
 // Do something
} else {
 // Do something else instead
}

string gender = "M";
int males = 0;
int females = 0;
int undefined = 0;

if (gender == "M") {
 males++;
} else if (gender == "F") {
 females++;
} else {
 undefined++;
}

最后,if then else控制结构可以“嵌套”到其他if then else控制结构中。

if (condition) {
 if (condition) {
 // Do something
 }
}

string species = "human";
string furColor = "";
int brownFur = 0;
int blackFur = 0;

// != is interpreted as "is not equal to"
if (species != "human") {
 if (furColor == "brown") {
 brownFur++;
 } else if (furColor == "black") {
 blackFur++;
 }
}

1.5.2 案例控制结构

一个case选择控制结构与一个变量一起工作,根据它包含的值,可以运行特定的代码。将验证所有的case子句,直到其中一个子句的条件导致true;否则,将运行最后一个子句中的代码default。如果其中一个case子句有一个导致true的条件,那么相应的代码块将会运行,所有剩余的子句都不会被验证,因此会被跳过。每个子句中都需要break关键字。这种形式的控制结构在必须执行许多检查的情况下很有用,因为它提供了比链接许多if - else语句更优雅的解决方案。

switch(variable) {
 case value:
 // Do something
 break;
 default:
 // Do something else then
 break;
}

string apple = "fruit";
int numberOfFruits = 0;
int numberOfVegetables = 0;
int numberOfExceptions = 0;

switch (apple) {
 case "fruit":
 numberOfFruits++;
 break;
 case "vegetable":
 numberOfVegetables++;
 break;
 default:
 numberOfExceptions++;
 break;
}

1.6 迭代

通常,脚本的某些部分可能需要重复多次。为了使程序更容易阅读、修改和调试,并且在某些情况下,为了使算法更有效,可以使用循环控制结构,而不是复制代码 x 次。基于预定义的条件,位于循环控制结构子句的花括号之间的代码将保持运行。然而,如果一个条件总是保持true,循环将无限运行并导致程序崩溃。

while 循环

本质上,在一个while循环中的代码不断重复自己while一些条件是true

while (condition) {
 // Do something
}

在下面的例子中,while循环运行了五次。

int number = 1;

while (number < 6) {
 number++;
}

用于循环

编写一个for循环比编写一个while循环要复杂一些。与后者不同,它不只是有一个条件。相反,它包括声明一个变量,设置一个条件,如果条件不为真,则导致循环停止运行,最后,设置变量每次增加/减少的量。

for (<declareVariable>; <condition>; <variableIncrement>)
{
 // Do something
}

例如,for循环可用于执行从 2 到 10 的所有偶数的求和。基本上,变量i被声明为一个整数,最初被赋予值 2,每次循环运行时,它的值将增加 2,并添加到变量 sum,直到i的值大于 10。

int evenSum = 0;

for (int i = 2; i <= 10; i += 2) {
 evenSum += i;
}

1.6.3 foreach 循环

这是对for循环稍加修改的版本,更常用于数组之类的结构。与其编写一个for循环来遍历一个数组的所有值,不如编写一个foreach循环,这样可以更快地完成工作。不像常规的for循环,其中数组的大小必须为条件部分写入,这对于foreach循环是不必要的。

foreach (<dataType> <newVariableName> in <arrayName>) {
 // Do something
}

例如,假设我们有一个由float值组成的数组,并希望得到该数组中所有元素的总和。foreach循环将遍历数组值的每个元素,并且每次自动将当前元素分配给临时float变量temp,该变量本身将被添加并存储在变量sum中。

float[] values = {66.3f, 346.21f, 45.8f, 890.8f, 556.99f};
float sum = 0.0f;

foreach (float temp in values) {
 sum += temp;
}

作为参考,这里是一个普通的for循环中的等价函数。注意values.Length返回值 5,但是循环不能等于这个,因为数组的最后一个索引会是 4。

float[] values = {66.3f, 346.21f, 45.8f, 890.8f, 556.99f};
float sum = 0.0f;

for (int i = 0; i < values.Length; i++) {
 float temp = values[i];
 sum += temp;
}

1.6.4 继续和中断

关键字continue告诉循环跳过任何剩余的代码,直接跳到循环的下一次迭代。一个简单的例子是遍历一个食物数组,并简单地跳转到下一个迭代实例,以避免在当前位置的数组元素不是水果时增加一个integer变量。

string[] food = {"fruit", "human", "fruit", "vegetable", "shoes"};
int fruits;

foreach (string current in food) {
 if (current != "fruit") {
 continue;
 }
 fruits++;
}

关键字break立即结束循环的执行,并跳转到循环之后和循环之外的代码行。例如,一旦在数组中找到一个特定的值,就可以用它来结束循环的执行,因为继续检查该数组的其余元素是徒劳的。

string[] alphabets = {"a", "b", "c", "d", "e", "f"};
string alphabetToSearchFor = "c";

int alphabetRealPosition = 0;

for (int i = 0; i < alphabets.Length; i++) {
 if (alphabets[i] == alphabetToSearchFor) {
 alphabetRealPosition = i + 1;
 break;
 }
}

1.7 功能

您可能拥有数百或数千行代码的脚本。虽然您可以在一个连续的流程中编写所有内容,但是将代码包装在一个函数中做一件特定的事情会使代码看起来更整洁,也更容易管理。在前面的“迭代”部分(1.6),我们学习了如何通过循环来减少代码重复。但是如果一些代码必须在程序的不同部分再次运行多次呢?这就是函数的用武之地。

基础知识

函数包含代码,在脚本中需要的部分,可以“调用”它们来执行编写它们要做的操作。

void <functionName> () {
 // Do something
}

注意,不返回值的函数,也就是说,脚本不期望从它们那里得到值,具有类型void。一个void函数完成它的任务,然后将控制返回给调用者。

一个简单的函数可以用来向用户显示一些东西。当然,功能不会自己运行。这就是为什么在我们代码运行的主流程中,我们必须调用它。Debug.Log函数将输出我们传递给它的string的日志消息。

PrintHelloWorld();

void PrintHelloWorld() {
 Debug.Log(“Hello World”);
}

参数

有时,我们可能还希望将值传递给函数,以便它们使用这些值执行操作,例如,如果我们希望将该函数用于类似的操作,但使用不同的值。我们传递给函数的值被称为参数,它们必须与实际参数具有相同的数据类型。

void <functionName> (<parameterType> <parameterName>) {
 // Do something
}

修改前面的示例,我们现在希望函数打印作为参数传递的三个数字的总和。传递的值由函数中的局部名称标识,以便可以引用它们。求和的结果是 11,并将在日志消息中输出。

int a = 5;
int b = 4;

PrintSum(a, b, 2);

void PrintSum(int first, int second, int third) {
 Debug.Log(first + second + third);
}

返回一个值

在前面的例子中,我们的函数只执行一个操作。它们没有真正意义上的“返回”一个值。未声明为void的函数可以返回一个值,该值可以分配给变量或用于控制流程。例如,我们可以直接调用if子句中的函数,而不是让boolean变量保存一个在if else条件下使用的函数返回的boolean值。注意,当一个函数返回一个值时,该值前面必须有return关键字,任何代码,如果留在该函数中,都不会运行。

<functionType> <functionName> (<parameterType> <parameterName>) {
 // Do something
 return <value>;
}

再拿前面的例子来说,这次我们把函数的返回值赋给c

int a = 5;
int b = 4;
int c = 0;

c = Add3Numbers(a, b, 2);

int Add3Numbers(int first, int second, int third) {
 return first + second + third;
}

让我们考虑另一个例子,这次用一个boolean值。如果变量eat不包含值fruit,函数将返回false

string food = "fruit";
int numberFruits = 0;

if (itsAFruit(food)) {
 numberFruits++;
}

bool itsAFruit(string eat) {
 if (eat == "fruit") {
 return true;
 }
 return false;
}

全球和本地

基本上,在脚本的所有函数外部声明的变量被称为global,而在函数内部声明的变量被称为localglobal变量可以从脚本中的任何地方访问,而local变量只能在声明它们的function中访问。

在下面的例子中,a是一个global变量,因为它和它的值可以在脚本中的任何地方被访问、读取和修改。即使一个global变量作为一个parameter被传递给一个function,那个function引用被传递参数的名字只有它自己知道,因此bc都是local参数,只能在函数temporaryFunction()内部被访问、读取和修改。

int a = 100;

void temporaryFunction(string b) {
 int c;
}

1.8 协程

协程与函数非常相似,除了当你调用一个函数时,它在返回之前运行完成。函数中发生的任何动作都不是随时间推移而发生的,因为函数中的代码是在一次帧更新中运行的。虽然可以使用循环和检查来实现这一点,但使用协程通常更有用。与每次执行都必须手动调用的函数不同,协程可以设置为在指定的延迟自动运行。协程的返回值是强制的,不能赋给任何变量或直接使用。

1.8.1 定义和调用协程

一个协程只能被调用一次,其中的代码将在一些特定的关键字定义的帧数下运行。

StartCoroutine("<coroutineName>"); // or StartCoroutine(<coroutineName>());
IEnumerator <coroutineName>(<parameters>) {
 // Do something
 yield return <returnValue>;
}

协程总是被定义为IEnumerator,并且必须总是有那个yield return <returnValue>;行。该yield return <returnValue>;行之后的任何代码都将在指定为returnValue的帧数之后运行。

例如,为了在游戏中每运行一帧就将integer变量numberFrames的值增加 1,可以使用下面的代码。yield return null;相当于yield return 1;,在我们的例子中,它导致coroutine的最后一行每帧运行一次。

int numberFrames = 0;
StartCoroutine("CountFrames");
IEnumerator CountFrames() {
 yield return null;
 numberFrames++;
}

1.8.2 用秒代替帧

coroutines可以等待若干秒,而不是等待帧。

IEnumerator <coroutineName>(<parameters>) {
 // Do something
 yield return new WaitForSeconds(<numberOfSeconds>);
}

演示这一点的一个很好的例子是在特定的时间间隔检查一些条件,而不是在每一帧检查。下面指定的for子句无限运行。if子句将以作为参数传递的值为间隔运行。

string weather = "rainy";
bool happy = false;
float numberSeconds = 0.5f;
StartCoroutine(CheckWeather(numberSeconds));
IEnumerator CheckWeather(float amount) {
 for ( ; ; ) {
 yield return new WaitForSeconds(amount);
 if (weather == "sunny") {
 happy = true;
 } else {
 happy = false;
 }

 }
}

二、Unity 简介

正如你已经知道的,我们将使用 Unity 游戏引擎来开发游戏。Unity 已经用 C++ 编程语言编写,但是它的脚本应用编程接口(API 我们实际用来编码游戏的东西)是用 C#写的。Unity 不仅仅可以用来做游戏。它甚至可以用于为电影、建筑或汽车制造创建可视化效果。

使用 Unity 而不是从头开始编写游戏的好处是,Unity 已经提供了许多现成的工具来帮助我们制作游戏,例如物理或照明。与其他游戏引擎相比,Unity 更受欢迎,这使得解决 bug 或学习如何做一些事情更容易,因为很可能一些东西已经出现在互联网上。我们希望在 Unity 中开发的任何游戏都是一个项目。

2.1 创建 Unity 帐户

为了使用 Unity,您必须在 https://id.unity.com 设置一个帐户。同样,你可以使用你的谷歌或脸书账户创建一个账户(图 2-1 )。

img/491558_1_En_2_Fig1_HTML.jpg

图 2-1

创建 Unity 帐户

2.2 下载 Unity 和附加软件

Unity 有四个许可证:个人版、高级版、专业版和企业版。个人拥有其他营业执照提供的大部分功能,并且是免费的(图 2-2 )。想要更多功能而不是个人优惠的个人可以购买 Plus。如果从你的游戏开发公司或组织获得的收入超过特定的阈值,必须购买专业或企业许可证。使用个人许可证的最大好处是能够定制游戏的闪屏(当游戏开始时),使用深色编辑器 UI,更好的支持,优质资源的可用性(尽管不是绝对必需的),以及更多的诊断/分析。更多信息请访问 https://store.unity.com/

img/491558_1_En_2_Fig2_HTML.jpg

图 2-2

统一计划

2.2.1 统一集线器

Unity Hub 是一个应用,可用于下载多个版本的 Unity,以及它们各自的模块(如果需要的话),并且包含您正在处理或已经创建的项目(云或本地)的列表(图 2-3 )。

img/491558_1_En_2_Fig3_HTML.jpg

图 2-3

Unity Hub 项目

对于 MacOS 或 Windows 用户,可以从 https://unity3d.com/get-unity/download/ 下载 Unity Hub,对于 Linux 用户,可以从 https://forum.unity.com/threads/unity-hub-v2-0-0-release.677485/ 的论坛获取一个可执行文件。下载完成后安装或运行即可。

接下来,你必须下载一个版本的 Unity 编辑器(实际的游戏引擎)。对于本书中的示例,您必须使用任何 2019.3.x 版本。首先使用您之前创建帐户时使用的凭据登录 Unity Hub。然后,点击您的个人资料图片或姓名首字母(右上角)。转到管理许可证并激活新的 Unity 个人许可证。最后,转到 Installs,点击 Add,选择 Unity 的 2019.3.x 版本,并选择您希望安装的模块。因为我们将开发一款手机游戏,所以至少要选择 Android 和/或 iOS 构建支持模块。注意,如果你没有苹果电脑,你将无法制作 iOS 游戏。对于 Android 构建支持,也可以查看 Android SDK & NDK 工具和 OpenJDK(图 2-4 )。文档和其他东西都是可选的。

img/491558_1_En_2_Fig4_HTML.jpg

图 2-4

从中心下载 Unity 编辑器

您还需要一个集成开发环境(IDE)来编写脚本。Visual Studio 代码是一个轻量级的优秀选择。可以从 https://code.visualstudio.com/ 下载。我们稍后会将它链接到 Unity 编辑器。目前,只需安装它。

2.2.2 创建空项目

在 Unity Hub 中,点按蓝色的“新建”按钮,给您要创建的项目命名,然后选取一个存储位置。选择 3D 作为模板。点击创建,稍等片刻,项目应该打开(图 2-5 )。

img/491558_1_En_2_Fig5_HTML.jpg

图 2-5

创建新项目

首先,去编辑➤首选项➤外部工具,并确保您使用内置的 JDK,SDK 和 NDK。应勾选相应的复选框。您还必须在外部脚本编辑器选项卡中指定 vscode 选项,以便在适当的 IDE 中编写和修改脚本。该选项位于外部工具标题的正下方(图 2-6 )。

img/491558_1_En_2_Fig6_HTML.jpg

图 2-6

检查首选项

2.3 基本窗口

Unity 提供了几个具有特殊功能的窗口来帮助开发者开发游戏。你已经知道项目在 Unity 中意味着什么。游戏项目包含场景。例如,把场景想象成游戏的关卡。例如,当很多东西目前不需要的时候,加载所有的东西是一个坏主意。在场景中,你可以设计你的关卡并使它们可玩。

现在,场景中出现的所有东西都被称为游戏对象(在本书的第三章会有更多的介绍)。当你制作游戏时,你可能会需要图像、声音、3D 模型等。您在项目中导入的所有内容都是一种资源,无论您是否在场景中使用过它。

我将浏览 Unity 中一些最常用的窗口。您的空项目看起来应该是这样的(图 2-7 ):

img/491558_1_En_2_Fig7_HTML.jpg

图 2-7

空旷的场景

您可以通过点击 Unity 编辑器右上角的小布局按钮来更改这些窗口的布局(图 2-8 )。您也可以通过拖动它们的边缘来调整它们的大小。

img/491558_1_En_2_Fig8_HTML.jpg

图 2-8

布局

2.3.1 项目窗口

如果您使用的是默认布局,通常可以在编辑器的底部找到它。项目窗口(图 2-9 )是您已经创建或导入到 Unity 项目中的资源和目录的集合。您也可以使用搜索栏搜索整个项目,按名称或类型查找资源。

img/491558_1_En_2_Fig9_HTML.jpg

图 2-9

项目窗口

层次结构窗口

层级窗口(图 2-10 )基本上包含了当前在编辑器中打开的场景中出现的所有游戏对象的列表。默认情况下,它包含一个摄像头和一个光源,你会在第三章中了解更多。它位于编辑器的左上角,在默认布局中。

img/491558_1_En_2_Fig10_HTML.jpg

图 2-10

默认情况下的“层次结构”窗口

尝试从层级窗口中创建一些基本的游戏对象,通过左键单击小加号图标或右键单击窗口中的任意位置,弹出一个菜单供选择。新创建的游戏对象将立即出现在层级窗口中(图 2-11 )。

img/491558_1_En_2_Fig11_HTML.jpg

图 2-11

创建 3D 游戏对象

2.3.3 场景窗口

场景窗口主要用在关卡设计中,必须放置游戏对象来创建游戏区域。可以使用鼠标在场景中导航,并在场景窗口中直接更改场景中的对象。注意红色代表 x 轴,绿色代表 y 轴,蓝色代表 z 轴(图 2-12 )。

img/491558_1_En_2_Fig12_HTML.jpg

图 2-12

带立方体的场景窗口

可以通过在场景窗口中左键单击对象来选择对象。也可以尝试右键单击并拖动来旋转,或者使用滚轮来放大和缩小。最后,您可以点击并按住滚轮,移动鼠标进行平移。如果在“检查器”窗口中左键单击并选择对象,它们在“场景”窗口中也会显示为选中状态。

有七个工具(图 2-13 )可以帮助你在场景视图中执行操作。首先是手工具。选中时,当您单击它们时,不会选择任何对象。相反,鼠标左键将用于在场景中平移,就像前面描述的按住滚轮一样。

img/491558_1_En_2_Fig13_HTML.jpg

图 2-13

七个场景工具

第二个场景工具是移动工具(图 2-14 )。当在场景窗口中选择一个对象时,可以拖动箭头将其向特定轴的方向移动。物体也可以被两个小方块拖动,同时沿两个轴移动。

img/491558_1_En_2_Fig14_HTML.jpg

图 2-14

移动工具

第三个场景工具是旋转工具,其工作方式与移动工具相同,只是它用于旋转游戏对象。通过沿着彩色圆圈拖动,你可以在三个轴中的一个轴上旋转游戏对象,或者沿着灰色圆圈,同时在两个轴上旋转(图 2-15 )。

img/491558_1_En_2_Fig15_HTML.jpg

图 2-15

旋转工具

第四个工具是缩放工具(图 2-16 ),它允许我们缩小或放大选定的游戏对象。通过沿着彩色方块拖动,你可以沿着相应的轴放大或缩小游戏对象,通过从游戏对象中心的小方块拖动,你可以同时沿着所有轴均匀地放大或缩小游戏对象。

img/491558_1_En_2_Fig16_HTML.jpg

图 2-16

缩放工具

第五个场景工具是矩形工具,用于移动和缩放 2D 对象。当我们需要移动或缩放图像、按钮和 2D UI 元素时,我们将会用到它。

第六个工具结合了移动、旋转和缩放工具的功能。我们将跳过它做什么,现在使用第七个也是最后一个(多重)工具(图 2-17 )。

img/491558_1_En_2_Fig17_HTML.jpg

图 2-17

多功能工具

在场景视图中,也可以单击窗口右上角的小圆锥体,使视角垂直于轴。例如,尝试单击红色圆锥体,使视角垂直于 x 轴,本质上,您看到的内容现在将以二维形式出现,以 z 轴和 y 轴为界(图 2-18 )。

img/491558_1_En_2_Fig18_HTML.jpg

图 2-18

沿 x 轴方向观察

你也可以点击小圆锥下面的文字,在透视或正交视图之间切换,这取决于你正在制作的游戏类型。

场景视图左上角的第一个按钮允许你以另一种形式查看游戏对象。转向线框的一个很好的例子是调整汽车的车轮(图 2-19 )。

img/491558_1_En_2_Fig19_HTML.jpg

图 2-19

场景窗口中的线框着色

窗口顶部的小 2D 按钮使所有东西都出现在 2D,这对于处理 2D UI 元素或 2D 游戏很有用。其他按钮主要用于打开或关闭诸如灯光、音频或效果之类的东西。

最后,默认情况下还有两个按钮,标记为“中心”和“全局”。单击前者时,将显示“中心”或“轴心”。当使用移动或旋转等变换工具时,箭头将被放置在所选游戏对象的中心或枢轴点。现在在编辑器中尝试一下,以便更好地使用它们。尝试移动、旋转和缩放 3D 对象,并练习到目前为止所学的一切。

例如,如果场景中有两个立方体并且都被选中,如果第一个按钮被设置为中心,当前工具将被放置在与两个立方体等距的位置(图 2-20 )。

img/491558_1_En_2_Fig20_HTML.jpg

图 2-20

两个游戏对象的中心点

如果使用 Pivot,该工具将被放置在两个立方体之一的中心,因此称为轴心点。在这种情况下,轴心点将由首先选择两个立方体中的哪一个来确定。如果首先选择左边的,则其中心将是支点(图 2-21 )。

img/491558_1_En_2_Fig21_HTML.jpg

图 2-21

两个游戏对象的枢轴点

现在转到全局和局部模式。基本上,当第一个按钮的模式设置为全局时,所有工具相对于世界的定位都是相同的。红色箭头会一直指向右边(x 轴);绿色箭头会一直指向上(y 轴);蓝色箭头总是指向前方(z 轴)。这些将不取决于所选游戏对象的位置、旋转或比例。

然而,对于本地模式,该工具将始终取决于所选游戏对象的位置、旋转或缩放。在某种程度上,这就像说蓝色箭头“是游戏对象的 z 轴”,这意味着蓝色箭头将指向游戏对象的向前移动,而不是法线,它跨越了 Global 定义的世界空间。在下面的例子中,立方体被轻微旋转,因此它的“向前”指向上方一点(图 2-22 )。

img/491558_1_En_2_Fig22_HTML.jpg

图 2-22

选择游戏对象的本地模式

在全局/局部按钮旁边还有一个小磁铁图标。这可用于打开/关闭网格捕捉。当刀具定位设置为全局时,可使用此选项。网格捕捉允许您在所有轴上以 1 为单位移动对象,从而将游戏对象的位置捕捉到场景中最接近的整数位置。

游戏视图

如果使用默认布局,可以在场景窗口旁边找到它。它主要用于在导出之前测试游戏项目,因为它向第三方用户展示了实际导出游戏的表示(图 2-23 )。

img/491558_1_En_2_Fig23_HTML.jpg

图 2-23

游戏视图

图 2-23 显示了如果有人现在尝试玩你的空游戏会是什么样子。显示 1 允许您在不同的显示之间切换,以查看特定地点的外观。

自由外观选项卡允许您设置分辨率/比率,这样您就可以更容易地看到游戏如何在具有特定分辨率/比率的显示器上呈现。自由视角占据了游戏窗口的整个尺寸。您也可以创建新的分辨率或比率(图 2-24 )。

img/491558_1_En_2_Fig24_HTML.jpg

图 2-24

在游戏视图中设置新的分辨率/比率

使用缩放滑块,您可以放大或缩小游戏窗口,尽管如果您没有实现类似的东西,这在实际游戏中是不可用的。播放时最大化可以设置为开或关。如果被激活,它会显得更白,这样你就可以全屏测试你的游戏了。请注意,在这一点上,如果您打开或关闭它,什么都不会发生。

统计数据允许你预览一些关于游戏的信息,比如 CPU 使用率或者当前顶点的数量(图 2-25 )。

img/491558_1_En_2_Fig25_HTML.jpg

图 2-25

在游戏视图中显示统计数据

Gizmos 允许你预览更多显示给最终用户的东西,比如碰撞器这样的组件,你将在接下来的章节中了解更多。最后,进入游戏模式。

当你想测试你的游戏而不导出它的版本时,你可以在编辑器中完成。游戏窗口的顶部有三个图标。第一个(最左边的)是播放按钮。当你点击那个按钮时,编辑器变暗,按钮变成蓝色,你就进入了所谓的播放模式。在游戏模式下,你可以像普通用户一样玩游戏,如果他们有那个游戏的版本的话。要退出播放模式,只需再次单击播放按钮。

在游戏模式下(图 2-26 ,可以点击旁边的按钮——暂停按钮,暂时暂停游戏。再次点击暂停按钮将恢复暂停。最后一个(最右边的)按钮是步进按钮,每当你点击它,它会自动播放一帧并暂停游戏,如果它还没有暂停的话。你仍然可以点击暂停按钮继续游戏。如果我们想知道每一帧发生了什么导致了一个 bug,那么 Step 按钮是很有用的。

img/491558_1_En_2_Fig26_HTML.jpg

图 2-26

进入播放模式

请注意,您在播放模式下所做的任何更改都是暂时的。也就是说,例如,如果您在播放模式中移动对象,当您返回正常编辑模式时,更改将恢复到进入播放模式之前的状态。

2.3.5 检查员窗口

如果您使用的是默认布局,检查器就是屏幕右侧的大垂直三角形。它包含了许多关于所选游戏对象的信息,基于附加到这个(这些)游戏对象的组件。默认情况下,所有游戏对象都有一个变换组件(图 2-27 和 2-28 )。大多数组件的字段或属性的值都可以在此窗口中调整。

img/491558_1_En_2_Fig28_HTML.jpg

图 2-28

多维数据集的典型默认检查器窗口:第二部分

img/491558_1_En_2_Fig27_HTML.jpg

图 2-27

多维数据集的典型默认检查器窗口:第一部分

我们作为例子的立方体,或者我们创建的一个新的立方体,默认情况下应该有这些组件。标签和层的用途将在最后几章中解释,你看到的组件的使用将在第三章中记录。当静态旁边的复选框被选中时,无论我们做什么,我们的游戏对象都不会在运行时移动或物理改变。这有性能增益。建议您尝试更改一些组件的属性,看看会有什么不同。

2.3.6 统一素材商店

如果您还记得在您学习项目窗口的章节中提到的素材定义,有几种方法可以将素材导入 Unity。其中最常见的是通过打开.unitypackage文件。Unity 包,或以扩展名.unitypackage结尾的文件,是一个包含多个素材的压缩文件。

对于本节,我们将主要关注从素材商店下载和导入素材,而不是从第三方非官方来源。素材商店是一种市场,你可以浏览和下载游戏的素材。把它想象成 Unity 的 Google Play for Assets。

要访问素材存储,您必须转到窗口并单击素材存储选项卡。或者直接按 Ctrl+9。您可以调整“素材存储”窗口的大小或将其放置在某个位置。它应该看起来有点像全屏显示的图 2-29 。

img/491558_1_En_2_Fig29_HTML.jpg

图 2-29

素材商店

您可以使用搜索栏搜索素材,并使用价格和类别等过滤器。出于本教程的考虑,我们将下载并导入一个名为 Simple Input 的包,它碰巧是免费的。使用定价过滤器仅针对免费素材(图 2-30 )。

img/491558_1_En_2_Fig30_HTML.jpg

图 2-30

搜索素材

然后单击弹出的第一个结果(它应该看起来像前面截图中的第一个结果)。在素材页面上,您可以阅读描述、预览将导入的文件,以及查看屏幕截图或评论。准备好了就点击下载。当它完成下载时,你现在应该在那个按钮上看到 Import 而不是 Download(图 2-31 )。点击它。

img/491558_1_En_2_Fig31_HTML.jpg

图 2-31

简单输入系统素材

Unity 现在将解压软件包。最后,点击导入。一旦所有内容都被导入,素材将出现在您的项目窗口中(图 2-32 )。

img/491558_1_En_2_Fig32_HTML.jpg

图 2-32

导入素材——我们项目中的简单输入系统

控制台窗口

控制台是一个只读窗口,可以显示日志、警告或错误(图 2-33 )。在测试游戏项目的代码时,您通常会将值输出到控制台,以确保一切按预期运行。这叫做调试。Unity 也可能警告你一些事情,这些事情可能不会马上真正影响你的项目,但从长远来看会引起问题。至于错误,你必须修正它们;否则,您将无法运行或构建您的游戏。

img/491558_1_En_2_Fig33_HTML.jpg

图 2-33

控制台中记录、警告和错误消息的示例

当您单击控制台窗口中的日志/警告/错误时,更多详细信息将出现在屏幕底部,主要指示与相关消息相关的语句和行号。如果您在控制台中双击一条消息,负责的脚本将在 Unity 已分配使用的默认文本/代码编辑器中打开。

清除按钮将删除控制台窗口中的所有消息,除了那些必须完全修复才能继续开发游戏项目的消息(图 2-34 )。只要启用了“播放时清除”,每次进入播放模式时,它都会自动执行“清除”按钮的操作。类似地,每次 Unity 成功构建游戏的可执行文件时,Clear on Build 就会运行。

img/491558_1_En_2_Fig34_HTML.jpg

图 2-34

控制台窗口中按类型和流派显示的消息数量

Collapse 选项卡将把相同的控制台消息编辑在一条消息中。实例的数量将显示在控制台窗口最右侧的小圆圈中。错误暂停将在收到错误时暂停游戏模式,最后一个下拉按钮编辑器允许您选择调试消息的来源,例如,可能来自连接到您计算机的设备。

搜索栏允许您搜索特定的消息,最后三个彩色小按钮允许您选择想要的调试消息类型。例如,单击黄色按钮将停止控制台窗口中弹出的所有警告消息。但是,自您上次点击清除后收到的每种类型的日志消息的数量仍将继续显示在它们各自的按钮旁边。

构建设置

这可以从文件➤构建设置中访问。在构建设置窗口中,您可以切换到您想要导出游戏的平台,并调整更多选项,这些选项将在以后的章节中更全面地介绍(图 2-35 )。您当前所在的平台将在其标签前显示一个小的 Unity 徽标。对于未来的项目,切换到 Android 平台。您将很快看到如何配置和构建。在“构件设置”窗口中拖放您想要的场景。第一个是玩家打开游戏时看到的。最后,保存您的场景和项目是一个很好的做法。从“文件”选项卡执行此操作。

img/491558_1_En_2_Fig35_HTML.jpg

图 2-35

“构件设置”窗口

三、游戏对象、预设、材质和组件

如第二章所述,用 Unity 搭建的游戏通常都是场景布置。这些场景通常包含许多对象来增强它们,并使游戏具有交互性和趣味性。目标是有一个具体的游戏,有坚实的机制,良好的游戏性,和良好的图形。

3.1 游戏对象和预设

在 Unity 中,场景中的物体被称为“游戏物体”创建新场景时,它包含主摄影机和平行光。这些是游戏对象。你在层级窗口中找到的每个对象都是一个游戏对象。如果你创建一个立方体,它也是一个游戏对象。

现在,如果你有多个场景,并且在所有的场景中使用一个公共的游戏对象,如果你可以在所有的场景中拖放那个对象,而不是为每个场景从头开始配置,那就太好了,对吗?这就是预制构件的用武之地。基本上可以保存一个游戏对象的版本,拖放到其他场景中(图 3-1 )。你只需要在项目窗口中拖放一个游戏对象,它就会变成一个预置。

img/491558_1_En_3_Fig1_HTML.jpg

图 3-1

制作预制品

你刚刚变成预设的游戏对象现在在层级标签中会有一点蓝色。至于你的预设,你可以加载另一个场景,并将其拖入新打开场景的层次或场景标签中。

你也可以直接改变一个预置。你只需要在项目窗口中双击它。场景窗口现在将有一个蓝色的背景,你现在将能够看到该预设的子对象(如果它有任何子对象的话),并对其属性以及子对象的属性进行更改。

在做了想要的改变之后,你可以通过点击层级窗口左上角预设名称旁边的小箭头或者使用快捷键比如 Ctrl 来保存它们;前一个动作会自动保存对预设所做的更改,并返回到之前打开的场景。然后,该预设的所有实例将在发现它们的所有场景中用这些变化进行更新(图 3-2 )。

img/491558_1_En_3_Fig2_HTML.jpg

图 3-2

打开预设

如果你对你在场景中变成预设的游戏对象进行了更改,在检查器窗口中有一个覆盖按钮,你可以点击它使你所做的更改应用到预设,从而应用到该预设在其他场景中的每个实例(图 3-3 )。你也可以将属性还原为预设版本的属性。这不是一个必要的步骤,你也可以在一个场景中有一个游戏对象,它来自一个预置,但是没有完全相同的属性。

img/491558_1_En_3_Fig3_HTML.jpg

图 3-3

覆盖预设属性

如果你制作了一个预置,把它放在一个场景中,并且不希望它被将来对预置的修改所更新或覆盖,你可以通过在检查器中右键点击它,或者点击“解包预置”或者“完全解包预置”来使预置实例成为一个独立的游戏对象。例如,如果你在一个场景中放置了一个预置,并且它的属性与原始预置中的属性完全不同,你可能希望那个版本是完全独立的。

你可以制作预置的预置,例如,通过创建一个游戏对象,它有多个预置作为孩子,并且它自己变成一个预置。前一个选项将只移除第一层嵌套预设,而后一个选项将移除所有嵌套预设。还需要注意的是,删除一个预设并不会删除场景中该预设的实例。这些预设实例将成为独立的游戏对象,并在层级中沿着它们的名称带有红色(图 3-4 )。嵌套预设在游戏中非常有用,在游戏中所有的敌人都以相似的方式行动,并且共享相同的基础特征。

img/491558_1_En_3_Fig4_HTML.jpg

图 3-4

删除预设

3.2 组件

每个游戏对象都有组件。组件基本上是一个可以附加到游戏对象上的模块。除了空游戏对象已经提供的属性和特性之外,组件还提供了几个新的属性和特性。有些组件需要游戏对象上的其他组件才能正常工作,而有些组件是强制性的,即使你想创建一个空的 3D 对象。当您选择游戏对象时,您将在检查器窗口中看到所有附加到它们的通用组件,也可以通过单击它们的名称来收缩或扩展(图 3-5 )。

img/491558_1_En_3_Fig5_HTML.jpg

图 3-5

多个组件

在检查器窗口中,您可以更改组件的值。您还可以单击组件右上角的三个点,以调出更多选项,例如 Reset,它将为组件分配默认值,就像您刚刚添加它一样。

也可以通过单击“复制组件”将组件值同时复制并粘贴到两个组件中。在选择了包含您想要覆盖其值的组件的游戏对象后,单击粘贴组件值。你也可以复制组件并直接粘贴到另一个游戏对象上。

还应该注意到,一个游戏对象可以有一个以上的特定组件的实例,尽管也有一些例外。你也可以通过上下拖动或者使用三个点并点击上移或下移来重新排列游戏对象上的组件。

在三点图标旁边,还有一个按钮。单击它允许您使用相应组件的预设。

要获取组件的手册或文档,只需单击左侧预设按钮旁边的问号图标。将打开一个浏览器选项卡,显示合适的相关信息(图 3-6 )。

img/491558_1_En_3_Fig6_HTML.jpg

图 3-6

查看文档

最后,你可以通过点击添加组件按钮并浏览或搜索你想要添加的组件,或者通过使用编辑器菜单(图 3-7 )来给游戏对象添加新的组件。

img/491558_1_En_3_Fig7_HTML.jpg

图 3-7

添加组件

3.2.1 转换

如果你按照第二章中的学习,你已经从场景窗口中与变换组件进行了交互,并且可能对它有一个坚实的概念。每个游戏对象都需要转换组件。与整数或浮点数据类型不同,转换组件由三组Vector3值组成。您可以将Vector3视为由三个值组成的浮点数组。从第一个索引开始,或者从左边开始,这三个值分别由“x”、“y”和“z”字符表示。变换组件的三组Vector3值是所选游戏对象的位置、旋转和缩放(图 3-8 )。

img/491558_1_En_3_Fig8_HTML.jpg

图 3-8

变换组件

x 轴用红色表示,水平方向从左(-)到右(+)。y 轴用绿色表示,垂直方向从下(-)到上(+),z 轴用蓝色表示,从后(-)到前(+)。所有的轴都互相垂直。

位置Vector3XYZ值表示游戏对象在世界中的位置。您可以更改这些值,方法是将它们直接输入到特定轴名称旁边的文本框中,或者在“检查器”标签本身中将光标从轴名称向右(+)或向左(-)。

旋转Vector3xyz值表示游戏对象在世界中的旋转,比例Vector3xyz值表示游戏对象在世界中的位置。

但是,如果选定的游戏对象是另一个游戏对象的子对象,那么它的位置、旋转和缩放都是相对于其父对象的。例如,下面是两个不同立方体的变换组件(图 3-9 和 3-10 ):

img/491558_1_En_3_Fig10_HTML.jpg

图 3-10

立方 2 的变换组件

img/491558_1_En_3_Fig9_HTML.jpg

图 3-9

立方 1 的变换组件

现在,如果我们必须使立方体 2 成为立方体 1 的子对象,通过在层次窗口中将它的游戏对象拖动到后者的游戏对象上,下面是它的变换看起来的样子(图 3-11 ):

img/491558_1_En_3_Fig11_HTML.jpg

图 3-11

Cube2 的转换组件(如果它是 Cube1 的子元素)

因为它与立方体 1 处于相同的位置,相对于其父体,立方体 2 的Vector3位置在所有轴上都将为 0。因为它的旋转在任何地方都是 0,为了保持这一点,立方体 2 必须在其相对的 y 轴上减去 90 度,以保持该值为 0,因为立方体 1 在其自身的 y 轴上旋转了 90 度。至于比例,这是不言自明的:立方 2 比立方 1 大两倍。

相机

在电子游戏中,相机相当于我们的眼睛。你在游戏中感知到的一切都被一个叫做摄像头的组件“看到”。通常,在一个特定的时间,你在一个单人游戏中只能启用一个主摄像头,以向玩家展示他们所能看到的。如果你在玩第三人称游戏,摄像机将会在你所控制的主要角色的后面,因此,会产生另一个实体正在监视和跟踪后者的印象。在第一人称游戏中,相机充当你所控制的角色的眼睛。

在一个空场景中,通常应该有一个相机组件已经连接到一个游戏对象(主相机),如果你没有对它做任何修改的话(图 3-12 )。

img/491558_1_En_3_Fig12_HTML.jpg

图 3-12

相机组件

清除标志选项允许您从预定义的列表中选择应该显示在摄像机空白区域的内容。

  • 默认情况下,它被设置为天空盒,正如您稍后将了解到的,它由总共六幅图像组成,这些图像通常相互补充,形成一种围绕场景的立方体。

  • 此外,您可以选择纯色选项,并在下面的背景属性中选择一种颜色。

  • 如果选择“仅深度”,空白区域中不会显示任何内容,如果没有被任何内容覆盖,“不清除”将持续显示上一帧中存在的内容。

剔除遮罩是一种机制,它基于已分配给组(称为层)的图形元素来控制渲染到该相机的内容。

接下来,您可以选择是让相机使用透视视图还是正交视图。如果您选择正交(默认情况下,这是透视),相机看到的一切都将在某种程度上 2D 视图。

  • 使用正交相机,您可以调整相机的大小,即它在特定时刻或帧可以“看到”的区域。

  • 使用透视相机,您可以选择它的视野,从字面上看,这是调整它从中心“看到”的“角度”的过程。

您还可以调整视野是沿着水平(x)轴(从上到下)还是沿着垂直(y)轴(从左到右)。

尽管如此,对于透视视图,您可以通过勾选物理摄像机来选择使摄像机更具可配置性(图 3-13 )。这将允许您调整更多的设置,如相机的焦距和传感器大小。顺便说一句,要改变相机的位置和旋转/方向,你必须修改游戏对象的转换组件中相应的值,该组件连接了相机组件。

img/491558_1_En_3_Fig13_HTML.jpg

图 3-13

透视照相机

现在是裁剪平面。Unity 的 1 个单位相当于现实世界中的 1 米。对于相机,“剪裁平面”设置有一个近值和一个远值。这两个值代表游戏对象到摄像机的最小和最大距离,以便后者渲染它。如果相机比游戏对象的近距离更近,比方说,相机位于游戏对象的正中心,并且近距离值大约为 0.5,它不会被渲染。如果现在另一个游戏对象离相机很远,距离大于“远”值,它也不会被渲染。

至于 Viewport Rect 设置,由两组Vector2值组成。因为屏幕上显示的一切都是 2D 形式,所以没有第三个值来代表 z 轴。

  • XY值允许您分别水平和垂直调整相机渲染的位置。

  • WH值分别代表用于水平和垂直渲染摄像机的屏幕部分。值 1 表示使用屏幕的整个宽度或高度,值 0 表示不使用。

例如,如果您正在为两个玩家制作一个具有分屏功能的游戏机赛车游戏,您可以有两个摄像机,每个摄像机占据一半的屏幕,跟随两个玩家中的一个。两个相机的W值为 0.5,H值为 0,Y值为 0,但是X值不同,以匹配屏幕左半部分和右半部分的位置。

如果在一个场景中使用多个摄影机,可以为它们设定不同的深度值。例如,如果您正在制作一个游戏,其中您可以在第三人称和第一人称视图之间切换,您可以对任何一个视图使用两个相机,但是由于它们都占用 100%的屏幕空间,深度值决定了哪个相机将被渲染给玩家。在该示例中,玩家将看到具有最高深度值的相机正在“看”什么

其余的设置将有一个非常简短的描述,对于简单或小型项目,你可能不会去弄乱那些,如果你这样做,最好是编辑项目本身的设置,而不是相机的个别设置。渲染路径设置允许你选择游戏对象如何被一个相机渲染。

目标纹理允许您将 2D 渲染纹理指定给相机组件。该纹理将随摄像机在场景中看到的任何东西而更新。例如,当你想创建一个鸟瞰图形式的小地图时,这是很有用的。可以设置一个摄像头,从上面跟着玩家往下看(90,0,0)。然后它可以输出到一个 2D 纹理,该纹理可以被分配到一个“矩形”中,该矩形将总是显示在屏幕的右上角。

遮挡剔除是一种流行的技术,可以显著提高某些类型游戏的性能。如果您正在使用该技术,并且特别希望相机从中受益,请勾选相应的复选框。为了遮挡剔除正常工作,你的场景必须为这个特定的特征“烘焙”;否则,勾选复选框不会导致任何变化。

至于高动态范围(HDR)和多采样抗锯齿(MSAA)渲染,它们可以让你的游戏看起来更好,但特别是使用 MSAA,有重要的性能成本。

最后,勾选允许动态分辨率将允许相机缩放渲染纹理,如果你构建游戏的平台支持的话。

照明

灯光在电子游戏中非常重要。主灯光的位置和旋转决定了游戏对象可见的部分,相对于它们与光源投射光线方向的角度,从而决定了阴影投射的方向和大小。

默认情况下,Unity 中的一个空场景会有一个被称为平行光的游戏对象,其中有一个灯光组件(图 3-14 )在一个方向上提供均匀的光线,模拟类似太阳的东西。如果场景中没有光源,世界将会完全黑暗。

img/491558_1_En_3_Fig14_HTML.jpg

图 3-14

轻组分

在“层次”窗口中,可以从四种默认类型的光源中创建,即平行光、点光源、聚光灯和区域光源。平行光可以用来照亮整个场景,基本上就像太阳一样。点光源用于更特殊的场景,例如中世纪村庄中的火把。例如,聚光灯可用于在黑暗的房子里模拟恐怖游戏中的手电筒,区域光可用于均匀照亮指定的区域。

为了保持这一部分的简单,我将只基于定向光源的解释。用其他类型的光源获得的附加设置是不言自明的。

首先,您可以设置光源发出的光的颜色。然后,可以将灯光模式设置为实时、混合或烘焙。

  • 如果你使用的是实时光照模式,那么在你玩游戏的时候,游戏物体会被加阴影。

  • 使用“烘焙”,可以“烘焙”场景以生成一种将自动指定给该光源的照明数据资源。

  • 如果你使用混合,你将有一个烘焙和实时照明的组合。

如果你制作的游戏需要一个移动的光源,或者游戏对象是渐进或随机产生的,最好使用实时,因为照明数据会更准确。但是,如果您想在照明方面节省一些性能,并且如果对象在场景中几乎是静态的,以及光源,您可能会考虑过早烘焙场景并使用烘焙。

如果你使用混合或实时作为照明模式,你可以为将要产生的阴影类型做一些额外的设置。你可以选择从没有阴影到有软阴影或硬阴影。软阴影比硬阴影看起来更平滑,但需要更多的处理能力。然后你可以修改将要产生的阴影的值,比如它们的强度,它们的分辨率(也可以在项目的设置中设置),以及它们与各自游戏对象的距离偏差(图 3-15 )。

img/491558_1_En_3_Fig15_HTML.jpg

图 3-15

立方体上正常强度的光

您也可以增加或减少光源的强度和间接乘数。间接光是被一个游戏对象反射到另一个游戏对象上的光。增加这两个值中的任何一个都会使场景看起来更亮。如果光源的强度被提升到其原始值的两倍(图 3-16 ),上图中的场景看起来会是这样:

img/491558_1_En_3_Fig16_HTML.jpg

图 3-16

立方体上的高强度光

cookie 属性允许您将 2D 纹理分配给光源。纹理将被用作遮罩。你可以把它想象成一个放在光源(灯泡)前的厚纸板形状(可能是恐龙的形状)。这将定义光源投射时获得的阴影、轮廓或图案。您也可以在它下面的选项中编辑 cookie 掩码的大小。

勾选绘制光晕将在光源周围创建一个模糊的球体,其半径等于其范围(如果使用点光源或聚光灯光源,此属性可用),并且颜色与光源投射的光相同。

“光斑”可用于允许场景中的光源渲染光斑,如果您正在制作电影,这可能会很有用。如果使用某种形式的正向渲染,可以更改渲染模式以反映场景中灯光的重要性。最后,以类似于相机的方式,您可以选择将受光源在其剔除遮罩属性中影响的游戏对象层。没有被选中的层的游戏对象不会受到光源的任何影响。在 Unity 的文档中,您可以找到不同灯光样本的更多细节。

渲染器

我现在要讨论的是让 3D 游戏对象可见的两个关键要素。任何缺少这些的游戏对象都将是透明的和不可见的。第一个组件是网格过滤器,它从您的素材中提取一个网格,并将其传递给第二个组件,即用于在屏幕上渲染的网格渲染器(图 3-17 )。

img/491558_1_En_3_Fig17_HTML.jpg

图 3-17

渲染器组件

在接下来的主题中,你将会学到材质到底是什么,但是现在,假设它们是颜色。

渲染器可以利用多种材质,具体取决于如何设置网格过滤器组件中的网格。如果你想自定义一个游戏对象如何接收或投射阴影,可以调整灯光属性。

在制作游戏时,你可能不需要弄乱渲染器的其他属性,但你可以随时访问 Unity 的文档或手册来了解更多信息。

碰撞器

游戏中的物理依赖于刚体和对撞机。碰撞器是一组允许碰撞发生的组件。碰撞器被用作触发器也很常见。例如,在游戏中离非玩家角色(NPC)足够近可能会触发 NPC 和你的角色之间的对话。

在 Unity 中,有六种类型的碰撞器,主要是

  • 盒子(图 3-18

  • 范围

  • 胶囊

  • 网状物

  • 车轮

  • 地带

在本书中,我将介绍前四个对撞机。车轮碰撞器用于制作陆地车辆的车轮,以及其他类似的对象,而地形碰撞器用于地形,这是另一种形式的原生 3D 对象,但具有更多可配置的选项。这本书不会涉及地形。

img/491558_1_En_3_Fig18_HTML.jpg

图 3-18

盒子碰撞器组件

默认情况下,在编辑器中创建的立方体带有一个盒子碰撞器(图 3-19 )。如果你点击编辑碰撞器按钮,你将能够在你的场景视图中缩放碰撞器。你只需要向你想要缩放碰撞器的方向拖动出现的小方块。

img/491558_1_En_3_Fig19_HTML.jpg

图 3-19

修改场景中长方体碰撞器的边界

在检查器窗口中,改变Vector3xyz值,即所谓的“中心”,将使碰撞器向各自的方向移动。改变大小值会使碰撞器变大或变小,这取决于你在哪个轴上修改它的值。应该注意的是,增加或减少游戏对象的缩放变换将使其碰撞器的大小以相似的比例减少。例如,如果您创建一个立方体,并使其所有的比例Vector3值等于 2,即使其碰撞器的大小在所有轴上都是 1,假设您没有手动修改任何东西,碰撞器仍然会环绕立方体的整个体积或大小。

上面的物理材质标签可以用来让碰撞器模拟一种特殊的真实材质。例如,一些物理材质可以使对撞机在你走在上面时感觉更滑,例如冰,而其他材质可以使它感觉像似乎有更多摩擦的东西,例如沥青。

如果我们想在两个物理对象碰撞时触发一个事件,但不让它们相互弹开,就使用触发碰撞器。所以,如果你想在地板上移动时打开灯,这些会有帮助。如果你在碰撞器组件上勾选了 IsTrigger,与之相关的游戏对象将会看起来缺乏参与碰撞的能力。换句话说,你将能够穿越游戏对象,即使当它的 IsTrigger 框被选中时,它有一个碰撞器组件。这在您想要创建触发区域时非常有用,例如,在游戏中,在一个区域中行走会触发某些事情的发生,如播放过场动画。

球体对撞机(图 3-20 )的性质与箱式对撞机非常相似。唯一的区别是它们有一个半径属性,而不是一个Vector3大小的属性。您可以在场景窗口中使用该属性的第一个按钮来修改碰撞器,但是拖动一个点将沿所有轴均匀地增加半径。

img/491558_1_En_3_Fig20_HTML.jpg

图 3-20

球体碰撞器组件

胶囊由两个半球组成,一个在胶囊的顶部,一个在底部。这两个半球之间有一段距离,称为高度。胶囊碰撞器同样具有这些属性(图 3-21 )。

img/491558_1_En_3_Fig21_HTML.jpg

图 3-21

胶囊碰撞器组件

最后说一下网格碰撞器(图 3-22 )。例如,如果你有一个不规则形状的游戏对象,你可以给它添加一个网格碰撞器组件。默认情况下,网格碰撞器会将碰撞添加到游戏对象的整个表面。

img/491558_1_En_3_Fig22_HTML.jpg

图 3-22

网格碰撞器组件

要制作网格碰撞器,请使用 IsTrigger。必须先标记为“凸”。滴答滴答将使碰撞器使用相当数量的 3D 规则形状来覆盖游戏对象的整个表面区域和体积。使用 Convex 还允许使用网格碰撞器在其他游戏对象之间进行碰撞。然而,建议您尽可能使用之前讨论的其他碰撞器,而不是这些,因为它们提供了性能提升,即使网格碰撞器被标记为凸面。

3.2.6 刚体

刚体组件(图 3-23 )通常与碰撞器组件一起使用,它是一个神奇的组件,可以将游戏对象变成反映现实世界中对象属性的对象。它可以使物理引擎允许对象从其他对象反弹,并模拟重力之类的东西。如果你正在制作一个与物理有关的游戏,你很可能不得不使用刚体。

img/491558_1_En_3_Fig23_HTML.jpg

图 3-23

刚体组件

Mass 属性允许您设置对象的质量。一个单位相当于 1 公斤。更大的值会让游戏对象感觉更重。如果使用重力,增加质量会使游戏对象对外力(例如爆炸)的反应更弱,下落更快。

阻力相当于空气阻力,当游戏物体从高处落下时,可以看到这个数值的差异。角阻力几乎是相同的,但它可以由多少空气阻力将影响游戏对象的旋转扭矩来定义。这些值中的任何一个值为 0 都可以解释为“这个游戏对象不受空气阻力的影响。”

不勾选“使用重力”将会阻止游戏对象自动下落,如果它被放置在离地面一定高度的地方,下面没有任何东西支撑它。

如果运动学被勾选,游戏对象将不会受到普通物理的影响。为了使游戏对象移动或影响其位置或旋转,您必须操纵其各自的变换值。这对制作移动平台很有用。

例如,如果游戏中玩家角色的运动不稳定,插值可以用来使游戏对象感觉更平滑。将“插值”设定为“插值”将基于前一帧的变换平滑变换,而在“外推”中,将基于估计的下一个运动平滑变换。当我们开始编码时,你会学到更多关于插值的知识。

碰撞检测系统可以从离散模式改变为连续模式,如果游戏对象移动得如此之快,以至于它能够通过其他碰撞器,因为碰撞检测系统检测它的速度不够快。使用离散模式以外的模式会有性能成本。

最后,你可以为游戏对象设置约束。勾选任一复选框都不允许游戏对象在各自的轴上移动或旋转。这并不意味着它不会通过代码或脚本。这只是意味着应用于刚体的普通物理(例如,碰撞)不会对其产生任何影响。

3.2.7 音频源和听众

音频监听器组件通常在主相机游戏对象上。它实现了一个类似麦克风的设备。它会记录周围的声音,并通过播放器的扬声器播放出来。一个场景中只能有一个侦听器。例如,如果在场景中的某一点,您有一辆汽车发动机正在运行并产生声音,当摄像机靠近它时,声音会以更高的音量播放。

相反,音频源组件定义了播放什么声音以及如何播放。声音来源的位置将由音源组件所连接的游戏对象的位置决定(图 3-24 )。

img/491558_1_En_3_Fig24_HTML.jpg

图 3-24

音频源组件

音频源组件的第一个属性是 AudioClip。这通常是在 Unity 中作为资源导入的音频文件。混音器组是另一个组件或资源,您可以使用它来进一步个性化将要产生的声音质量。

勾选静音会禁止音频收听者拾取其音频源产生的音频。稍后,您可以勾选旁路,以防止音频源产生的声音受到其他效果的影响,无论是听众效果还是其他类型组件产生的效果,混响区域都会相应地应用到它。

“唤醒时播放”将使音频源在场景加载后立即播放指定的音频剪辑,而“循环”将使音频源从头开始重放音频剪辑,每次它都自动完成播放。

如果场景中有多个音频源,可以使用优先级滑块为每个音频源设定不同的优先级。例如,如果音频收听者与两个音频源的距离相等,则两个音频源中具有最低优先级滑块的那个音频源的声音会比另一个音频源的声音大。

音量滑块的作用非常明显。音量为 1 的音频源将以扬声器设定的最大输出音量播放分配给它的音频片段。请注意,音量为 0 的音频源产生的声音是听不到的。

音高滑块用于设定音频源产生的声音的频率。它也可以用来加快或减慢声音。

立体声声相滑块设定发送到混响区的输出信号量。该数量在(0–1)范围内是线性的,但允许在(1–1.1)范围内放大 10 dB,这对于实现近场和远场声音的效果很有用。

空间混合、混响区域混合和 3D 声音设置超出了本书的范围。

粒子系统

粒子系统是一个看起来在特定位置和旋转发射某种粒子或形状的组件(图 3-25 )。例如,使用粒子系统可以帮助你模拟火,雪,或者只是汽车尾气中的烟雾。

img/491558_1_En_3_Fig25_HTML.jpg

图 3-25

场景中的粒子系统

在你的场景窗口中,默认情况下,一个粒子系统看起来就像前面的截图。这三个按钮将分别从左边,暂停,重启,或停止粒子系统。停止和暂停的区别在于,停止会重新启动系统,但会立即暂停。

  • 可以更改播放速度,以查看以指定速度以外的速度运行的粒子系统。

  • 播放时间包含一个值,表示自粒子系统开始运行以来经过的秒数。

  • 粒子是该粒子系统当前活动的已生成粒子数,速度范围是这些粒子速度的最小-最大值。

  • 模拟层允许你在层上模拟粒子系统,而不是在游戏对象的层上。

  • 勾选重新模拟将使应用到粒子系统的更改立即显示。

  • “显示边界”将使 3D 体积出现在场景窗口中,这将指示粒子在该系统中可以行进的最大距离。

  • 最后,如果勾选了最后一个复选框,只显示选中的,将隐藏当前效果中所有未选中的粒子系统。

持续时间(见图 3-26 )定义了粒子系统发射的时间。这意味着,在该秒数过去后,将不再创建更多的粒子或形状。当然,如果勾选了循环,这个值就没有任何重要性了,因为系统会一直发出信号。

img/491558_1_En_3_Fig26_HTML.jpg

图 3-26

粒子系统组件中的第一个设置

开始延迟是系统开始发射前等待的时间。“开始寿命”定义粒子发射后多少秒后将被自动销毁。开始速度是粒子发射时最初行进的速度。

起始尺寸或 3D 起始尺寸可用于定义粒子的尺寸。开始旋转或 3D 开始旋转定义其旋转。翻转旋转可用于翻转粒子的旋转。“开始颜色”在创建粒子时更改粒子的颜色。

重力修改器可以创建受重力影响的粒子。高于 0 的值将使粒子下落得更快。低于 0 的值将根据前面的陈述起作用,但是粒子将向上而不是向下,0 将使粒子完全不受重力影响。

模拟空间可以设置为本地、世界或自定义。

  • 如果设置为局部,粒子将相对于它们的粒子系统所附着的游戏对象的变换移动。

  • 在世界中,它们相对于世界或场景移动。

  • 在“自定义”中,可以指定另一个变换,使系统相对于该变换。

模拟速度是粒子系统播放的乘数。如果使用“未缩放”选项,更改增量时间模式对于暂停时播放效果非常有用。

缩放模式用于相对于整个层次、局部粒子节点调整粒子大小,或者仅将缩放应用于形状。

唤醒时勾选播放将使粒子系统自动开始运行。

发射器速度允许你改变模式为变形或刚体,这取决于系统移动时如何计算速度。

最大粒子数定义了某一特定时刻系统中粒子的最大数量。如果达到该数量,将不会发射更多的粒子,直到粒子数量少于定义的数量。

勾选“自动随机种子”会使每次播放效果时的模拟不同。

在“停止动作”中,您可以定义在粒子系统停止且所有粒子都已被销毁的情况下要做的事情。例如,您可以选择禁用粒子系统组件或销毁以后者为组件的游戏对象。

剔除模式定义了当系统不在屏幕上时会发生什么,即当粒子在屏幕上不可见时。

  • 追赶模式会暂停屏幕外模拟,但当它们变得可见时,会执行一个大的模拟步骤,给人一种从未暂停的感觉。

  • 自动对循环系统使用暂停模式,否则始终模拟。

  • AlwaysSimulate 永远不会暂停模拟,即使在屏幕外。

当“环形缓冲区模式”设定为“启用”时,粒子将保持活动状态,直到“最大粒子缓冲区”填满,此时新粒子将替换最旧的粒子,而不是在粒子寿命结束时死亡。

我将详细讨论发射(图 3-27 )和形状,但对于其余的大多数属性,我将只陈述它们的用途。粒子系统不必利用检查器中所有可用的属性。

img/491558_1_En_3_Fig27_HTML.jpg

图 3-27

粒子系统组件的发射属性

“随时间变化的速率”属性中的值表示每秒发射的粒子数量。速率随距离的变化与速率随时间的变化是一样的,但作用于单位秒。

“爆发”阵列允许您在特定的时间帧发射粒子。其时间属性允许您选择何时发射粒子爆发,计数指定要发射的粒子数量。此外,通过在下拉列表中选择值以外的其他设置,可以设置曲线或创建要发射的粒子数范围。

周期值允许指定重复脉冲的次数。通过在访问其下拉菜单时选择该属性,可以将其设置为无穷大。

Interval 允许您每 x 秒重复一次脉冲,概率是 0 到 1 之间的一个值。如果 Probability 设置为 0,则永远不会发生猝发,如果设置为 1,则总是会发生。值为 0.5 将使突发发生 50%的时间,或者不发生 50%的时间。

您可以通过单击下面的加号图标添加更多组值,或者通过单击减号图标删除组值。

形状代表发射器的 3D 体积(图 3-28 )。例如,球形会导致粒子向各个方向发射。我们将看到这部分的圆锥形状。其他形状提供的不同选项也很容易理解。

img/491558_1_En_3_Fig28_HTML.jpg

图 3-28

粒子系统组件的形状属性

较高的角度值将使发射的粒子向更多方向运动,增加半径将增加粒子可以覆盖的体积。半径厚度的值可以从 0 到 1。值为 0 将使发射的粒子看起来更密集。

“弧”中的值表示从发射器中心可以产生粒子的最大角度。值为 0 将使粒子仅从发射器的中心发射,而值为 360 允许粒子在发射器底部区域的任何点发射。例如,当我们使用圆锥体时,值为 360 会使粒子在形成发射器底部的小圆上的任意点繁殖。

“模式”允许您选择如何在圆弧周围产生粒子。“随机”模式使它们在相对于原始 Arc 值的任何位置繁殖,而“扩散”允许您选择在特定角度繁殖粒子。值为 0 表示禁用此行为。

“从使用发射”的值可以更改,以指定希望粒子从何处发射,从基础还是从体积本身发射。使用纹理 2D 资源,可以修改粒子采样颜色的位置。

Vector3位置允许你从游戏对象的变形位置移动发射器体积。旋转和缩放起着类似的作用。

勾选“对齐到方向”将根据粒子的初始行进方向自动对齐粒子。

随机化方向取 0 到 1 的值。值 1 将用随机方向覆盖粒子的初始行进方向。

类似地,球形化方向用从形状变换中心向外投射粒子的方向来覆盖初始行进方向。

最后,“随机化位置”将起始位置移动一个随机量,直到它包含的最大值。

转到 Shape 属性底部的四个按钮,单击第一个按钮可以打开或关闭形状 gizmo 编辑模式。这与碰撞器的“修改碰撞器”按钮的作用相同,允许您在场景窗口中调整发射器体积的边界或形状。

其他三个按钮分别允许您使用箭头等导向移动、旋转或缩放发射器体积,当您切换它们时,这些导向将出现在场景窗口中。

可以调整“一生中的速度”和“一生中的极限速度”的值,分别使粒子随时间增加或减少速度。

“继承速度”允许您控制粒子从发射器本身继承的速度。

“力随寿命变化”和“颜色随寿命变化”包含可以修改的属性,以分别使粒子获得/失去力并显示颜色随时间的变化。

“颜色按速度”的工作方式类似于“颜色随寿命”的工作方式,但它是根据粒子的速度而不是时间来工作的。大小和旋转随寿命或速度的变化类似。

例如,外力可以被修改以使粒子受到风的影响。“噪波”允许您将湍流添加到粒子的运动中,“碰撞”允许您指定粒子可以碰撞的多个碰撞平面。

触发器允许您根据粒子是在碰撞形状内部还是外部来执行脚本代码。子发射器允许每个粒子在另一个系统中发射粒子。纹理片动画允许您指定纹理片资源,并对每个粒子进行动画/随机化。灯光用于控制附加到粒子上的光源,轨迹用于将轨迹附加到粒子上(在下一节“轨迹渲染器”中会详细介绍)。自定义日期非常复杂,并且允许粒子与脚本或着色器进行交互。

至于渲染器的属性,你可以定义如何渲染粒子,例如,控制它们的颜色,轨迹,渲染模式,排序模式,最小/最大尺寸,相对于相机的对齐,沿轴翻转/旋转,以及它们如何与阴影/灯光交互。

轨迹渲染器

轨迹是粒子系统的另一种形式,它画出了游戏对象的位置(图 3-29 )。把它们想象成尾巴。例如,如果你有一架飞机,你可能希望它的机翼上有轨迹,以模拟在空中飞行的效果。

img/491558_1_En_3_Fig29_HTML.jpg

图 3-29

来自场景中游戏对象上的轨迹渲染器组件的轨迹

轨迹渲染器(图 3-30 )组件的第一件事是某种宽度(y 轴)对时间(x 轴)的图表。你可以在图上添加更多的点,让轨迹随着时间变大或变小。

img/491558_1_En_3_Fig30_HTML.jpg

图 3-30

轨迹渲染器组件

  • 时间轴(x 轴)的值实际上对应于轨迹被设置为渲染的总时间的百分比,由 Time 属性定义(在我们的示例中为 5)。这个时间值 5 可以被解释为踪迹将持续的最大时间。下面是一个例子。如果该值设置为 10,并且汽车持续行驶,将会持续产生多段踪迹,看起来好像踪迹将达到最大长度值 10。轨迹将保持这么长(看起来像一个长矩形),因为新的片段正在产生,以取代汽车上的前一个片段,最大值为 10,直到汽车刹车。轨迹会变得越来越短,直到长度为 0。这需要 10 秒钟的时间,因为每一个棋子在产生后 10 秒钟就会被销毁,最远的棋子最先消失。

  • 宽度轴(y 轴)的值将对应于在特定时间点形成的轨迹的宽度。

最小顶点距离是从上一个顶点开始在轨迹上产生一个新点的最小距离。

当没有踪迹时,勾选自动毁灭会自动毁灭以踪迹渲染器为组件的游戏对象。不点击发射将暂停轨迹生成。

“颜色”允许您为沿着轨迹的颜色设置渐变。拐角顶点是为每个拐角添加的顶点数量。端帽顶点是要添加到轨迹每一端的顶点数。

对齐允许您旋转轨迹以面向其变换组件或相机。如果选择使用 TransformZ 模式,线将沿变换的 XY 平面拉伸。

另一方面,纹理模式可以设置为另一种模式,这取决于您希望如何放置坐标。

如果选中,生成照明数据将为关联的着色器生成数据。

可以应用阴影偏置来防止自阴影伪像。值为 0.5 表示每段轨迹宽度的 50%。

您将在下一节了解材质,但是现在,假设它们定义了轨迹的颜色。

照明可以让您选择投射或接收阴影的方式。同样,探针也是关于照明和反射的。

3.3 材质

如前所述,材质可以改变装备了渲染器组件的游戏对象的外观。这包括对象的纹理、颜色和平滑度,以及其他几个属性。材质拥有的属性由该材质使用的着色器定义。着色器定义了对象的外观,材质可以被视为着色器的一个实例,就像数据类型和变量一样。在项目窗口中右键单击任意位置并指向创建➤材质,可以创建一个材质(图 3-31 )。您可以重命名刚刚创建的材质。

img/491558_1_En_3_Fig31_HTML.jpg

图 3-31

创建材质

要将一个材质应用到场景中的游戏对象,你可以简单地将它直接拖放到场景窗口中的对象上,层次窗口中游戏对象的名称上,或者检查器视图的底部,如果有问题的游戏对象被选中的话(图 3-32 )。

img/491558_1_En_3_Fig32_HTML.jpg

图 3-32

使用渲染器将材质应用到游戏对象

要编辑一个材质的属性,你可以简单地在任何一个应用了它的渲染器的游戏对象上展开它,或者在项目窗口中选择它,在检查器窗口中进行(图 3-33 )。

img/491558_1_En_3_Fig33_HTML.jpg

图 3-33

材质的特性

默认情况下,创建的材质将使用标准着色器。这是可以改变的,符合特定的要求。出现在属性旁边的透明小方块可以被赋予 2D 纹理,这样它们就不会像默认情况下那样显得单调。

反照率是材质的主色,可以挑别的颜色。使用该材质的对象的所有实例将实时显示和应用更改。

“金属色”和“平滑度”滑块将分别使材质的颜色看起来或多或少有点金属色和平滑度。也可以将源更改为反照率 Alpha 来模拟另一种效果。

如上所述,您可以为法线、高度、遮挡和细节遮罩设置 2D 纹理。勾选发射后,您可以使材质发射 HDR 颜色。

例如,如果您有一个使用栅格 2D 纹理的材质,平铺和偏移就很有用。更改这些值将使材质重复自身或沿轴移动。

我不会详细讨论二级地图和其他选项,因为这对于本书来说不是必需的。

3.4 标签和层

如前所述,标签可用于例如在射击游戏中识别与敌人相撞的物体。如果是球员,我们应该移除他的健康;否则,如果是子弹,损害应该由敌人承担。

图层也可以应用于游戏对象。在编辑器中,你可以定义一层 GameObject 是否可以和另一层 GameObject 发生碰撞。默认情况下,当您创建一个层并将其指定给游戏对象时,如果您不修改任何内容,它会使用现有层与所有对象发生冲突。层也可以用于定义哪些对象由相机渲染或受特定光源影响。

在编辑➤项目设置➤物理,你可以设置许多默认的物理属性,如重力和摩擦力设置值。我们将保持目前的一切,但你可以随时改变价值观,以更好地了解自己的变化。向下滚动,你会发现层碰撞矩阵,在这里你可以设置游戏对象的层是否可以与其他游戏对象的层发生碰撞(图 3-34 )。请注意,取消勾选或禁用两层之间的碰撞将使两个游戏对象之间不可能发生碰撞。这两层中的每一层,包括 IsTrigger,都调用不再有效的内容。

img/491558_1_En_3_Fig34_HTML.jpg

图 3-34

层碰撞矩阵的一个例子

要创建新的标签或层,前往编辑➤项目设置➤标签和层,并展开各自的选项卡。对于标记,单击加号图标,输入名称,保存并关闭窗口。对于层,输入你看到的第一个空矩形命名用户层并关闭窗口(图 3-35 )。

img/491558_1_En_3_Fig35_HTML.jpg

图 3-35

标签和图层

要将标签或/和层应用到游戏对象,在场景或层次窗口中选择它,并选择要应用到它的相应标签或/和层(图 3-36 )。

img/491558_1_En_3_Fig36_HTML.jpg

图 3-36

将标签和层应用到游戏对象

3.5 脚本

脚本可以分为两类,一类是组件,另一类是独立的素材。脚本只能用 UnityScript 编写(像 C#一样,只是做了一些修改),因为其他所有以前的语言都已被弃用。当你选择游戏对象并输入你想要添加的脚本名称时,你可以通过使用检查器窗口底部的添加组件菜单来创建脚本,或者在项目窗口中右击并创建一个脚本(图 3-37 )。请注意,当您使用前一个选项,并且您键入的脚本名称不存在时,您可以直接创建一个。

img/491558_1_En_3_Fig37_HTML.jpg

图 3-37

创建脚本

创建脚本后,您还可以在项目窗口中看到它。选择一个脚本会在检查器中显示其内容的预览(图 3-38 )。

img/491558_1_En_3_Fig38_HTML.jpg

图 3-38

在检查器中查看脚本

如果您双击该脚本,它将在您在“编辑➤”首选项中设置的代码编辑器中打开。创建的任何新脚本的前三行用来引用包含类的名称空间。这些将允许您编写代码,利用流行和重要的数据类型,如列表和数组。通过您的脚本,using UnityEngine;行将让您轻松地与引擎中的其他组件进行交互。

您在脚本中编写的代码通常放在它的开始和结束花括号之间。: MonoBehaviour部分是让你的脚本实际上表现得像 Unity 中的一个组件。

接下来,如果你想声明全局变量,你可以在任何函数之外,在类的花括号内这样做。

当您进入播放模式时,void Start() {}函数中编写的代码将运行一次。写入void Update() {}的指令将每帧执行一次。如果你的游戏运行速度是 60 FPS,更新函数中的代码一秒钟就会运行 60 次。

图 3-39 显示了一个声明一个全局整型变量并使其在 Unity 编辑器和其他脚本中公开可见的例子。请注意,如果您省略了public部分,变量将默认标记为private,其他脚本将无法直接访问这些变量,也无法在编辑器中看到这些变量。

img/491558_1_En_3_Fig39_HTML.jpg

图 3-39

一个脚本如何默认+一个全局整数变量:myInt

保存你的脚本后,在 Unity 中自动编译后,你应该可以在游戏对象上看到一个新的区域,这个区域有这个脚本作为组件。在编辑器中编辑该字段的值会直接改变myInt变量保存的值(图 3-40 )。在后面的章节中,你会学到更多关于 Unity 脚本的知识。

img/491558_1_En_3_Fig40_HTML.jpg

图 3-40

从检查器中查看和修改脚本变量

四、用户界面

通常,用户界面(UI)是指用户与计算机系统交互的方式。在我们的例子中,这个术语主要指游戏中的 UI 元素,比如按钮和操纵杆,它们允许玩家与我们的游戏进行交互。此外,本章还将讨论其他类型的 UI 元素,如文本、滑块和图像,播放器不能与之交互,但可以用来显示重要信息或提供更好的用户体验。对于本章,建议您使用 2D 视图。你可以点击场景窗口顶部标有 2D 的小按钮(图 4-1 )。此外,我们将使用 Rect 工具(图 4-2 )。

img/491558_1_En_4_Fig1_HTML.jpg

图 4-1 和 4-2

2D 视图按钮(左)和矩形工具(右)

4.1 画布

在 Unity 中,2D UI 元素必须是一个叫做 Canvas 的游戏对象的子元素。默认情况下,当你在编辑器中创建一个 UI 元素时,Unity 会创建一个画布,如果还没有创建画布的话,它会使元素成为画布的子元素。现在,让我们看看一个空画布游戏对象上的多个组件。这可以通过在层次窗口中右键单击并从 UI 中选择 Canvas 来创建。您可能还注意到,在场景中创建了另一个名为 EventSystem 的游戏对象。我将首先分析 Canvas GameObject 上的不同组件。如果在场景视图中缩小,画布应该看起来像下面的截图。请注意,你也可以双击 UI 元素,或者任何游戏对象,将它们完全显示在视图中(图 4-3 )。

img/491558_1_En_4_Fig2_HTML.jpg

图 4-3

在 2D 视图的“场景”窗口中显示的画布

如前所述,UI 元素需要是 Canvas GameObject 的子元素,以便可见和/或可交互。但是,例如,如果一个画布有四个与子元素大小相同的 UI 元素,并且位于相同的确切位置,则第一个元素将首先在屏幕上绘制,最后一个元素将在它们之上绘制(在 4.3 节“文本”中演示)。

画布组件

画布组件中有三种呈现元素的模式(图 4-4 )。

img/491558_1_En_4_Fig3_HTML.jpg

图 4-4

画布组件

在第一种默认模式“屏幕空间-覆盖”中,画布将与屏幕大小相同,因此与屏幕分辨率相匹配。如果勾选了像素完美,则渲染 UI 时不会消除锯齿以提高精确度。如果在场景中使用了具有相同模式的多个画布组件,则排序顺序值最高的组件将被渲染。更改目标显示允许您在编辑器中测试多个画布或视图。我不会真的去详细说明这个选项或者附加的着色器通道。

如果模式设置为屏幕空间-相机(图 4-5 ),画布及其子元素将根据指定的相机游戏对象进行渲染。

img/491558_1_En_4_Fig4_HTML.jpg

图 4-5

画布组件设置为屏幕空间—相机渲染模式

另一种说法是,画布可以被认为是一个平面。您可以在“平面距离”字段中设置它与相机的距离。虽然您不会看到画布大小的变化,例如,如果您正在使用 3D 游戏对象,那么画布与相机的渲染距离将会有明显的差异。图 4-6 展示了这一点。

img/491558_1_En_4_Fig5_HTML.jpg

图 4-6

带有相机屏幕空间模式和 3D 对象的画布示例

第三个也是最后一个模式,世界画布,使画布在游戏中表现为 3D 平面。使用这种模式可以让你在一个场景中拥有尽可能多的画布游戏对象,它们都将由指定的摄像机渲染。例如,在一个游戏中,这种模式可以用来制作漂浮在敌人身上的生命值条。因此,画布将与其子滑块所代表的敌人处于相同的 3D 位置和旋转。图 4-7 举例说明。

img/491558_1_En_4_Fig6_HTML.jpg

图 4-7

世界空间中带有健康栏的画布

监视器的屏幕也使用了一个世界空间画布游戏对象。

画布缩放器

画布缩放器(图 4-8 )是一个非常有用的组件,它允许我们指定画布应该如何根据不同的屏幕尺寸进行渲染。它仅适用于前一组件的屏幕空间模式。

img/491558_1_En_4_Fig7_HTML.jpg

图 4-8

画布缩放组件

在第一种模式下,画布将始终保持相同的大小。例如,如果你为一个 720p 屏幕制作一个游戏,并相应地调整你的元素的大小以匹配它,当分辨率加倍时,所有的元素将仍然是相同的物理大小,因此,看起来小两倍,假设屏幕大小对于 720p 和 1440p 屏幕是恒定的(图 4-9 )。增加比例因子将使画布按比例变大。最后一个选项适用于精灵,我们不会使用它,因为我们更关心 2D 游戏。

img/491558_1_En_4_Fig8_HTML.jpg

图 4-9

画布缩放组件设置为随屏幕大小缩放

您可以设置一个参考分辨率,以此作为开发游戏的目标。然后,您可以选择让画布匹配屏幕的宽度或高度,或者相应地收缩或扩展。对于前者,您可以将画布设置为仅匹配屏幕的宽度或高度。例如,将滑块一直拉到最左边,无论屏幕尺寸的像素高度是否增加,画布都不会发生变化,而宽度保持不变。但是,在这种情况下,如果以像素为单位的屏幕宽度发生变化,画布将按相等的比例缩放。

最后,在最后一种模式中,恒定物理大小,您可以根据厘米、毫米、英寸、磅或十二点活字来设置画布的实际物理大小。这就是你需要知道的全部。

由于这本书旨在帮助你开发手机游戏,特别是 Android 平台,如果我们在创建一个风景游戏,画布缩放器通常设置为匹配高度。例如,如果我们有一部分辨率为 1920 × 1080 的手机,并且游戏将以横向(水平)模式运行,则最终的屏幕高度将对应于值 1080。通常,屏幕在垂直方向比水平方向变得更大。我们的声明试图传达这样的信息,例如,如果我们的手机长宽比为 16:9、18:9 或 21:9,我们的按钮和其他元素将保持相同的大小,除非屏幕实际上更大而不是更长。因此,我们的游戏将能够在这些设备上全屏播放,而不会影响我们的元素的布局,例如它们的大小。现在,如果我们的游戏是在一个更大屏幕的设备上玩,而不仅仅是一个更长的屏幕,它将缩放并仍然正常显示。根据你想做的游戏,这并不总是你想做的,你可能会追求另一个选择。

图形光线投射仪

该组件(图 4-10 )主要用于确定画布上的元素是否被点击。

img/491558_1_En_4_Fig9_HTML.jpg

图 4-10

图形光线投射器组件

我不会详细讨论这些属性,因为您很少与这个组件交互。如你所知,它们用于调整什么物体应该能够阻挡光线投射。

4.2 直肠变换

这是一个你会在所有 UI 类型的游戏对象上看到的组件(图 4-11 )。它相当于 3D 游戏对象上的转换组件,但是对于 UI 元素,有一些额外的选项和属性。对于画布游戏对象,该组件中的值可能被锁定。

img/491558_1_En_4_Fig10_HTML.jpg

图 4-11

矩形转换组件

位置 X、Y 和 Z 对应于变换组件的典型位置Vector3。相同的逻辑适用于旋转和缩放字段。改变WidthHeight值是不言自明的。请注意,如果您更改 Pos Z 中保存的值,这实际上不会有什么不同,除非两个或更多元素重叠,在这种情况下,具有最高 Pos Z 值的元素将是可见的元素。例如,如果有许多空的 2D 方块(图像),每个都具有相同的大小,并且正好位于相同的位置,但是每个都具有不同的颜色,则该组中具有最高位置 Z 的那个将是可见的。然而,这条规则也有例外。还要注意,根据所选的锚类型,字段的名称和数量会有所不同。

锚点指定 UI 元素的位置相对于其父元素的矩形变换和位置的点。就 X 和 Y 坐标轴而言,每个最小值和最大值可以在 0 到 1 的范围内。值 0 对应于画布的最左侧位置,而值 1 对应于最右侧位置。类似地,对于Y值,该范围从画布的最高位置到最低位置。通常Vector2的最小值和最大值是相同的。

虽然定位点与元素相对于其父元素本身的定位有关,但支点则是相对于元素本身移动 UI 元素的中心。同样,XY的值从 0 到 1,但是这一次,它们表示基于自身宽度和高度的 UI 元素枢轴的位置。例如,如果X枢轴值设置为 0,并且您试图增加元素的宽度,枢轴将在最左端,并且如果该值为 0.5,元素将仅从其右侧缩放,而不像从其中心在两个水平方向缩放,即在元素的中心。

你也可以通过点击 Rect Transform 组件左上角看起来像网格的东西来改变元素的锚点和枢轴(图 4-12 )。

img/491558_1_En_4_Fig11_HTML.jpg

图 4-12

锚点预设

单击这些预设中的任何一个都会自动将其锚点值分配给元素。此外,如果在执行此操作的同时还按住 Shift 按钮,则 pivot 值也会以类似的方式进行修改。如果在这个过程中按住 Alt 键,不管是否按住 Shift 键,元素也将移动到锚位置,因此有一个(0,0)的Vector2位置。在了解了其他 Canvas UI 元素之后,您将能够测试所有这些元素。

4.3 文本

要创建 UI 元素,只需右键单击层次结构窗口中的空白区域,转到 UI,然后选择所需的元素。在本节中,我们将关注名为 Text 的 UI 元素。确保它是画布游戏对象的子对象。双击该元素将使其可见并位于场景窗口的中心(图 4-13 )。

img/491558_1_En_4_Fig12_HTML.jpg

图 4-13

文本用户界面元素

当然,您可以沿着文本元素的边缘拖动,使其占据更大的区域,或者手动修改其 Rect Transform 组件的值(图 4-14 )。

img/491558_1_En_4_Fig13_HTML.jpg

图 4-14

文本组件

修改文本组件上默认写入“新文本”的字段会直接改变元素上实际写入的内容。

接下来,如果在项目中导入了字体文件,您可以指定要使用的字体。您可以为文本元素选择普通、粗体和/或斜体字体样式,并指定字体大小和行距。例如,较大的字体大小值会使文本看起来更大,而较小的行距值会减少段落中各行之间的距离。勾选富文本允许您在文本中设置特定的单词,例如将它们放在类似 HTML 的标签之间。

接下来,您可以将文本左对齐、居中对齐或右对齐,以及在其 Rect Transform 组件占据的区域的顶部、中间或底部对齐。勾选“按几何图形对齐”将执行一些小的更改,使文本更多地反映您之前的选择,就其实际几何图形而言。

对于水平溢出,您可以选择在一行的字数超过 Rect 转换的宽度时让文本在新的一行上继续(换行),或者忽略 Rect 转换的宽度并在同一行上继续(溢出)。

对于垂直溢出,您可以选择截断或溢出。以类似的方式,在后一种情况下,文本将继续在 Rect 变换高度的边缘之前,并且只占用它所需要的空间,而在前一种情况下,在达到 Rect 变换可以容纳的最大行数之后,其余的文本或行将被丢弃,并且不可见。

勾选最佳匹配允许您设置文本字体大小的最小和最大值。这些值将覆盖以前的字体大小字段。只要 Rect 变换具有足够容纳所有文本的区域,文本的字体大小就不会小于最小值,并且倾向于尽可能接近最大值。如果 Rect 转换可以轻松容纳最小值的所有文本,字体大小将在内部增加到一个更高的值,但小于或等于最大值设置,而 Rect 转换可以容纳全部文本。

请注意,如果在不使用溢出模式的情况下,指定大于 Rect 转换所能容纳的字体大小或最小字体大小,则文本中的部分或全部字符可能不可见。

接下来,如果您希望文本具有某种效果,如水平渐变,您可以更改文本的颜色并指定一种材质。

勾选 Raycast 目标允许您稍后通过脚本添加事件,例如,当您希望在单击文本 UI 元素时发生一些事情。

4.4 图像

请记住,位于画布子元素列表底部的 UI 元素会呈现在列表中位于它们上方的元素之上。如果我们创建一个 UI 图像元素,并将其放在画布子元素列表中的文本元素之前,会发生什么情况(图 4-15 ):

img/491558_1_En_4_Fig14_HTML.jpg

图 4-15

在文本 UI 元素上呈现图像

然而,如果文本和图像元素交换位置,图像将位于文本元素的前面或顶部(图 4-16 )。

img/491558_1_En_4_Fig15_HTML.jpg

图 4-16

在图像 UI 元素上呈现文本

该组件可用于向用户显示非交互式图像。您可以将此用于装饰或图标等元素(图 4-17 )。

img/491558_1_En_4_Fig16_HTML.jpg

图 4-17

图像组件

图像可以设置为显示实际的图片图形。请注意,图像将被缩放以匹配 Rect 变换的尺寸。您可以将图像素材标记为 Sprite 2D 类型,这样您就可以将它分配给 UI 图像组件的 Source Image 属性,以便它显示该图像。要将导入的图像标记为精灵 2D 类型,您只需在项目窗口中选择它,并在检查器中将其纹理类型更改为精灵(2D 和用户界面)(图 4-18 )。在第六章中,我们将为暂停按钮使用雪碧 2D 纹理。

img/491558_1_En_4_Fig17_HTML.jpg

图 4-18

标记为 2D 雪碧的纹理

在图像组件上设置另一种颜色可以看作是在图像上放置一个颜色过滤器。Material 属性可用于在要渲染的最终图像上应用其他效果。勾选光线投射目标让 Unity 考虑光线投射的图像。

4.5 原始图像

与图像不同,只有纹理类型的图像才能在原始图像组件上渲染(图 4-19 )。

img/491558_1_En_4_Fig18_HTML.jpg

图 4-19

原始图像组件

颜色和材质属性的工作方式与图像组件中的类似。至于Vector2XY以及WH,它们分别对应于所分配纹理的位置和大小,相对于矩形变换组件。修改XY值将使纹理从矩形变换的中心偏移XY的量,并且修改WH值将相应地改变纹理相对于矩形变换的实际宽度和高度的宽度和高度。

4.6 滑块

Slider UI 元素在各种情况下都很有用。它们可以用来轻松地为敌人和/或玩家制作生命条,或者是可交互的,以便改变一些游戏内的值,例如音量(图 4-20 和 4-21 )。

img/491558_1_En_4_Fig20_HTML.jpg

图 4-21

滑块组件

img/491558_1_En_4_Fig19_HTML.jpg

图 4-20

滑块

如果互动保持勾选,你将可以在游戏进行的时候拖动和调整滑块的旋钮。过渡允许您根据滑块的值或旋钮的状态设定视觉反馈。

例如,使用色调过渡允许您为旋钮设定定义的颜色,这取决于旋钮是被停用还是被按下。以类似的方式,子画面交换导致正在使用的子画面发生定义的变化,并且动画将相应地触发设置动画。当然,如果转换被设置为无,则不会有任何变化。

导航中的选项允许您通过键盘按键控制滑块。如果不希望滑块的值发生变化,请使用键盘按键将该属性设置为 None。Fill Rect 和 Handle Rect 是必须指定的 Rect 变换组件,以便滑块知道它的填充和手柄在哪里。

方向直观地定义了滑块的最小值和最大值所在的位置。如果设置为从右到左,滑块可以容纳的最小值将在滑块的最右端可见。

在 Direction 属性的正下方,有两个字段允许您设置滑块可以容纳的最小值和最大值。如果旋钮位于滑块的任意一端,它将代表我刚才讨论的字段中设置的最小值或最大值,这取决于方向属性。

勾选整数将确保滑块代表的值仅为整数。当此选项被勾选时,带有小数/分数部分的数字不会出现。

通过调整值滑块所做的更改也将通过场景和游戏窗口中的滑块反映出来。此值滑块从最小值(左)设置一直到最大值(右)设置。

最后,您可以设置滑块在它的值改变时做一些事情。例如,当滑块的值改变时,你可以让滑块调用游戏对象的某个脚本中的函数。我们稍后将利用这一点。

接下来,让我们快速看一下默认情况下组成 Slider GameObject 一部分的子对象(图 4-22 )。

img/491558_1_En_4_Fig21_HTML.jpg

图 4-22

典型的 Slider UI 元素的子游戏对象

背景有一个图像组件,是一个完整大小的滑块游戏对象。更改该组件的属性将直接改变滑块的背景。

填充区域只是一个空的游戏对象,但是它是滑块填充正常工作所必需的。接下来,填充游戏对象的工作方式类似于背景游戏对象。由于滑块的值更接近其最大值,填充将占据更大的区域并覆盖背景。

手柄填充区域表示滑块手柄可以移动的区域。最后,句柄只是一个图像组件(带有 Rect 转换),默认情况下使用一个普通的白色圆形精灵。如果你决定改变游戏中滑块的值,手柄就是你要与之交互的可视组件。

4.7 按钮

按钮可以被认为是在游戏中为了做某些事情而与之交互点击组件。按钮的一个很好的例子是看起来像箭头的东西,当你按下它时,它会让你的角色跳起来。默认情况下,按钮在创建时看起来如下(图 4-23 ):

img/491558_1_En_4_Fig22_HTML.jpg

图 4-23

一个按钮

要改变按钮的大小,只需在游戏对象的 Rect Transform 组件中编辑适当的值。要更改其默认外观,根据需要编辑其游戏对象上的图像组件(图 4-24 )。

img/491558_1_En_4_Fig23_HTML.jpg

图 4-24

按钮组件

前几个属性的作用与 Slider 组件的相同。如果未勾选可交互,您将无法与按钮交互。转换基本上定义了按钮的外观,这取决于按钮被按下或禁用时的状态。

渐隐持续时间值越小,反映的变化越快。例如,如果按钮被按下,并且正在使用色彩过渡,则如果“淡化持续时间”值设置为 0(秒),按钮将立即过渡到“按下的颜色”中指定的颜色。

同样,可以将按钮设置为执行某些操作,如该组件底部的 OnClick()列表中所定义的。

默认情况下,按钮也有一个文本游戏对象作为子对象。然而,如果你不需要它,你可以安全地删除或销毁它。

4.8 输入字段

输入字段实际上只是一个文本框。点击它将调出移动设备上的默认键盘,并允许您键入一些内容。作为子元素,它有两个文本 UI 元素。第一个称为占位符,它将包含一些虚拟文本,而没有输入任何内容。另一个文本 UI 元素将包含您键入的文本。您可以修改这些文本 UI 元素的属性,以获得您想要的样式(图 4-25 )。

img/491558_1_En_4_Fig24_HTML.jpg

图 4-25

输入字段

在输入字段组件上,您将再次发现交互、转换和导航属性。这些将做与按钮和滑块相同的事情。

与文本相关的属性分别表示用于显示键入内容的文本 UI 元素、直接映射到该文本 UI 元素的文本属性的字段以及该输入字段可以容纳的最大字符数。请注意,占位符文本将会消失,除非在 text 属性中没有设置任何内容。如果不为空,用户输入的数据前面将会有我们设置的文本。

为要输入的数据类型选择适当的内容类型有助于定义如何显示输入的数据(图 4-26 )。例如,如果设置为 Password,当用户在输入字段中输入内容时,所有字符将自动替换为星号。请访问 InputField 的文档,了解许多内容类型选项之间的区别。

img/491558_1_En_4_Fig25_HTML.jpg

图 4-26

输入字段组件

根据您在“内容类型”中设置的值,您可能还有一个“行类型”属性,该属性允许您选择是否只能在一行中设置数据格式(单行),是否可以跨多行设置数据格式,或者用户是否可以通过按 Enter/Return 键(多行换行)或不按下 Enter/Return 键(多行提交)来跨越一个新行。使用后一个选项,文本将在需要时自动跨多行。

默认情况下,Placeholder 属性只引用输入字段的第一个子字段,作为包含占位符文本的文本 UI 元素。

脱字符号闪烁速率中的值定义脱字符号每秒闪烁的次数。插入符号宽度中包含的较高值将使插入符号更宽,您也可以通过勾选自定义插入符号颜色并选择一种颜色来为插入符号选择自定义颜色。

选择颜色是字符被选中时的突出显示颜色。勾选隐藏移动输入将在 iOS 设备上隐藏屏幕键盘上的原生输入栏。

如果勾选了只读复选框,将无法在输入字段中输入更多字符。

最后,您可以向 OnValueChanged()和 OnEndEdit()列表添加操作。只要输入字段中保存的值发生变化,就会执行前一个选项中的操作。例如,如果您进入播放模式并输入三个字符,OnValueChanged()将被调用三次,任何操作集将被执行三次。至于 OnEndEdit(),每次用户完成编辑文本内容时,都会执行在那里分配的操作,无论是提交内容还是单击某处将焦点从输入字段移开。

4.9 切换

Toggle 与 Button 非常相似,工作方式基本相同。唯一的基本区别是 Toggle 在特定时间有一个TrueFalse值(图 4-27 )。每次单击或触摸切换按钮时,它都会在这两个值之间交替变化。如果它在被点击/触摸之前是True,它将有一个False值;否则,它将有一个True值。

img/491558_1_En_4_Fig26_HTML.jpg

图 4-27

一个开关

在一个 Toggle 组件上(图 4-28 ,IsOn 表示 Toggle UI 元素的True / False状态。勾选此项时,切换处于真实状态。

切换过渡属性允许您设置用户与切换 UI 元素交互时的视觉效果。

img/491558_1_En_4_Fig27_HTML.jpg

图 4-28

肘节组件

图形被设置为具有图像组件的游戏对象。这个图形将代表切换 UI 元素的True / False状态。

至于组属性,你可以在这里分配一个带有切换组组件的游戏对象。切换组基本上是切换 UI 元素的集合。例如,一次只有一个 toggle 处于True状态,有时会很有用。这就是为什么切换组可能会派上用场,但它们超出了本书的范围。

和前面介绍的大多数 UI 元素一样,当 toggle 的值改变时,可以执行一些动作。

默认情况下,一个切换游戏对象有两个子对象(图 4-29 )。

img/491558_1_En_4_Fig28_HTML.jpg

图 4-29

典型切换 UI 元素的子游戏对象

第一个子元素是 Background,默认情况下它有一个图像组件。这个游戏对象本身有另一个图像 UI 元素作为子元素,名为 Checkmark。这个对号游戏对象激活或去激活,取决于开关的True / False状态。

两个子元素中的另一个名为 Label,它只是切换 UI 元素的背景/复选标记旁边的文本。它不是切换 UI 元素的基本部分,可以安全地删除。

4.10 下拉菜单

想象一下,创建一个定义好的选项列表,用户可以从中选择一个。这也正是下拉 UI 元素存在于 Unity 中的原因(图 4-30 )。

img/491558_1_En_4_Fig29_HTML.jpg

图 4-30

下拉菜单

当触摸/单击下拉菜单时,会显示一个包含所有预定义选项的扩展菜单。如果选项的数量要求显示比已经为下拉菜单定义的区域更大的区域,滚动条将显示在选项列表的右侧。选择一个选项后,下拉列表将折叠回其初始状态,显示当前选择的选项。

下拉 UI 元素有一个很长的子元素列表,但是由于它们的精确名称,通过使用 Rect 工具,你可以很容易地理解它们的用途(图 4-31 )。在本节中,我们将只研究 Dropdown 组件中的新属性。

img/491558_1_En_4_Fig30_HTML.jpg

图 4-31

下拉组件

模板、标题文本和标题图像分别是对矩形转换、文本和图像组件的引用。默认情况下,Template 是包含许多子对象的 GameObject,这些子对象构成了当单击/触摸下拉菜单时出现的列表的一部分。标题文本表示将写入当前所选选项名称的文本组件。Caption Image 不是必需的,但它代表的是代表当前所选选项的图像的图像组件。

项目文本和项目图像的工作方式与标题文本和标题图像完全相同,但这一次通常表示下拉列表中的选项。

Value 是选项列表中当前选定选项的索引。它从 0 到列表中的项目数减 1。

Alpha 渐变速度是转换到完全不透明的下拉列表或完全透明的下拉列表所需的秒数。

最后,您拥有预定义选项的实际列表,您可以从中添加或删除选项。对于每个选项,您可以更改名称,指定一个精灵来代表它,并在选项列表中对它进行重新排序。

4.11 滚动条

滚动条(图 4-32 )非常类似于滑块。主要的区别是滚动条为它们的句柄提供了更多的调整,但是没有可以设置的最小值或最大值,它们只能有一个从 0 到 1 的值。

img/491558_1_En_4_Fig31_HTML.jpg

图 4-32

滚动条

如果滚动条的值为 0,滚动条的手柄将位于其左边缘附近,如果值为 1,则位于右边缘附近。

你可以参考关于滑块的部分来理解滚动条组件提供的大多数属性(图 4-33 )。仅有的两个新属性是大小和步数。

img/491558_1_En_4_Fig32_HTML.jpg

图 4-33

滚动条组件

大小滑块的值总是介于 0 和 1 之间。值为 1 将使控制柄的宽度和高度等于 Rect Transform 组件的宽度和高度。步数将定义滚动条在其手柄被拖动时可能停止的次数。例如,如果该属性的值设置为 4,当您进入播放模式并拖动滑块时,在滚动条的整个宽度上,一次只能有四个位置可以放置手柄。默认值 0 允许控制柄沿该宽度自由定位。

4.12 Scrollview

Scrollview UI 元素有一个 viewport(图 4-34 )。您可以在其中放置几个 UI 元素,比如文本、图像或按钮。默认情况下,它还带有水平和垂直滚动条。您可以使用它们在放置在其视口中的元素之间滚动。

img/491558_1_En_4_Fig33_HTML.jpg

图 4-34

卷轴检视

使用 scrollview 是一种在移动游戏的定义区域放置多个元素的优雅解决方案,因为与通常在比手机屏幕更大的屏幕上玩的 PC 或主机游戏不同,移动游戏开发人员通常必须显示多个元素,这些元素通常不能太小,因为最终用户会发现很难与所显示的内容进行交互,或者无法正确感知所显示的内容。scrollview 也可以用来滚动一个大的图像或文本元素。

Content 表示 scrollview 的子视图的 Rect 变换,它将包含您放置在其视口中的所有元素(图 4-35 )。

img/491558_1_En_4_Fig34_HTML.jpg

图 4-35

Scrollview 组件

不勾选水平或垂直将在运行时禁用相应的滚动条。移动类型可以设置为无限制、弹性或夹紧。使用最后两个选项将使内容保持在滚动矩形的范围内。但是,当内容到达滚动矩形的边缘时,Elastic 会反弹内容。后者的反弹量将由弹性属性决定。

如果勾选了惯性,拖动后释放手柄,内容将继续移动。否则,内容只会在拖动时移动。如果设置了惯性,减速率将决定内容停止移动的速度。速率为 0 将立即停止运动,而速率为 1 将使运动永不减速。

滚动敏感度是使用滚轮和触控板事件进行滚动的敏感度。其他一些属性只是用来将滚动条指向视口和滚动条。滚动条有一个可见性属性。将该属性设置为 Permanent 将防止滚动条被隐藏,而 Auto Hide 和 Expand Viewport 将在不需要时隐藏内容。发生这种情况时,后一种情况也会导致视口扩展。间距是滚动条和滚动条右下角之间的距离。

4.13 面板

面板只是一个具有一定透明度的图像,它的矩形变换沿着其父对象的整个宽度和高度伸展。也就是说,如果您使一个面板 UI 元素成为设置为“屏幕空间-覆盖”的画布元素的子元素,它将在播放模式下占据您屏幕的整个可视区域(图 4-36 )。

img/491558_1_En_4_Fig35_HTML.jpg

图 4-36

面板组件

4.14 事件系统

事件系统是控制与 UI 系统的所有交互的组件,接收来自键盘、鼠标、触摸屏等的输入。,并将它们转换成与底层 UI 元素的交互(图 4-37 )。

img/491558_1_En_4_Fig36_HTML.jpg

图 4-37

事件系统组件

它计算出用户与哪个画布和控件进行了交互,并相应地激活它。如果没有事件系统,UI 将仅仅在屏幕上绘制,什么也不做。

此外,事件系统允许 UI 控件(支持事件,如复选框和按钮)在用户与它们交互时通知 Unity 项目。例如,一个用户点击一个按钮,然后按钮激活或停用另一个游戏对象。如果使用正确,这可能是一个非常强大的系统。

第一次选择对应于运行时第一次选择的游戏对象。勾选发送导航事件允许 EventSystem 发送导航事件,如移动、提交和取消。拖动阈值对应于拖动的软区域,以像素为单位。

独立输入模块(图 4-38 )是实际检测输入(键盘按键、鼠标指针点击、触摸)并向游戏发送相应事件的组件。没有这个组件,你将无法与你的游戏互动。

img/491558_1_En_4_Fig37_HTML.jpg

图 4-38

独立输入模块组件

带有String值的属性正好对应于代表游戏中一些常见输入的轴。每秒输入操作中保存的值表示每秒允许的输入数,重复延迟是每秒输入操作重复率生效之前的延迟秒数。勾选强制模块激活将强制独立输入模块激活。

在我们将要制作的游戏中,我们不需要编辑事件系统中任何组件的任何属性。

4.15 输入轴介绍

由于我们将专注于制作手机游戏,对于许多类型的游戏来说,了解 axes 如何工作是很重要的。要与冒险 3D 手机游戏互动,您可能需要使用操纵杆来移动玩家。这个操纵杆很可能会使用一个或多个轴。

像下面这样的操纵杆(图 4-39 )可以用来在 3D 游戏中向各个方向移动玩家的角色。玩家可以握住游戏杆的手柄,向他们喜欢的方向移动。

img/491558_1_En_4_Fig38_HTML.jpg

图 4-39

操纵杆轴的典型图示

所示的操纵杆利用了两个轴:水平轴和垂直轴。当手柄未按下时,它位于操纵杆总面积的中间。在这个位置,它的两个轴的值都为 0。参考上图,如果手柄被推到左上角,它将有一个近似值-0.5 来表示其水平轴,另一个近似值+0.5 或 0.5 来表示其垂直轴。

你将很快学会在手机游戏中使用操纵杆,以及如何映射它们来触发角色的动作。操纵杆允许我们做的另一件有趣的事情是根据一个轴的值来修改角色的速度。例如,如果我们将角色的最大速度设置为 10 单位/秒,我们可以将它乘以操纵杆纵轴的当前值,这样我们就可以通过不完全向上推操纵杆手柄来使角色走得更慢。

五、构建我们的第一款 Android 游戏:球形射手游戏

就这样,我们现在准备在 Unity 中构建一个真正的 3D 手机游戏!在这一章,我们要做一个简单的游戏。基本上,我们的游戏角色将是一个有炮塔的立方体(我们称之为坦克)。使用两个操纵杆,玩家可以移动坦克和发射子弹。接下来,我们要制造一个敌人,并产生它的副本。敌人将试图与玩家的坦克走在同一方向,游戏的目的是在他们成功之前摧毁他们。

5.1 渲染管道

将图形绘制到屏幕(或渲染纹理)的过程称为渲染。这个过程是影响游戏性能的关键因素之一。默认情况下,Unity 中的主摄像头会将其视图渲染到屏幕上。

最近,Unity 发布了可脚本化渲染管道(SRP)。SRP 旨在允许开发人员通过脚本控制渲染,从而提供高度的定制化。

在许多可以使用 SRP 创建的渲染管道中,Unity 提供了两个预建的 SRP:高清渲染管道(HDRP)和通用渲染管道(URP)。

虽然 HDRP 可以让你为高端平台创建尖端的高保真图形,但我们不会用它来制作手机游戏,因为它的性能成本很高。

5.1.1 通用渲染管道(URP)

URP 是制作手机游戏的一个非常优雅的解决方案。它提供了几个图形/质量选项,可以很容易地进行调整,在许多类型的游戏中,它被证明比 Unity 用于制作新项目的默认渲染管道提供了明显的性能提升。

您可以创建一个默认使用 URP 的新项目,但是为了解释如何将它添加到现有项目中,我们将选择标准选项(图 5-1 )。

img/491558_1_En_5_Fig1_HTML.jpg

图 5-1

制作新项目

首先,我将为我们将要制作的游戏设定 1920 × 1080 的分辨率或 16:9 的纵横比(图 5-2 )。

img/491558_1_En_5_Fig2_HTML.jpg

图 5-2

我的编辑器的布局

最后,对于这一部分,我们将添加 URP 包到我们的游戏和切换我们的游戏项目,以利用它。前往➤窗口软件包管理器。在长长的软件包列表加载之前,您可能需要等待一段时间。如果看起来所有的东西都已经被加载了,但没有出现这种情况,请在包管理器窗口中点击。

滚动或搜索通用 RP 包。完成后,点击它。点击 install 并等待所有东西被导入(图 5-3 )。完成后,您可以关闭软件包管理器窗口,因为我们不再需要任何软件包。

img/491558_1_En_5_Fig3_HTML.jpg

图 5-3

从软件包管理器窗口安装通用 RP 软件包

最后,要允许我们的项目使用 URP,我们必须告诉它这样做。首先,右键单击项目窗口中的任意位置,然后单击“创建➤渲染➤通用渲染管道➤管道资源(正向渲染器)”。这将为 URP 创造一个素材,许多属性可以调整,以轻松地改变我们的游戏的图形/质量。两个新素材应该出现在您的项目窗口中(图 5-4 )。

img/491558_1_En_5_Fig4_HTML.jpg

图 5-4

URP 管道素材

此时,您必须知道不同属性的作用。我们现在剩下要做的就是将我们刚刚创建的 URP 管道素材拖放到编辑➤项目设置➤图形中的可脚本化渲染管道设置选项卡(图 5-5 )中。

img/491558_1_En_5_Fig5_HTML.jpg

图 5-5

在项目设置的图形部分添加 SRP

要记住的一件重要事情是,如果您正在处理一个项目,并且您决定切换其渲染管道,您必须确保您正在处理的所有材质都使用与您要使用的新渲染管道兼容的着色器。否则,你的场景/游戏窗口中的所有东西都将呈现粉红色。幸运的是,Unity 提供了一个简单的解决方案。

如果您要切换到的新渲染管道是 URP(您必须为 HDRP 做类似的事情),除了我在本节前面讨论的所有内容,您还必须单击编辑➤渲染管道➤通用渲染管道➤升级项目材质到通用 RP 材质。这样做将自动升级项目中的所有材质,以使用 URP 提供的等效着色器。您也可以选择第二个选项,即根据您的需要,仅升级您选择的材质。

要完成这一部分并开始有趣的部分,只需将项目的构建平台切换到 Android。打开构建设置(Ctrl+Shift+B 或文件➤构建设置),单击 Android 选项,确保它高亮显示,并点击切换平台(在左下角附近找到)。一个 Unity 的 logo 应该会出现在它右边的 Android 标签旁边,你可以关闭构建设置窗口(图 5-6 )。通常,如果切换到不同平台的步骤是在游戏开发的后期完成的,许多素材,如精灵或纹理,将不得不再次进行,这将花费相当多的时间。这就是为什么最好在游戏开发的早期阶段就切换平台,如果你确定你主要开发什么平台的话。

img/491558_1_En_5_Fig6_HTML.jpg

图 5-6

切换到 Android 构建平台

5.2 环境

目前,这款游戏只会有一个地面和一些看不见的墙。地面本身只会是一个大立方体。右键单击等级选项卡,然后单击 3D 对象➤立方体。选择后者后,确保在“检查器”标签中将它的位置和旋转设定为(0,0,0)。给它一个(150,0,150)的标度。图 5-7 显示了它的转换应该是什么样子。

img/491558_1_En_5_Fig7_HTML.jpg

图 5-7

地面游戏对象的变换组件

接下来,前往编辑➤项目设置➤标签和层,并创建一个地面标签。将平面重命名为 Ground,并为其指定该标签。您也可以将其标记为静态(图 5-8 )。

img/491558_1_En_5_Fig8_HTML.jpg

图 5-8

将地面游戏对象标记为静态

对于不可见的墙,创建一个空的游戏对象,将其命名为墙,并重置其变换组件,使其位置和旋转为(0,0,0),比例为(1,1,1)。您也可以将其标记为静态(图 5-9 )。

img/491558_1_En_5_Fig9_HTML.jpg

图 5-9

墙壁游戏对象的变换组件

创建一个新的立方体游戏对象作为墙的子对象,并将其命名为墙 1。给它一个位置(0,0,-75),一个旋转(0,0,0),一个刻度(150,50,1)。如果你看着你的场景窗口,立方体通常应该在你地面的前沿(图 5-10 )。

img/491558_1_En_5_Fig10_HTML.jpg

图 5-10

场景中的地面和墙壁 1 游戏对象

在 Wall 1 游戏对象上,禁用它的网格渲染器组件(勾选网格渲染器标签旁边的复选框),这样墙实际上是不可见的。图 5-11 显示了我们在第一面墙上发现的所有组件。

img/491558_1_En_5_Fig11_HTML.jpg

图 5-11

墙上的组件 1 游戏对象

现在,简单地复制(Ctrl+D)墙 1 游戏对象三次,并将新的实例放置在地面的剩余边缘。下表将为您提供必要的转换值。

|

名字

|

位置

|

循环

|

规模

|
| --- | --- | --- | --- |
| 墙壁 1 | (0, 0, -75) | (0, 0, 0) | (150, 50, 1) |
| 墙壁 2 | (-75, 0, 0) | (0, 90, 0) | (150, 50, 1) |
| 墙壁 3 | (0, 0, 75) | (0, 0, 0) | (150, 50, 1) |
| 墙壁 4 | (75, 0, 0) | (0, 90, 0) | (150, 50, 1) |

到目前为止,我们的层级应该是这样的(图 5-12 ):

img/491558_1_En_5_Fig12_HTML.jpg

图 5-12

当前出现在场景中的游戏对象,如层级中所示

如果你选择了墙壁游戏对象或者所有真实的 3D 墙壁,你的场景应该是这样的(图 5-13 ):

img/491558_1_En_5_Fig13_HTML.jpg

图 5-13

预览地面和不可见的墙壁游戏对象

为了完成这一节,我们只需要添加一些其他类型的材质到我们的地面。如果它保持这样,玩家可能很难察觉他们的坦克在移动(如果屏幕上只有地面和坦克)。作为一个解决方案,我们可以使用网格纹理的材质。为了让游戏看起来更有趣,让我们使用一个卡通风格的石头纹理。

在项目窗口中,创建两个新文件夹:一个名为 Materials,另一个名为 Textures。然后,前往素材商店(Ctrl+9 或窗口➤素材商店),搜索石材地板,按价格排序(从低到高),下载并导入如图 5-14 所示的素材。

如果该素材不再可用,请从以下链接下载: https://raw.githubusercontent.com/EdgeKing810/SphereShooter/master/Assets/Textures/Stone_floor_09.png 。通过将它从文件管理器拖放到项目窗口的 Unity 窗口,将其导入编辑器。然后,将导入的纹理放在名为 Textures 的文件夹中。

img/491558_1_En_5_Fig14_HTML.jpg

图 5-14

素材商店中的石材地面纹理瓷砖素材

当您从商店导入素材时,名为 stone_floor_texture 的新文件夹一定已经形成。将 Stone_floor_09 纹理(看起来像正方形的)移动到您在上一步中创建的纹理文件夹中(拖放),并删除 stone_floor_texture 文件夹。

在您的材质文件夹中,右键单击并点击创建➤材质。命名为地面。将 Stone_floor_09 纹理拖放到地面材质上底图标签旁边的小方块中,或者单击同一标签旁边的圆形图标并选择该纹理。将底图的颜色设置为 RGBA (255,255,255,255)或十六进制 FFFFFF。金属和平滑滑块都应该设置为 0,这样游戏会有更好的外观。最后,将两个耕作值(XY)设置为 15(图 5-15 )。这将使纹理在我们的地面上水平和垂直重复 15 次。只需在场景或层级窗口中拖放地面游戏对象上的材质并保存即可。

img/491558_1_En_5_Fig15_HTML.jpg

图 5-15

地面游戏物体的材质

地面现在应该如图 5-16 所示。恭喜你,我们简单的游戏环境已经准备好了!

img/491558_1_En_5_Fig16_HTML.jpg

图 5-16

你的地面游戏对象应该是什么样子

5.3 我们的玩家(坦克)

在这一部分,我们将用一个立方体、一个球体和一个圆柱体来创建我们的玩家坦克。我们还将编写我们的第一个脚本,允许我们的坦克移动,瞄准,并用双操纵杆设置射击。

制造水箱

参考图 5-22 来了解一下我们玩家的坦克会是什么样子。

img/491558_1_En_5_Fig17_HTML.jpg

图 5-17

玩家游戏对象上的组件

  1. 让我们从做一个立方体开始。将其命名为 Player,并赋予其位置(0,1,0)。其旋转和缩放将分别为默认值(0,0,0)和(1,1,1)。

  2. 给它分配玩家标签。(默认已经存在。)

  3. 不要将玩家坦克标记为静态,因为这将阻止它以后移动。

  4. 制作两个新材质,随心所欲的命名,给它们一个自己选择的底图颜色。我将制作一个青色(0,110,255)和一个黄色(255,255,255)材质,并将它们的金属色和平滑度滑块降低到 0。

  5. 在玩家游戏对象上拖放你创建的两个材质中的一个(在我的例子中,是青色的那个)。

  6. 给玩家添加一个刚体组件,并检查所有约束(除了位置XZ),这样玩家坦克就不会在我们不希望的轴上旋转或移动(图 5-17 )。

现在,创建一个球体作为玩家游戏对象的子对象。贴上旋转体的标签。在接下来的步骤中,它将收到一个模仿坦克炮塔的圆柱体,当玩家(你)试图瞄准时,它将成为旋转的对象。给它一个位置(0,0.5,0),一个旋转(0,0,0),一个刻度(0.75,0.75,0.75)。移除它的球体碰撞器组件,让它使用我们之前创建的两种材质中的第二种。在我的情况下,我会给它黄色的材质(图 5-18 )。

img/491558_1_En_5_Fig18_HTML.jpg

图 5-18

旋转体游戏对象上的组件

要制作炮塔,请创建一个圆柱体对象作为旋转体的子对象,并将其命名为炮塔。再次,删除它的碰撞器组件(在这种情况下,胶囊碰撞器一),并给它相同的材质是用在旋转器上,使这两个物体看起来是一个单一的。刀架的位置必须为(0,0.2,0.8),旋转角度必须为(90,0,0),刻度必须为(0.4,0.8,0.4)(图 5-19 )。

img/491558_1_En_5_Fig19_HTML.jpg

图 5-19

炮塔游戏对象上的组件

我们的子弹需要从炮塔顶端射出。我们将稍后对此进行编码,但现在,只需创建一个空的游戏对象作为炮塔的子对象。它的位置为(0,1,0),旋转角度为(-90,0,0),刻度为(1,1,1)。将其命名为 bulletEnd(图 5-20 )。

img/491558_1_En_5_Fig20_HTML.jpg

图 5-20

bulletEnd 游戏对象的变换组件

此时,您的层级窗口应该如下所示(图 5-21 ):

img/491558_1_En_5_Fig21_HTML.jpg

图 5-21

在我们的场景中当前出现的游戏对象,如层级中所见

您为玩家坦克选择的颜色可能会有所不同,但在此阶段它应该类似于图 5-22 。

img/491558_1_En_5_Fig22_HTML.jpg

图 5-22

玩家坦克游戏对象的外观

设置我们的场景

在我们的游戏中实现操纵杆相关行为的一个快速而优雅的解决方案是从素材存储中导入简单的输入系统素材(图 5-23 )。

img/491558_1_En_5_Fig23_HTML.jpg

图 5-23

导入简单输入系统素材

接下来,我们希望游戏中有两个操纵杆:一个用于移动我们的坦克,另一个用于瞄准它的炮塔。创建一个 UI ➤画布。将其 Canvas Scaler 组件设置为具有屏幕大小 UI 缩放模式的缩放。您可以自由使用您选择的参考分辨率和屏幕匹配模式,但我将使用 1920 × 1080 的分辨率,并且只匹配高度(1080)(图 5-24 )。

img/491558_1_En_5_Fig24_HTML.jpg

图 5-24

画布游戏对象的组件

从你的项目窗口,拖放插件➤简单输入➤预置➤操纵杆预置在你的场景中,作为画布的孩子。你会注意到,在你的层次窗口中,新操纵杆的标签带有蓝色。这是因为它目前仍然是一个预置。您对预设(项目窗口中的实例)所做的任何更改都将应用于它在任何其他地方的任何实例,例如,在您的场景中。但是,我们不需要这种能力。你可以在你的场景中右键点击游戏杆,然后点击“解包预设”或“完全解包预设”。它会像一个普通的游戏对象那样运作。重命名为移动操纵杆。

将移动操纵杆的矩形变换的宽度和高度设置为 300。将其位置设置为(300,300)。它的孩子命名为 Thumb,应该有 150 的宽度和高度(图 5-25 )。同样,你可以自由选择其他值。

img/491558_1_En_5_Fig25_HTML.jpg

图 5-25

移动操纵杆的矩形变换

不需要更改任何其他属性,例如图像组件颜色的轴心点/锚点。您可能还会注意到游戏杆上有一个同名的脚本。这是一个脚本,它将负责使游戏杆具有交互性,并将我们的动作转化为游戏中的输入(图 5-26 )。

img/491558_1_En_5_Fig26_HTML.jpg

图 5-26

移动游戏杆的游戏杆脚本

X 轴和 Y 轴字段转换为轴的名称,用于分别表示操纵杆水平和垂直方向的-1 到 1 值。值标签将显示其数值。在运动轴中选择的选项将定义操纵杆将作用于哪些轴。价值乘数是不言自明的。如果设置为 5,操纵杆沿一个轴的数值范围将为-5 到 5。Thumb 表示操纵杆的子对象,该对象将移动以提供玩家指向操纵杆的方向的视觉反馈。移动区域半径是拇指可以移动到的离操纵杆中心的最大距离。动态操纵杆选项只是在一定的延迟后使操纵杆不可见,没有交互,并允许玩家使操纵杆出现在他们触摸屏幕的任何地方(或定义的区域)。

我们将保留这些选项,因为它们适用于移动操纵杆。复制移动操纵杆游戏对象,将新实例命名为 Look 操纵杆,并将其定位在相同的 Y 位置,但在不同的 X 位置,这等于移动操纵杆从屏幕左边缘到屏幕右边缘的距离。这些操纵杆游戏对象在左下角有一个枢轴点,使它们的 X 和 Y 位置相对于画布上的那个点。因为我已经将屏幕宽度设置为 1920(在画布缩放器中),所以我的 Look 操纵杆的新 X 位置将是 1920–300,等于 1620。只需将 Look 操纵杆上脚本的轴更改为 X 轴的 MouseX 和 Y 轴的 MouseY 即可(图 5-27 )。

img/491558_1_En_5_Fig27_HTML.jpg

图 5-27

Look 操纵杆的操纵杆脚本

我们的游戏将遵循自上而下的摄像机视角。要做到这一点,把你的主相机游戏物体放在你的坦克上面,旋转它,让它看起来向下。我的主摄像头的位置是(0,12.5,0),旋转是(90,0,0),缩放是(1,1,1)。其其他组件上的所有其他属性都设置为默认值。我还将在环境下为它的相机组件设置一个纯色,这样当坦克到达地面边缘时,就有了一个更合适的背景。我使用的是(60,70,60,255)的颜色,十六进制为 3C463C(图 5-28 )。

img/491558_1_En_5_Fig28_HTML.jpg

图 5-28

主相机游戏对象上的组件

我还旋转了我的方向灯,让地面上形成的阴影看起来更适合我(图 5-29 )。然而,这不是必需的。

img/491558_1_En_5_Fig29_HTML.jpg

图 5-29

我的平行光游戏物体上的变换组件

你的游戏窗口应该看起来像下面的截图(图 5-30 ),添加了操纵杆,相机在这一点上重新定位/旋转。

img/491558_1_En_5_Fig30_HTML.jpg

图 5-30

游戏窗口当前应该是什么样子

球员移动

是时候让我们的玩家坦克动起来了!为了保持我们的资源有组织,在项目窗口中创建一个名为 Scripts 的新文件夹。在该文件夹中,右键单击并创建一个 C#脚本。命名为 playerMovement。同样,如果你想选择另一个应用来编辑脚本,前往编辑➤首选项➤外部工具,并选择一个你想要的。然后,双击脚本将其打开。

在第三章的最后一节,我讨论了空白 Unity C#脚本中所有内容的用途,所以我不再赘述。首先,在第四行添加行using SimpleInputNamespace;,就在using UnityEngine;之后。这将使我们能够将操纵杆轴上的输入与游戏中的实际动作相匹配。

正如我们对 playerMovement 类中的第一行所做的那样,我们将创建一些变量来保存值或引用其他组件。此外,我们不会使用void Update() {},我们将删除所有评论。使您的代码看起来像下面这样:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SimpleInputNamespace;

public class playerMovement : MonoBehaviour {
 public Transform rotator;
 private Rigidbody cubeRb;

 public float speed = 5.0f;

 private Vector2 input;

 void Start() {

 }
}

rotator变量将引用一个变换组件,我们稍后将基于输入旋转它,这样我们的坦克可以用它的炮塔瞄准。由于它被标记为 public,我们可以稍后在检查器中自己将它可视化地分配给我们的脚本。cubeRb是私有变量,在检查器中不可见。我们将从脚本本身给它分配玩家坦克的刚体组件。虽然在游戏中有许多移动角色的方法,但是我们将使用的一种方法是根据移动操纵杆的输入来修改坦克(其刚体)的速度。

speed变量将包含一个浮点值,并在检查器中可见。我们将把操纵杆的输入值乘以这个值,使玩家的坦克移动得更快或更慢。

最后,我们将把我们的输入存储在一个Vector2变量中。因为我们的坦克只是沿着 X 和 Z 轴移动,所以我们不需要使用Vector3变量。注意,花括号也可以放在新的一行上(默认情况下是这样的)。当涉及到编码时,个人偏好有很多。

该脚本将被附加到我们的球员坦克,并将作为一个组件稍后。它将执行的许多移动或炮塔旋转将与我们玩家坦克的刚体有关,这将在CubeRb变量中引用。为了实现这一点,我们可以在我们的Start函数中添加一行,这样当游戏开始时,cubeRb就会被引用。

void Start() {
 cubeRb = GetComponent<Rigidbody>();
}

这一行可以解释为“获取当前游戏对象上的刚体组件,并在我们的cubeRb变量中引用它。”现在,每次我们对cubeRb变量做什么,都会直接影响到我们玩家坦克上的刚体组件。不一定要用变量,但是比每次都输入GetComponent<Rigidbody>()要方便。我们还通过在当前系统中缓存 MonoBehaviour 组件来节省性能。

为了保持我们的代码有组织和干净,我们将利用许多功能,并有一个更加模块化的方法。为了获得操纵杆输入,我们将使用下面的函数。你可以把它加在Start后面。

bool GetInput(string horizontal, string vertical) {
 input.x = SimpleInput.GetAxisRaw(horizontal) * speed;
 input.y = SimpleInput.GetAxisRaw(vertical) * speed;

 return (Mathf.Abs(input.x) > 0.01f) || (Mathf.Abs(input.y) > 0.01f);
}

基本上,我们正在创建一个名为GetInput的函数。我们将向它传递两个字符串,每个字符串分别对应于操纵杆的水平轴和垂直轴。

然后,我们将使用SimpleInput.GetAxisRaw(<axisName>)获取这些轴的当前数值,将它们乘以速度变量中保存的浮点值,并将它们存储在input的 X 或 Y 位置;,我们的Vector2变了。

此外,该函数将返回一个布尔值。如果我们的Vector2变量input的两个值中至少有一个值大于或小于但不等于 0,它将返回true。返回的值true可以被解释为“操纵杆正在被交互”,因为简单输入操纵杆在没有被保持/触摸时,其两个轴的值都是 0。

由于操纵杆轴输入可以小于 0 (-1 到 1),我们可以制定一个公式,例如“如果水平轴小于 0 或水平轴大于 0 或垂直轴小于 0 或垂直轴大于 0,则返回true,否则返回 false”,在 UnityScript 中,该公式可以写成:

if (input.x < 0 || input.x > 0 || input.y < 0 || input.y > 0) {
 return true;
} else {
 return false;
}

也许你已经注意到了,if语句中的条件本身会给出一个truefalse值,所以我们可以自己返回这个值,而不是生成一个又长又大的if-else语句。现在整个陈述已经简化为

return (input.x < 0 || input.x > 0 || input.y < 0 || input.y > 0);

我们可以通过使用已经可用的Mathf.Abs()函数来进一步简化。Abs部分代表“绝对”。这意味着,对于传递给该函数的任何数字,它都将返回其绝对值。如果你给它传递一个正值,将不会有任何变化,但如果你传递一个负值,它将被转换成一个正数。例如,向函数一次传递一个值 0、-9.88、12.5 和-78.489 将返回 0、9.88、12.5 和 78.489。这就是我如何获得前面图片中的 return 语句。请随意使用任意数量的括号,以保持代码的整洁。

为了真正移动玩家,我们将再次创建并使用另一个函数。

void MovePlayer() {
 cubeRb.velocity = Vector3.Normalize(new Vector3(input.x, 0, input.y)) * speed;
}

简而言之,我们将设置玩家坦克刚体的速度(通过使用引用它的cubeRb变量)来匹配我们的水平和垂直输入。由于我们的刚体需要一个Vector3的速度值(在 3D 轴上),我们的Vector2变量inputY值将对应于这里的 Z 轴。我们还将使我们的Vector3值正常化,这样玩家坦克在对角移动时不会跑得更快。这将迫使我们的Vector3的大小为 1,所以我们将再次乘以速度变量中的值。你还会注意到我们不退还任何东西。这是因为我们的函数被标记为void

对于上下文,我们将再次获取输入,但稍后将它们存储在输入Vector2变量中,用于负责旋转刀架的轴。rotator是一个引用带有转换组件的游戏对象的变量(我们立方体上的球体)。我们想让它绕 Y 轴旋转。默认情况下,旋转以四元数格式表示,而不是以Vector3格式表示。因此,要使用它们的变换组件根据Vector3旋转游戏对象,我们需要修改它们的eulerAngles属性。

void RotateTurret() {
 rotator.eulerAngles = new Vector3(0, Mathf.Atan2(input.x, input.y) * 180 / Mathf.PI, 0);
}

如果你学过一点三角学,你就会知道,为了求出两条线之间的角度,我们用 tan。我们正在做完全相同的事情:找到 X 和 Y 操纵杆输入之间的角度。因为我们要获得的角度是弧度形式的,我们必须把它转换成度。我们可以将该值乘以 180,然后除以 pi ( Mathf.PI)或者只乘以Mathf.Rad2Deg,本质上做的是同样的事情。最后,在获得以度为单位的角度后,我们只需创建一个新的Vector3变量,赋予它的Y值一个与我们的角度相等的值,并将其赋给我们希望旋转的游戏对象的变换的eulerAngles属性——在我们的例子中,是我们的旋转体。

为了完成这个脚本,我们必须调用我们的函数,以便使用它们。之前,我们讨论了一个名为Update()的游戏循环,它运行每一帧并执行放在其花括号内的代码。因为我们现在正在处理刚体,因此,物理相关的东西,最好利用另一个名为FixedUpdate()的函数,它每隔一段时间运行一次,而不是每帧运行一次。这会让我们的游戏看起来更流畅。

void FixedUpdate() {
 if (GetInput("Horizontal", "Vertical")) {
  MovePlayer();
}

 if (GetInput("MouseX", "MouseY")) {
  RotateTurret();
 }
}

在第一行,我们使用了GetInput函数,传递了"Horizontal""Vertical"轴。输入变量Vector2将保存这两个轴的当前值。if语句将确保MovePlayer()函数仅在玩家当前正在与移动操纵杆交互时被调用。

类似地,我们再次调用GetInput函数,但是这一次,传递 Look 操纵杆的轴。如果玩家与后者互动,那么只有炮塔(旋转体)才会旋转。如果我们没有这个检查,那么每次我们放开 Look 操纵杆时,炮塔都会跳回原来的位置(指向上),这有点破坏游戏性。

如果你被困在某个地方,这里有完整的代码。但是,总是建议您自己键入代码。函数不必在另一个函数之前或之后键入。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SimpleInputNamespace;

public class playerMovement : MonoBehaviour {
 public Transform rotator;
 private Rigidbody cubeRb;
 public float speed = 5.0f;
 private Vector2 input;

 void Start() {
  cubeRb = GetComponent<Rigidbody>();
 }

 void FixedUpdate() {
  if (GetInput("Horizontal", "Vertical")) {
   MovePlayer();
  }

  if (GetInput("MouseX", "MouseY")) {
   RotateTurret();
  }
 }

 bool GetInput(string horizontal, string vertical) {
  input.x = SimpleInput.GetAxisRaw(horizontal) * speed;
  input.y = SimpleInput.GetAxisRaw(vertical) * speed;

  return (Mathf.Abs(input.x) > 0.01f) || (Mathf.Abs(input.y) > 0.01f);
 }

 void MovePlayer() {
  cubeRb.velocity = Vector3.Normalize(new Vector3(input.x, 0, input.y)) * speed;
 }

 void RotateTurret() {
  rotator.eulerAngles = new Vector3(0, Mathf.Atan2(input.x, input.y) * 180 / Mathf.PI, 0);
 }
}

完成后,只需保存脚本并返回 Unity 编辑器。将脚本拖放到玩家坦克上或添加组件➤玩家移动。当玩家坦克被选中时,从脚本的 rotator 字段的层次中拖放 Rotator 游戏对象。进入游戏模式,并尝试与移动和查看操纵杆互动。一个应该使玩家坦克移动并向指定的方向前进,而另一个应该使炮塔看起来在旋转。

摄像机定位

在测试上一部分的游戏时,你可能已经注意到玩家坦克会离开屏幕。这不是我们想要的,所以,在这一节中,我们将配置主摄像机平滑地跟随玩家坦克。这一次,创建一个名为 cameraFollow 的脚本并打开它。

我们将只使用两个变量:一个名为player的转换变量,它将引用我们的玩家坦克的转换,以及一个浮点变量height

public Transform player;
public float height = 12.5f;

为了让摄像机跟随玩家,我们必须使用一个函数,比如Update(),将摄像机的位置设置为玩家的位置,除了Y值,我们将把它设置为保存在height变量中的值,这样我们就可以有一个自上而下的视图。

void LateUpdate() {
 this.transform.position = new Vector3(player.position.x, height, player.position.z);
}

代替传统的Update(),我们将使用LateUpdate(),它非常类似,但是在其他更新循环运行之后运行。将与摄像机运动相关的代码放入其中是一个很好的做法,因为这意味着在摄像机必须移动之前,所有与运动相关的代码已经被首先执行了。这是完整的代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class cameraFollow : MonoBehaviour {

 public Transform player;
 public float height = 12.5f;

 void LateUpdate() {
  this.transform.position = new Vector3(player.position.x, height, player.position.z);
 }
}

保存脚本,将它添加到主相机游戏对象中,将玩家游戏对象从层级中拖放到脚本的玩家字段中,然后点击播放按钮。相机现在应该跟随玩家坦克。

5.3.5 让玩家射出子弹

本节分为两部分:制作子弹和射击。要制作子弹,首先在你的层级中创建一个球体(3D 物体➤球体)游戏物体。将其命名为 Bullet,并赋予其位置为(0,1,2),旋转为(0,0,0),缩放为(0.3,0.3,0.3)。接下来,制作一个名为 Bullet 的标签,并将其分配给游戏对象。此外,在子弹游戏对象的网格渲染器组件中的照明属性下,将投射阴影设置为关闭,以便子弹看起来没有阴影(图 5-31 )。

img/491558_1_En_5_Fig31_HTML.jpg

图 5-31

子弹游戏对象#1 上的组件

保持球体碰撞器属性不变,然后给子弹游戏对象添加一个刚体组件。取消勾选使用重力,仅勾选限制条件下的冻结位置 Y。此时,您可能还想为子弹游戏对象创建/添加一个材质。我将使用浅绿色的(图 5-32 )。

img/491558_1_En_5_Fig32_HTML.jpg

图 5-32

子弹游戏对象#2 上的组件

最后,我们希望我们的子弹最终被销毁,这样它们就不会一直留在我们的游戏中,导致游戏性能下降。为此,创建一个名为 destroyer 的脚本,等待它完成编译(见右下角的小加载图标),然后将其添加到 Bullet 组件上。打开脚本。以下是完整的代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class destroyer : MonoBehaviour {
 public float delay = 3.0f;

 void Start() {
  Destroy(this.gameObject, delay);
 }
}

在第一行,在类内部,我们创建了一个新的名为delay的公共 float 变量,并赋予它一个初始值3。然后,在我们脚本的Start函数中,我们告诉 Unity 给Destroy这个脚本所附加的游戏对象,在对应于当前保存在delay变量中的值的几秒钟之后。如果我们不向Destroy函数传递任何第二个参数,它会在游戏一开始就立即破坏我们的 GameObject。最后,在你的项目窗口中,创建一个名为 Prefabs 的文件夹,并将子弹游戏对象从你的层级中拖放到该文件夹中。您已经成功制作了一个预制组件!你现在可以安全地从你的场景中摧毁子弹游戏物体。

在接下来的步骤中,您还需要在发射子弹时播放声音效果。你可以从 https://raw.githubusercontent.com/EdgeKing810/SphereShooter/master/Assets/Sounds/fireBullets.wav 下载我要用的那个(右击另存为)。创建一个名为 Sounds 或 Sound Effects 的文件夹,并将.wav文件或您将要使用的声音文件从文件管理器拖放到 Unity 编辑器中。接下来,将一个音频源组件添加到你的玩家坦克游戏对象中,取消勾选“唤醒时播放”,并在 AudioClip 属性中分配你刚刚导入的音频文件(图 5-33 )。

img/491558_1_En_5_Fig33_HTML.jpg

图 5-33

玩家游戏对象上的音频源组件

是时候给我们的玩家坦克发射子弹的能力了!创建一个名为 bulletSystem 的新脚本并打开它。现在与 Look 操纵杆交互只会导致旋转器旋转,从而将炮塔瞄准我们想要的方向。然而,如果我们想要拍摄,操纵杆的手柄(拇指)必须离操纵杆的中心超过一个规定的距离。接下来,我们要检查从玩家最后一次射击开始是否已经过了足够的时间,以便能够再次射击。最后,如果满足这两个条件,我们只需在 bulletEnd 位置实例化(生成)一颗子弹(空的游戏对象,是我们炮塔的子对象),给子弹一个力,推动它向前,并发出射击声。

首先,将Using SimpleInputNamespace;行添加到脚本中,因为我们稍后也将获取操纵杆输入。以下是我们将在该脚本中使用的变量:

public Transform bulletEnd;
public Rigidbody bulletPrefab;

public float force = 500.0f;

float currentTime;
public float delay = 0.5f;

AudioSource audioSource;

将引用我们的炮塔游戏对象的子对象的变换组件,在那里项目符号应该被实例化。不出所料,bulletPrefab将参考我们创建的子弹预制体。force变量中的浮点值将定义已经实例化的子弹的推进力。currentTimedelay分别代表从游戏开始发射最后一颗子弹的秒数和玩家必须等待发射另一颗子弹的秒数。最后,audioSource private变量将引用玩家坦克上的音源,稍后播放指定的音效。

void Start() {
 audioSource = GetComponent<AudioSource>();
}

Start函数中,我们将在audioSource变量中引用脚本附加到的游戏对象(我们的玩家坦克)上的音频源。

由于我们的脚本必须处理施加力,因此,物理,我们将使用FixedUpdate

 void FixedUpdate() {
  if (((Mathf.Abs(SimpleInput.GetAxisRaw("MouseX")) > 0.75f) ||
       (Mathf.Abs(SimpleInput.GetAxisRaw("MouseY")) > 0.75f)) &&
     ((Time.time - currentTime > delay) || (currentTime < 0.01f))) {

    currentTime = Time.time;
    audioSource.Play();

    Rigidbody bulletInstance = Instantiate(bulletPrefab, bulletEnd.position, bulletEnd.rotation) as Rigidbody;
    bulletInstance.AddForce(bulletEnd.forward * force);
  }
 }
}

让我们首先分析一下,如果只有true,允许FixedUpdate循环中所有指令运行的条件。

((Mathf.Abs(SimpleInput.GetAxisRaw("MouseX")) > 0.75f) || (Mathf.Abs(SimpleInput.GetAxisRaw("MouseY")) > 0.75f))

只有当MouseX和/或MouseY当前具有大于 0.75 或小于 0.75 的值时,该语句才会产生true。在前面几节中,我已经解释了 playerMovement 脚本的类似语句。接下来,我们将从该语句中获得的布尔值与下面的一个链接起来:

((Time.time - currentTime > delay) || (currentTime < 0.01f))

只有当从最后一次发射子弹起已经过了比 delay 变量中保存的值更多的秒数,或者如果currentTime小于 0.01,这意味着这是我们第一次发射子弹(所以不需要等待),这个条件才会返回true。如果这两个条件都是true(因此有了&&符号),只有这样我们才会在if语句中运行代码。

将运行的前两行将把自游戏开始以来经过的秒数的值赋给 currentTime 变量,以指示子弹最后一次发射的时间是现在,并播放在 AudioSource 组件中分配的音频剪辑。

最后,我们正在创建一个名为bulletInstance的新刚体变量,当我们在场景中的bulletEnd位置和旋转实例化(克隆)子弹预设时,我们将其分配给该变量。bulletInstance,它现在在场景中拿着我们的子弹预制的副本,将被给予一个力,该力等于在类似命名的变量中存在的值,并且在我们炮塔的向前方向上(或者在这种情况下是bulletEnd)。

以下是完整的代码,如果你错过了什么。保存脚本并返回 Unity 编辑器。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using SimpleInputNamespace;

public class bulletSystem : MonoBehaviour {
 public Transform bulletEnd;
 public Rigidbody bulletPrefab;

 public float force = 500.0f;

 float currentTime;
 public float delay = 0.5f;

 AudioSource audioSource;

 void Start() {
 audioSource = GetComponent<AudioSource>();
 }

 void FixedUpdate() {
 if (((Mathf.Abs(SimpleInput.GetAxisRaw("MouseX")) > 0.75f) || (Mathf.Abs(SimpleInput.GetAxisRaw("MouseY")) > 0.75f)) &&
 ((Time.time - currentTime > delay) || (currentTime < 0.01f))) {

   currentTime = Time.time;
   audioSource.Play();

   Rigidbody bulletInstance = Instantiate(bulletPrefab, bulletEnd.position, bulletEnd.rotation) as Rigidbody;
   bulletInstance.AddForce(bulletEnd.forward * force);
  }
 }
}

将脚本分配给玩家坦克游戏对象。在层次中展开玩家游戏对象的子对象,并将 bulletEnd 游戏对象拖放到玩家游戏对象上脚本实例的 bulletEnd 字段中。以类似的方式,从项目窗口的项目符号预置字段中拖放项目符号游戏对象。进入播放模式,测试一切正常。你现在应该会射子弹了。

5.4 敌人

在本节中,我们将制作一个球形敌人,在游戏中实例化它的副本,并使这些副本以我们的玩家坦克为目标并向其移动。当敌人与玩家或子弹相撞时,也应该被消灭。让我们马上迈出第一步。

5.4.1 树敌

首先创建一个球体。创建并给它分配一个敌人的标签,并将这个新的球体游戏对象命名为敌人。把它放在(0,1.15,10)的位置,给它一个(0,0,0)的旋转,一个(1.5,1.5,1.5)的刻度。它的网格渲染器或球体碰撞器组件不需要修改任何属性。接下来,添加一个刚体组件,取消选中使用重力,并从约束选项卡冻结游戏对象的 Y 位置。

此外,添加一个音频源组件,并取消勾选唤醒时播放。这个音频源组件将会播放敌人被消灭的声音。下载以下音频文件,并将其导入到项目先前创建的声音文件夹中:

https://github.com/EdgeKing810/SphereShooter/blob/master/Assets/Sounds/explosion0.wav

在音频源的音频片段栏中分配“爆炸 0”。此时,你可能还想在敌人的游戏对象上创建/放置一个材质。我将创建和使用一个红色的(图 5-34 和 5-35 )。

img/491558_1_En_5_Fig35_HTML.jpg

图 5-35

敌人游戏对象#2 上的组件

img/491558_1_En_5_Fig34_HTML.jpg

图 5-34

敌人游戏对象#1 上的组件

为了让我们的游戏看起来更有趣,让我们给敌人添加一个轨迹渲染器。出于某种原因,我将在稍后的脚本阶段解释,创建一个新的空游戏对象作为我们的敌人游戏对象的子对象,并将其命名为 Trail Renderer。仅编辑其变换组件,并将其放置在(0,0,0)的位置。如果我们把它放得太高,轨迹渲染器会在玩家坦克的顶部渲染。

向子 GameObject 添加一个 Trail Renderer 组件。尝试在看起来像图形的东西上设置一个大约 0.35 的宽度值(首先右键单击,以设置精确的值),在“材质”下的元素 0 位置为其指定一个您选择的材质,并将“投射阴影”设置为“关闭”,在“照明”下(图 5-36 和 5-37 )。

img/491558_1_En_5_Fig37_HTML.jpg

图 5-37

轨迹渲染器游戏对象#2 上的组件

img/491558_1_En_5_Fig36_HTML.jpg

图 5-36

轨迹渲染器游戏对象#1 上的组件

5.4.2 从商店导入另一项素材

当我们的敌人与我们的玩家或子弹相撞时,我们会想要摧毁它(我们已经可以使用Destroy()做到这一点)。我们还可以添加一些视觉效果,比如一个爆炸粒子系统。幸运的是,素材商店里有一个包,可以提供我们需要的一切。下载并导入简单外汇素材(图 5-38 )。

img/491558_1_En_5_Fig38_HTML.jpg

图 5-38

素材存储中的简单 FX-卡通粒子素材

5.4.3 使我们的敌人移动并爆炸

敌人需要能够处理和做的一切都将被放入一个脚本中。从脚本文件夹中创建并打开一个名为“敌人”的脚本。我们会制造和使用许多变量。

const string playerTag = "Player";
const string bulletTag = "Bullet";
public float minSpeed = 1.0f;
public float maxSpeed = 6.0f;
float speed;
GameObject player;
public GameObject enemyExplosionPrefab;
AudioSource audioSource;

我们的玩家和项目符号使用的标签将存储在两个字符串常量中,分别标识为playerTagbulletTag。因为我们将在我们的代码中进一步使用这些常量,所以从长远来看使用这些常量会更容易引用它们,因为如果我们将来改变这些游戏对象的标签,我们将只拥有保存在这些常量中的值,而不是我们代码中的所有引用。

我们要做的另一件事是让我们的敌人以随机速度移动,让游戏更有趣。这个随机速度将在包含在minSpeedmaxSpeed变量中的两个浮点值的范围内,并存储在一个名为speed的变量中,以备后用。

玩家游戏对象变量将被用来包含对我们的玩家坦克游戏对象的引用。由于敌人将使用预设来制造,并在我们的场景中进行实例化,所以将玩家变量设为公共变量是没有用的,因为我们无法将玩家从我们的场景拖放到我们项目中的敌人预设中。这样做是没有意义的,例如,如果一个不同的场景被打开,一个预置不能从那个场景中引用一个游戏对象。取而代之的是,我们将编写一些东西,当它被实例化时,敌人可以自动找到玩家。

下一个游戏对象变量是enemyExplosionPrefab,它将被用来引用我们之前导入的简单 FX 素材中的爆炸预设。

audioSource只是一个变量,它将引用敌人上的音频源组件来播放我们在其音频剪辑字段中分配给它的爆炸声音。

我们将在我们的Start函数中放置一些代码,这样它只被执行一次,在我们的敌人游戏对象生命周期的开始。

void Start() {
 speed = Random.Range(minSpeed, maxSpeed);
 audioSource = GetComponent<AudioSource>();
 player = GameObject.FindWithTag(playerTag);
}

首先,我们将从我们设置的最小和最大值计算一个随机速度,并使用Random.Range函数将该浮点值存储在speed变量中。Random.Range将返回一个大于等于minSpeed但小于maxSpeed的随机值。

接下来,我们将在audioSource变量中存储一个对当前游戏对象(在我们的例子中,是我们的敌人)的音频源组件的引用。我们还使用了GameObject.FindWithTag函数,将playerTag常量中的字符串作为参数传递,以引用玩家坦克中的游戏对象。将搜索一个带有我们作为参数传递的标签的游戏对象,一旦找到一个符合标准的,它将返回它。

对于游戏循环,我们可以利用Update或者FixedUpdate

void FixedUpdate() {
 if (player) {
  transform.position = Vector3.MoveTowards(transform.position, player.transform.position, speed * Time.deltaTime);
 } else {
  GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
 }
}

在 e 中,我们将执行两个动作中的一个,这取决于我们的场景中是否有玩家坦克游戏对象。例如,如果我们的玩家坦克游戏对象在当前场景中被摧毁,player变量将保存一个值null,而不是一个实际的游戏对象引用。

检查“如果player变量当前正在引用一个游戏对象”的方法可以是if (player != null)或简单的if (player)。从逻辑上讲,如果player变量没有null值,那么它必须对应于一个游戏对象,在我们的例子中是玩家坦克,因为它是唯一一个使用playerTag常量中的值作为标签的游戏对象。

因此,如果player变量实际上对应于某个东西,我们希望敌人向其变换组件中的位置移动。要做到这一点,我们可以简单地将敌人游戏对象的变换位置值设置为等于由Vector3.MoveTowards函数返回的Vector3值。在我们的例子中,Vector3.MoveTowards使用了三个参数。第一个是 a Vector3值(我们敌人的当前位置),我们想把它逐渐变成作为第二个参数传递的值(玩家坦克的位置)。第三个值定义了我们希望第一个值变成第二个值的速率或速度;因此,我们传递了speed变量。每次FixedUpdate运行时,将返回一个更接近玩家位置的Vector3值。当使用Update运行每一帧时,将该值乘以Time.deltaTime会使过渡更加线性和平滑。在FixedUpdate不会有什么影响。

否则,如果我们的player变量对应于null,我们将希望让我们的敌人游戏对象停止移动并留在原地。如果我们一开始没有包含那个if语句,那么如果玩家坦克游戏对象被摧毁,我们会收到很多错误,因为脚本会试图将敌人移动到null的位置,这是无效的。

我们还将利用另外两个函数。接下来是OnCollisionEnter,当敌人与任何东西发生碰撞时,它会在我们的脚本中自动运行。我们传递给这个函数的参数对应于游戏对象的碰撞器与脚本所在的游戏对象的碰撞器所造成的碰撞。在我们的例子中,该参数将等于任何游戏对象的碰撞器与我们的敌人游戏对象(的碰撞器)所造成的碰撞。我们将把 Collider 引起的碰撞称为局部变量col

void OnCollisionEnter(Collision col) {
 if (col.gameObject.CompareTag(bulletTag)) {
  Destroy(col.gameObject);
 }

 if (col.gameObject.CompareTag(playerTag) ||
     col.gameObject.CompareTag(bulletTag)) {
  DestroyEnemy();
 }
}

第一个if条件检查与我们的敌人相撞的游戏对象是否是子弹游戏对象。我们通过访问导致碰撞的碰撞器的游戏对象,然后使用保存在bulletTag常量中的字符串值作为参数,检查它是否与子弹游戏对象具有相同的标签。我们也可以编写if (col.gameObject.tag == bulletTag),但是我的编写方式是推荐的方式,这也提供了一些性能上的好处。

如果是这种情况,我们将希望摧毁刚刚碰撞的子弹游戏对象。在下一个if条件中,我们检查敌人的游戏对象是否与子弹或玩家的坦克相撞。如果发生了这种情况,我们希望调用一个名为DestroyEnemy的函数,它将决定当敌人“死亡”时应该发生什么。

void DestroyEnemy() {
 GameObject explosionInstance = Instantiate(enemyExplosionPrefab, transform.position, enemyExplosionPrefab.transform.rotation);
 Destroy(explosionInstance, 5.0f);

 audioSource.Play();

 Transform trailRenderer = transform.GetChild(0);
 if (trailRenderer) {
  trailRenderer.parent = null;
  Destroy(trailRenderer.gameObject,   trailRenderer.GetComponent<TrailRenderer>().time);
 }

Destroy(this.gameObject);
}

DestroyEnemy函数中,我们要做的第一件事是从简单的 FX 实例化敌人的爆炸预设游戏对象,在enemyExplosionPrefab变量中引用,在敌人游戏对象的位置,但是在爆炸预设本身的旋转,并且在一个新的本地GameObject变量中存储一个引用,我们将创建这个引用并命名为explosionInstance

在爆炸粒子系统被实例化并在场景中运行后,我们将在五秒钟后销毁爆炸粒子系统的游戏对象,而不是用许多无用的游戏对象来膨胀我们的场景(这只是系统完全运行的充足时间)。然后,我们将播放敌方音源组件中持有的音频片段(explosion0)。

因为我们想让我们的轨迹渲染器自动消失,而不是在敌人“死亡”时立即被摧毁,所以我们在一个名为trailRenderer的新变换变量中创建了对它的引用。调用transform.GetChild(0)返回当前游戏对象(我们的敌人游戏对象)变换的第一个子对象(0 是第一个索引)。

接下来,如果敌人有一个子游戏对象,trailRenderer不应该等于null。只有这样,我们才会将trailRenderer游戏对象的父对象或敌人游戏对象的第一个子对象设置为等于null。这将使游戏对象没有父对象,因此,不再是任何游戏对象的子对象。然而,正如我之前所讨论的,对于爆炸游戏对象的实例,我们也将销毁trailRenderer的游戏对象,但是这一次,不是考虑并给出一个合适的值,我们将获取轨迹渲染器组件本身清除其轨迹所需的时间,或者换句话说, 轨迹达到长度/宽度为 0 所需的时间,并将其作为第二个参数传递给Destroy函数,这将导致轨迹渲染器的游戏对象在轨迹长度/宽度为 0 时立即被销毁。 请记住,默认情况下,轨迹会随着时间的推移自动销毁自己的一部分。现在,你可能明白为什么我们之前为轨迹渲染器组件制作了一个新的游戏对象,而不是把它放在主要的敌人游戏对象上。

最后,我们立即摧毁敌人的游戏对象。以下是完整的脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy : MonoBehaviour {
 const string playerTag = "Player";
 const string bulletTag = "Bullet";
 public float minSpeed = 1.0f;
 public float maxSpeed = 6.0f;
 float speed;
 GameObject player;
 public GameObject enemyExplosionPrefab;
 AudioSource audioSource;

 void Start() {
  speed = Random.Range(minSpeed, maxSpeed);
  audioSource = GetComponent<AudioSource>();
  player = GameObject.FindWithTag(playerTag);
 }

 void FixedUpdate() {
  if (player) {
   transform.position = Vector3.MoveTowards(transform.position, player.transform.position, speed * Time.deltaTime);
 } else {
   GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
  }
 }

 void OnCollisionEnter(Collision col) {
  if (col.gameObject.CompareTag(bulletTag)) {
   Destroy(col.gameObject);
  }

  if (col.gameObject.CompareTag(playerTag) ||
      col.gameObject.CompareTag(bulletTag)) {
   DestroyEnemy();
  }
 }

 void DestroyEnemy() {
  GameObject explosionInstance = Instantiate(enemyExplosionPrefab, transform.position, enemyExplosionPrefab.transform.rotation);
  Destroy(explosionInstance, 5.0f);

  audioSource.Play();

  Transform trailRenderer = transform.GetChild(0);
  if (trailRenderer) {
   trailRenderer.parent = null;
   Destroy(trailRenderer.gameObject,   trailRenderer.GetComponent<TrailRenderer>().time);
  }

  Destroy(this.gameObject);
 }
}

保存脚本。当你回到 Unity 编辑器时,把脚本放到敌人的游戏对象上,在脚本的 enemyExplosionPrefab 字段中拖放 SimpleFX ➤预设➤ FX_Fireworks_Blue_Small 预设,或者一个类似的。如果你点击 Play,你应该会看到我们场景中唯一的敌人会向玩家坦克移动,并发出声音,当它被摧毁时会产生爆炸,无论是被子弹击中还是与玩家游戏对象碰撞。在第六章中,我们将通过在设定的繁殖点随机繁殖一些敌人来改进游戏,增加(玩家的)生命值和高分,为游戏开始和玩家失败制作菜单,等等。

六、改进和构建球形射手

虽然从技术上来说,游戏可以按照上一章的配置来玩,但在这一章,我们将增加几个新的机制和特性。最后,我将讨论游戏中可以包含的其他功能,如果你希望继续开发它并触发一个版本,以便我们有一个可以安装在 Android 设备上的独立应用。

6.1 产卵的敌人

一个敌人很好,但如果我们有更多的敌人,游戏会更好。在这一节中,我们将在场景中定义的位置放置空的游戏对象,并在设定的延迟时间内,在定义的随机位置实例化(繁殖)敌人。

创建一个空的游戏对象,命名为 SpawnPoints,放在(0,1.15,0)的位置。它的旋转和缩放已经设置为(0,0,0)和(1,1,1)。我们可以将其标记为静态,但这不会有什么不同。

创建四个空的游戏对象作为 SpawnPoints 的子对象(图 6-1 )。你想怎么命名就怎么命名,分别放在(0,0,15),(15,0,0),(0,0,-15),和(-15,0,0)。

img/491558_1_En_6_Fig1_HTML.jpg

图 6-1

产卵点游戏对象

接下来,将敌人游戏对象从场景中拖放到项目窗口的预设文件夹中,并将其从场景中删除。

创建一个新的脚本,将其命名为 enemySpawner,等待它编译,将其放置在场景中的 SpawnPoints 游戏对象上,然后打开它。这就是在我们的场景中滋生敌人的原因。

public float delay;
public GameObject enemy;

我们将在一个名为delay的公共浮动变量中存储生成敌人之间的延迟,并在enemy中存储对敌人预设游戏对象的引用。

void SpawnEnemy() {
    int randomPos = (int)Random.Range(0, transform.childCount);
    Instantiate(enemy, transform.GetChild(randomPos).position, enemy.transform.rotation);
}

为了制造敌人,我们将使用一个名为SpawnEnemy的函数。在这个函数中,我们要做的第一件事是随机选取一个游戏对象的子对象的索引。有四个可能的产卵点可用,所以我们将最小值(0)和最大值(transform.childCount)传递给Random.Rage函数,以随机选择一个。通过这样做,我们可以添加尽可能多的种子点,而不用每次都编辑代码。由于将要返回的值将是一个浮点数,我们将其转换为一个整数(强制转换)并存储在本地变量randomPos中。在下一步中,我们在刚刚得到的索引处找到 SpawnPoints 的子对象,并在该位置和敌人的旋转处实例化一个敌人的游戏对象。

void Start() {
    InvokeRepeating("SpawnEnemy", 0.0f, delay);
}

最后,一旦游戏开始,每隔<delay>秒就会调用SpawnEnemy函数,使用InvokeRepeating函数,它有以下三个参数:

  1. 要调用的函数

  2. 开始时间

  3. 应该多久调用一次函数

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemySpawner : MonoBehaviour {
   public float delay;
   public GameObject enemy;

   void Start() {
       InvokeRepeating("SpawnEnemy", 0.0f, delay);
   }

   void SpawnEnemy() {
       int randomPos = (int)Random.Range(0, transform.childCount);
       Instantiate(enemy, transform.GetChild(randomPos).position, enemy.transform.rotation);
   }
}

只需设置一个你选择的延迟值,然后把敌人的预设拖回编辑器的敌人区域。测试敌人是否正在繁殖,向玩家坦克移动,并被消灭(图 6-2 )。

img/491558_1_En_6_Fig2_HTML.jpg

图 6-2

作为组件的enemySpawner脚本

6.2 评分

现在,当我们杀死一个敌人时,它就会被摧毁并消失。然而,如果我们能保留一个类似分数的东西,并随着敌人被杀而递增,那就太好了。

首先,在 Canvas GameObject 下创建一个 UI 文本元素。将其命名为 ScoreText,放置在(-660,350,0),并分别赋予其宽度和高度 375 和 100(图 6-3 )。

img/491558_1_En_6_Fig3_HTML.jpg

图 6-3

ScoreText UI 元素的 Rect 转换组件

对于文本组件本身,我们不会使用 Best Fit,因为随着我们的游戏进行,游戏中的文本 UI 元素似乎变得更大/更小,这看起来有点尴尬。因此,我们将字体大小设置为一个最大值,该值预计足以容纳我们希望存储在该 UI 元素中的所有信息。

我还添加了虚拟文本,并将字体设置为粗体。此外,我将文本水平对齐设置为左侧,垂直对齐设置为中间,并赋予黄色(图 6-4 )。

img/491558_1_En_6_Fig4_HTML.jpg

图 6-4

coretext ui 元素的文本组件

我们将希望我们的敌人发送一个调用,每次他们被子弹击中时增加分数,并相应地更新 ScoreText UI 元素。正如我之前提到的,我们不能直接引用场景中的东西到项目窗口中的预设。这是介绍实例的好时机。使用静态属性并使它们在场景中可公开访问,可以从场景中的任何地方访问和调用类和函数。

为了演示这一点,创建一个新的 GameObject,将其命名为 ScriptManager,并创建和打开一个名为 scoreManager 的脚本。将using UnityEngine.UI行添加到脚本中,以便我们能够稍后修改scoreTexttext值。

public static scoreManager instance;
public Text scoreText;
int score;

我们将使用三个变量。第一个将被命名为instance,它实际上对应于我们脚本的一个实例,可以从其他脚本中调用这个实例。另外两个变量,scoreTextscore,分别引用我们之前创建和配置的 UI 文本元素,并存储当前得分。

void UpdateScore() {
    scoreText.text = "Score: " + score.ToString();
}

我们也将有一个函数来更新scoreText的内容,这样我们就不必使用一种形式的Update()函数来不断地检查或更新。请注意,score 变量中的内容必须转换为字符串格式。

void Awake() {
    if (instance) {
        Destroy(this.gameObject);
    } else {
        instance = this;
    }
    UpdateScore();
}

public void IncreaseScore(int amount) {
    score += amount;
    UpdateScore();
}

在 Awake 函数中,我们将在场景中初始化其他脚本之前为该脚本创建实例。我们首先检查这个脚本的实例是否已经存在,如果存在,我们销毁当前的实例。否则,我们将场景中的脚本实例设置为当前实例。最后,我们更新了我们的scoreText,这样它就不会保留我们的虚拟文本。

public void IncreaseScore(int amount) {
    score += amount;
    UpdateScore();
}

我们还会有一个last函数,这样我们就可以改变分数值。我们可以向该函数传递一个值来指定分数的增量。我们只需要更新scoreText就可以了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class scoreManager : MonoBehaviour {
   public static scoreManager instance;
   public Text scoreText;
   int score;

   void Awake() {
       if (instance) {
           Destroy(this.gameObject);
       } else {
           instance = this;
       }
       UpdateScore();
   }

   public void IncreaseScore(int amount) {
       score += amount;
       UpdateScore();
   }

   void UpdateScore() {
       scoreText.text = "Score: " + score.ToString();
   }
}

回到编辑器中,我们将脚本放在 ScriptManager 游戏对象上,并将实际的 ScoreText UI 元素拖动到脚本的scoreText字段中(图 6-5 )。

img/491558_1_En_6_Fig5_HTML.jpg

图 6-5

作为组件的 scoreManager 脚本

如果你进入游戏模式,当你杀死敌人时不会有太大变化。这是因为敌人还没有做任何与增加分数相关的事情。打开敌人脚本,并在负责销毁与敌人碰撞的子弹的脚本后添加以下行:

scoreManager.instance.IncreaseScore(1);

这一行将导致敌人访问场景中的scoreManager实例并调用IncreaseScore函数,传入一个值1,这将使分数增加 1。

void OnCollisionEnter(Collision col) {
     if (col.gameObject.CompareTag(bulletTag))  {
         Destroy(col.gameObject);
         scoreManager.instance.IncreaseScore(1);
     }
}

保存脚本,这次进入播放模式,每杀死一个敌人分数就要加 1(图 6-6 )。如果和你相撞的是敌人,什么都不会改变。

img/491558_1_En_6_Fig6_HTML.jpg

图 6-6

杀死两个敌人后分数显示应该是什么样子

6.3 制作菜单

在这一部分,我们将构建玩家可以与之交互的三个菜单。第一个会在游戏开始时显示,另一个会在玩家失败时显示,最后一个会在玩家暂停游戏时显示。然而,我们将首先编码我们将需要这些菜单的 UI 按钮执行的一切。

6.3.1 所需的编码工具

创建并打开名为 utilityScript 的脚本。因为 UI 按钮已经可以做很多事情了,比如禁用游戏对象,我们不需要编写所有我们需要的行为。将行using UnityEngine.SceneManagement;添加到脚本中,以便我们稍后可以调用相关的方法/函数。

public void Restart() {
  SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

第一个功能将允许玩家在失败时重新开始游戏。使用 Unity 的 SceneManager,我们只需再次加载当前场景。我们通过使用LoadScene函数并传递当前场景的名称作为参数来实现。

下一个函数是关于退出游戏的。我们用Application.Quit()

public void Quit() {
    Application.Quit();
}

为了暂停和取消暂停游戏,我们将Time.timeScale值设置为 0 或 1。值为 0 将使所有东西都无法移动。

public void Pause() {
    Time.timeScale = 0.0f;
}

public void UnPause() {
    Time.timeScale = 1.0f;
}

最后,我们在游戏开始时暂停游戏,以便我们可以在第一个菜单上选择要做的事情。

void Start() {
    Pause();
}

以下是完整的代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class utilityScript : MonoBehaviour {
   void Start() {
       Pause();
   }

   public void Restart() {    SceneManager.LoadScene(SceneManager.GetActiveScene().name);
   }

   public void Quit() {
       Application.Quit();
   }

   public void Pause() {
       Time.timeScale = 0.0f;
   }

   public void UnPause() {
       Time.timeScale = 1.0f;
   }
}

将脚本放在 ScriptManager 游戏对象本身上。

开始菜单

每当加载场景时,都会显示该菜单。在场景中,禁用 SpawnPoints 游戏对象(选中时,在检查器中取消选中其名称左侧的复选框)。我们不希望在我们点击播放按钮之前就有敌人出现。

在 Canvas 下,创建一个新的名为 StartMenu 的空游戏对象。确保它位于层次结构中画布子元素列表的底部,以便它显示在所有其他元素的顶部。

使名为 Background 的 UI 图像成为 StartMenu 的子图像。给它一个比你在游戏窗口中使用的分辨率更大的宽度和高度。我使用的是 1920 × 1080 的分辨率,所以我将宽度指定为 2500,高度指定为 1250。如果你愿意,给 UI 图像组件一个你选择的颜色,并降低它的不透明度。我使用的是有点青色的颜色,不透明度为 190(图 6-7 )。

img/491558_1_En_6_Fig7_HTML.jpg

图 6-7

背景用户界面元素

接下来,您可以将一个文本 UI 元素作为 StartMenu 的子元素(图 6-8 )。命名为 Title。它将被用作我们游戏名称的标签。使用最佳拟合将其调至您想要的大小,并在两个轴的中心对齐(图 6-9 )。

img/491558_1_En_6_Fig9_HTML.jpg

图 6-9

标题 UI 元素的文本组件

img/491558_1_En_6_Fig8_HTML.jpg

图 6-8

标题 UI 元素的 Rect 转换组件

我们现在只需要两个按钮。创建一个 UI 按钮元素,仍然作为 StartMenu 的子元素,并将其命名为 PlayButton。我已经为 PlayButton 的图像组件选择了绿色,并将其设为 400 单位宽,100 单位高,并将其放置在(0,-180,0)(图 6-10 )。

img/491558_1_En_6_Fig10_HTML.jpg

图 6-10

PlayButton UI 元素

在 PlayButton 按钮组件的 OnClick()部分(图 6-11 ,点击三次加号图标(+)。在第一个槽中,拖放开始菜单并选择GameObject.SetActive(bool)功能,同时确保将要出现的复选框保持未选中状态。在第二个例子中,做类似的事情,除了这次使用 SpawnPoints 游戏对象,并确保复选框被选中。最后,在 ScriptManager 中拖动并选择utilityScript.UnPause()

img/491558_1_En_6_Fig11_HTML.jpg

图 6-11

PlayButton 的按钮组件的OnClick功能

要完成 playButton 元素,请选择它的子元素(名为 Text),使它显示 Play,并根据需要更改它的颜色。使用加粗字体并勾选最佳匹配(图 6-12 )。

img/491558_1_En_6_Fig12_HTML.jpg

图 6-12

PlayButton 的子级的文本组件

我们现在只需要退出按钮。复制 PlayButton,把新元素命名为 ExitButton,给它一个红色,放在(0,-315,0),给它的 Text UI 元素子元素一个 Exit 的值(图 6-13 )。

img/491558_1_En_6_Fig13_HTML.jpg

图 6-13

ExitButton 子级的文本组件

在 ExitButton 的 OnClick()中,引用 ScriptManager 并调用utilityScript.Quit()函数(图 6-14 )。

img/491558_1_En_6_Fig14_HTML.jpg

图 6-14

ExitButton 的按钮组件的OnClick()功能

你的层级应该包含以下元素和游戏对象作为画布游戏对象的子对象(图 6-15 ):

img/491558_1_En_6_Fig15_HTML.jpg

图 6-15

当前构成画布中的子对象的游戏对象

请注意,在编辑器中单击 Exit 按钮不会有任何作用。您可以进入播放模式并确保按钮现在工作(图 6-16 )。

img/491558_1_En_6_Fig16_HTML.jpg

图 6-16

开始菜单的外观

6.3.3 暂停菜单

在我们制作真正的暂停菜单之前,让我们制作一个可以在游戏中按下的暂停按钮。首先禁用开始菜单游戏对象,这样我们可以在游戏/场景视图中更容易地预览我们所做的一切。在项目窗口的纹理文件夹中下载并导入如下图片: https://raw.githubusercontent.com/EdgeKing810/SphereShooter/master/Asseimg/Pause.png 。选择它并在检查器中将其标记为 Sprite2D,然后点击应用(图 6-17 )。

img/491558_1_En_6_Fig17_HTML.jpg

图 6-17

导入 PauseButton 的纹理

接下来,创建一个 UI 按钮元素作为 Canvas GameObject 的子元素,并将其放在 StartMenu GameObject 之上或所有子元素的顶部。命名为 PauseButton。复制开始菜单游戏对象,命名新实例 PauseMenu,并启用它的游戏对象(图 6-18 )。

img/491558_1_En_6_Fig18_HTML.jpg

图 6-18

当前构成画布中的子对象的游戏对象

删除 PauseButton 的子级文本 UI 元素。除非禁用开始菜单和暂停菜单的游戏对象,否则您可能看不到暂停按钮。将 PauseButton 放置在(0,400,0)处,并使其宽度和高度都为 100。在其图像组件中,确保它使用暂停精灵作为源图像(图 6-19 )。

img/491558_1_En_6_Fig19_HTML.jpg

图 6-19

PauseButton 的矩形和图像组件

其按钮组件的OnClick函数应该禁用自己的 GameObject,启用实际 PauseMenu 的 game object,并从 ScriptManager 上的 utilityScript 调用Pause函数(图 6-20 )。

img/491558_1_En_6_Fig20_HTML.jpg

图 6-20

暂停按钮的按钮组件的OnClick功能

如果您禁用了 PauseMenu 游戏对象,现在重新启用它,同时保持开始菜单禁用。如果你愿意,PauseMenu 的背景可以换成另一种颜色。接下来,将标题改为暂停,并删除退出按钮游戏对象。重命名 PlayButton ResumeButton,给它一个蓝色,文本 Resume 在里面。也可以将它向底部移动一点(图 6-21 )。

img/491558_1_En_6_Fig21_HTML.jpg

图 6-21

当前构成画布中的子对象的游戏对象

ResumeButton 的 OnClick 应该启用 PauseButton 的 GameObject,禁用 PauseMenu 的 game object,从 ScriptManager 上的 utilityScript 调用UnPause函数(图 6-22 )。

img/491558_1_En_6_Fig22_HTML.jpg

图 6-22

ResumeButton 的按钮组件的OnClick功能

最后要做的是在场景中禁用 PauseMenu 游戏对象,并启用 StartMenu 的游戏对象。我们做了相反的事情,只是为了能够预览它会是什么样子。下面是我在玩游戏时点击暂停按钮时我的暂停菜单的样子(图 6-23 ):

img/491558_1_En_6_Fig23_HTML.jpg

图 6-23

PauseMenu 看起来怎么样

游戏结束菜单

你应该已经猜到这个菜单的用途了。复制 StartMenu,将新实例命名为 GameOverMenu,并禁用 StartMenu 和 PauseMenu 的 GameObjects。更改背景游戏对象的图像组件的颜色,改为在标题文本中显示游戏结束,并删除 PlayButton。重命名 ExitButton RestartButton,更改其颜色,并将其文本命名为子显示重新启动。最后,让它在 OnClick()中调用 ScriptManager 上的 utilityScript 的Restart函数(图 6-24 )。

img/491558_1_En_6_Fig24_HTML.jpg

图 6-24

RestartButton 按钮组件的OnClick功能

创建一个新的文本 UI 元素作为 GameOverMenu 的子元素,并将其命名为 ScoreLabel。将其放置在(-200,-25,0)处,宽度和高度分别为 365 和 80。让它显示分数:,水平左对齐,垂直底部对齐。给它 65 的字体大小,标记为粗体,改变颜色(图 6-25 )。

img/491558_1_En_6_Fig25_HTML.jpg

图 6-25

ScoreLabel 的 Rect 转换和文本组件

复制文本 UI 元素,将其命名为 Value,并使其成为 ScoreLabel 子元素。更改其颜色,将其水平右对齐,使其使用正常字体样式,并更改其 Rect Transform 属性,使其位置为(465,0,0),宽度和高度分别为 250 和 80(图 6-26 )。

img/491558_1_En_6_Fig26_HTML.jpg

图 6-26

值的矩形转换和文本组件

复制 ScoreLabel GameObject,将新实例重命名为 HighScoreLabel,使其位置为(-200,-125,0),将其文本值改为 High Score:,并更改其颜色(图 6-27 )。

img/491558_1_En_6_Fig27_HTML.jpg

图 6-27

HighScoreLabel 的 Rect 转换和文本组件

对于 GameOverMenu 游戏对象的子对象,我的层级窗口如下所示(图 6-28 ):

img/491558_1_En_6_Fig28_HTML.jpg

图 6-28

GameOverMenu 的子游戏对象

这是我的 GameOverMenu 之后的样子,当健康机制实现后,玩家输了一局(图 6-29 ):

img/491558_1_En_6_Fig29_HTML.jpg

图 6-29

GameOverMenu 看起来怎么样

同样,禁用 PauseMenu 和 GameOverMenu 的游戏对象,启用 StartMenu 的游戏对象。

6.4 添加健康

我们将采取类似的方法来实现对玩家健康的评分。玩家坦克上有一个独特的脚本实例,它有一个功能,所以敌人可以呼叫来降低它的生命值。我们还将实例化一个玩家失败时的爆炸,并创建必要的东西来给游戏菜单更多的上下文。

首先,复制 ScoreText UI 元素,并将其命名为 HealthText。更改文本组件的颜色,将其文本更改为 Health:,并将其放置在(-660,450,0)。确保 HealthText GameObject 与 ScoreText 的索引在同一个索引附近,这样它就不会出现在不需要的元素的下面或上面。创建一个名为 healthManager 的新脚本,将其放在 Player GameObject 上,然后打开它。将using UnityEngine.UI;行添加到脚本中,以便我们能够修改稍后将使用的文本元素的文本值。

public static healthManager instance;
public Text healthText;

public Text scoreText;
public Text highScoreText;

int health = 5;

public GameObject explosionPrefab;

同样,我们创建的第一个变量将对应于我们脚本的一个实例,这样就可以从其他脚本中调用它。healthText稍后将引用我们 HealthText 游戏对象的文本组件。至于scoreTexthighScoreText,会对应 GameOverMenu 中对应游戏对象的文本组件。整数变量health将记录玩家坦克当前的生命值。它最初的值为 5。最后,当玩家死亡时,我们将引用一个爆炸预置来实例化。

void Awake() {
    if (instance) {
        Destroy(this.gameObject);
    } else {
        instance = this;
    }
    UpdateHealth();
}

在我们脚本的Awake函数中,我们将确保场景中只有它的一个实例。如果您不理解这段代码,请参考第 6.2 节(“评分”)。

void UpdateHealth() {
    if (health <= 0) { GameOver(); return; }
    healthText.text = "Health: " + health.ToString();
}

在名为UpdateHealth的函数中,我们将更新在healthText变量中引用的文本,以显示玩家的健康状况。如果健康小于或等于 0,我们将调用一个函数来执行游戏结束。

public void ChangeHealth(int amount) {
    health += amount;
    UpdateHealth();
}

我们也将有另一个公开的功能,这样敌人就可以造成伤害。当然,敌人在调用这个函数时会传递一个负值,比如-1。

void GameOver() {
    healthText.text = "Health: 0";
    Instantiate(explosionPrefab, transform.position, explosionPrefab.transform.rotation);
    Destroy(this.gameObject);

    scoreText.transform.parent.parent.gameObject.SetActive(true);

    int score = scoreManager.instance.GetCurrentScore();
    scoreText.text = score.ToString();

    int highScore = PlayerPrefs.GetInt("HighScore", 0);
    if (score > highScore) {
        highScore = score;
        PlayerPrefs.SetInt("HighScore", highScore);
    }
    highScoreText.text = highScore.ToString();
}

GameOver函数中,简而言之,我们将不得不销毁玩家,并在GameOverMenu变量中更新相应游戏对象的分数/高分文本。

我们要做的第一件事是设置healthText文本值来显示生命值为 0,在玩家的位置产生爆炸,并摧毁玩家坦克游戏对象。你应该熟悉这些语法。

接下来,为了显示实际的 GameOverMenu 屏幕,我们使用带有参数truegameObject.SetActive方法作为 scoreText 转换本身的父转换的父 GameObject。如果你看看你的层次结构,你会看到 GameOverMenu 是 ScoreLabel 的父,它本身是 Value 的父,也就是我们的 scoreText。我们可以使用变量直接引用 GameOverMenu GameObject,但是这种方法对你很有用。

然后,我们将创建一个名为score的局部整数变量,我们将把从 scoreManager 实例(我们将在接下来的步骤中实现)调用的函数返回的值赋给它,以获得分数。我们将在scoreText变量中引用的文本组件中显示这一点。

对于高分,我们将首先创建另一个局部变量。对于这个变量,我们将获取并存储以前的高分。我们通过使用 PlayerPrefs 来做到这一点,即使在游戏关闭时,player prefs 也能够存储数据。取整数数据的语法是PlayerPrefs.GetInt(<ID>, <defaultValue>)。在我们的例子中,ID 是 HighScore。PlayerPrefs将搜索并返回与该 ID 相关联的存储值。如果该 ID 不存在,或者没有使用该 ID 保存数据,将返回默认值 0 并存储在highScore变量中。

然后,我们将检查我们在刚刚玩的游戏中的当前分数是否大于最高分数。如果是,我们将把与 HighScore ID 对应的变量highScorePlayerPref的值都设置为score的值。最后,我们更新在highScoreText变量中引用的文本组件的值。

完整的脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class healthManager : MonoBehaviour {
   public static healthManager instance;
   public Text healthText;

   public Text scoreText;
   public Text highScoreText;

   int health = 5;

   public GameObject explosionPrefab;

   void Awake() {
       if (instance) {
           Destroy(this.gameObject);
       } else {
           instance = this;
       }
       UpdateHealth();
   }

   public void ChangeHealth(int amount) {
       health += amount;
       UpdateHealth();
   }

   void UpdateHealth() {
       if (health <= 0) { GameOver(); return; }
       healthText.text = "Health: " + health.ToString();
   }

   void GameOver() {
       healthText.text = "Health: 0";
       Instantiate(explosionPrefab, transform.position, explosionPrefab.transform.rotation);
       Destroy(this.gameObject);

       scoreText.transform.parent.parent.gameObject.SetActive(true);

       int score = scoreManager.instance.GetCurrentScore();
       scoreText.text = score.ToString();

       int highScore = PlayerPrefs.GetInt("HighScore", 0);
       if (score > highScore) {
           highScore = score;
           PlayerPrefs.SetInt("HighScore", highScore);
       }
       highScoreText.text = highScore.ToString();
   }
}

为了让脚本正确编译和运行,我们必须首先在我们的 scoreManager 脚本中创建GetCurrentScore函数。

public int GetCurrentScore() {
  return score;
}

该函数可以被公开调用,并将返回保存在变量score中的整数值;

保存这两个脚本。回到编辑器中,将相应的组件分配给 healthManager 的字段。别忘了这个脚本应该在你的玩家游戏对象上(图 6-30 )。

img/491558_1_En_6_Fig30_HTML.jpg

图 6-30

作为组件的 healthManager 脚本

健康文本将对应于您在本节开始时创建的 Health Text 游戏对象的文本组件。分数文本和高分文本将分别对应于名为 Value 的游戏对象的文本组件,它是 ScoreLabel 和 HighScoreLabel 游戏对象的子对象。作为爆炸预设,我使用的是来自简单 FX ➤预设的 FX 爆炸碎石预设。

此外,我们必须修改敌人的脚本,这样敌人就可以造成伤害。编辑OnCollisionEnter函数,验证碰撞的物体是否是玩家,如果是,调用healthManager实例上的ChangeHealth函数。

  if (col.gameObject.CompareTag(playerTag))  {
      healthManager.instance.ChangeHealth(-1);
  }

我还修改了OnCollisionEnter函数,因此代码重复少了一点,整体看起来更整洁。但是,您不需要这样做。

void OnCollisionEnter(Collision col) {
     if (col.gameObject.CompareTag(bulletTag))  {
         Destroy(col.gameObject);
         scoreManager.instance.IncreaseScore(1);
         DestroyEnemy();
     }

     if (col.gameObject.CompareTag(playerTag))  {
         healthManager.instance.ChangeHealth(-1);
         DestroyEnemy();
     }
 }

现在,如果你玩这个游戏,你会看到当敌人与你碰撞时,健康文本会更新。当你的生命值达到 0 时,将会看到一个爆炸,并显示正确的scorehighScore值。

当你输的时候你也会注意到错误(图 6-31 )。

img/491558_1_En_6_Fig31_HTML.jpg

图 6-31

cameraFollow 脚本导致的错误

这些与 cameraFollow 脚本在玩家坦克被摧毁时无法跟随它有关,因为它对应的是一个值null。编辑 cameraFollow 脚本,并添加一个检查,以防止当玩家坦克被摧毁,因此不存在时出现该错误。

void LateUpdate() {
  if (player) {
    this.transform.position = new Vector3(player.position.x, height, player.position.z);
  }
}

6.5 新的敌人

虽然目前的敌人做得很好,但让我们介绍一个需要三枪才能被摧毁的新敌人。如果和玩家碰撞也会造成更大的伤害。

从复制项目窗口中的敌人预设开始。将新实例命名为 EnemyBig,原始实例命名为 EnemySmall。双击打开 EnemyBig 预置(图 6-32 )。给它一个(3,3,3)的比例,并通过使用新的材质来改变它的颜色和它的轨迹渲染器子对象的颜色。我还将使用 0.75 的宽度使轨迹渲染器更宽。最后,我们还想把敌人游戏对象上敌人脚本的最大速度值降低到 3,并使用另一个爆炸预设。为此,我将使用简单 FX ➤预设 fx _ 烟花 _ 蓝色 _ 大,但用橙色。

img/491558_1_En_6_Fig32_HTML.jpg

图 6-32

新敌人游戏对象上的组件

现在让我们修改负责产生敌人的脚本,enemySpawner,这样它可以随机产生我们的两个敌人预置中的任何一个。我们要做的第一件事是将public GameObject enemy语句转换成一个引用带有标识符“敌人”的游戏对象数组的语句:public GameObject[] enemies

然后,在包含 instantiate 指令的代码行之前添加一行代码,创建一个名为enemy的本地 GameObject 变量,该变量将从enemies数组中随机分配一个(enemy) GameObject。

void SpawnEnemy() {
    int randomPos = (int)Random.Range(0, transform.childCount);
    GameObject enemy = enemies[(int)Random.Range(0, enemies.Length)];
    Instantiate(enemy, transform.GetChild(randomPos).position, enemy.transform.rotation);
}

回到编辑器中,将两个敌人预设拖到 SpawnPoints 游戏对象上的 enemySpawner 组件的相应槽中(图 6-33 )。

img/491558_1_En_6_Fig33_HTML.jpg

图 6-33

更新的 enemySpawner 脚本作为一个组件

你现在应该可以看到新的敌人在你玩的时候被实例化了,但是它的行为仍然和原来的一样。打开敌方脚本。是时候做些改变了。

我们将使用两个新变量:healthdamageToCause。这两个变量都是全局整型变量,可以公开访问,默认值为 1。

public int health = 1;
public int damageToCause = 1;

保存脚本,然后在项目窗口中的 EnemyBig 预置上,将这两个变量的值都设置为 3(图 6-34 )。

img/491558_1_En_6_Fig34_HTML.jpg

图 6-34

EnemyBig 游戏对象组件的最终属性

然后,修改OnCollisionEnter函数,这样当敌人与子弹相撞时,这减少其生命值一(health--),而不是调用DestroyEnemy函数。如果敌人与玩家发生碰撞,它应该会造成相当于保存在damageToCause变量中的值的伤害,所以只需将传递给 healthManager 脚本实例的ChangeHealth函数的-1值替换为-damageToCause。最后加一个 check,这样敌人的生命值小于等于 0 就调用DestroyEnemy函数。

void OnCollisionEnter(Collision col) {
     if (col.gameObject.CompareTag(bulletTag))  {
         Destroy(col.gameObject);
         scoreManager.instance.IncreaseScore(1);
         health--;
     }

     if (col.gameObject.CompareTag(playerTag))  {
   healthManager.instance.ChangeHealth(-damageToCause);
         DestroyEnemy();
     }

     if (health <= 0) {
         DestroyEnemy();
     }
 }

保存脚本,并前往播放模式。你应该看到敌人的游戏对象需要三发子弹才能被摧毁,如果他们与玩家坦克相撞,会造成 3 点伤害。

为了完成这一部分,让我们在敌人的脚本中添加更多的东西,这样敌人会随着时间的推移变得更快,让游戏不那么无聊(不是说它是,但无论如何)。

敌方脚本的Start函数的第一行,除了速度变量,还要加上(Time.time / 25)的值。我们调用的第一个方法将返回秒数,因为游戏开始了,简而言之,我们要确保被繁殖的敌人的速度每 25 秒增加 1。

void Start() {
 speed = Random.Range(minSpeed, maxSpeed) + (Time.time / 25);
 audioSource = GetComponent<AudioSource>();
 player = GameObject.FindWithTag(playerTag);
}

6.6 健康盒子

这是我们将对游戏进行修改的最后一部分。我将会介绍一个小立方体,它将会沿着地面游戏对象的区域以确定的间隔随机产生。如果玩家坦克撞上了这些小立方体中的一个,我们将称之为生命盒,我们将增加他们的生命值。

6.6.1 制作健康盒子

在你的场景中,创建一个 3D 对象➤立方体游戏对象。将其命名为 healthBox,并赋予其位置为(0,1,3),旋转为(0,0,0),缩放为(0.4,0.4,0.4)。您也可以为其指定绿色材质。然后,检查其箱式碰撞器组件的isTrigger属性(图 6-35 )。

img/491558_1_En_6_Fig35_HTML.jpg

图 6-35

健康盒游戏对象上的组件

由于我们让健康盒游戏对象有一个 isTrigger 碰撞器,敌人可以直接通过它,而不需要我们弄乱标签和层属性。我们的玩家坦克也可以捡起它,继续它的路线,不受我们想要它去的方向的影响,因为没有碰撞发生。

添加健康

现在,如果你玩,玩家坦克只是通过健康盒游戏对象,真的没有更多的事情发生。为了使坦克具有交互性,并赋予它在游戏中的意义,我们必须编写一个脚本来定义应该发生什么。创建一个脚本,命名为 healthBox,并打开它。

我们将需要一个函数在发生 isTrigger 碰撞时运行,如果这是玩家坦克的碰撞器导致的,我们必须调用该函数来增加玩家的生命值并摧毁生命盒的游戏对象。如果我们不执行这最后一步,玩家可以不断地进出同一个生命盒来增加他们的寿命。我们还会增加玩家的生命值 2,但是你可以选择另一个值。

完整的脚本如下。保存它,并将其放置在场景中的健康盒游戏对象上。您还会注意到,我们使用的OnTriggerEnter函数带有一个作为参数传递的碰撞器类型引用。该函数的执行方式与我们一直使用的著名的OnCollisionEnter函数类似,但这次是为了处理触发冲突。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class healthBox : MonoBehaviour {
   void OnTriggerEnter(Collider col) {
       if (col.gameObject.CompareTag("Player")) {
           healthManager.instance.ChangeHealth(2);
           Destroy(this.gameObject);
       }
   }
}

当你现在进入游戏模式时,你应该会注意到 healthBox GameObject 被破坏,当你与它“碰撞”时,你的生命值增加 2(或你在上一步中选择的值)。

6.6.3 沿地图生成生命盒

在一个预设中转动健康盒游戏对象,并将其从你的场景中删除(图 6-36 )。

img/491558_1_En_6_Fig36_HTML.jpg

图 6-36

生命盒游戏对象在预置中被打开后

创建另一个脚本,将其命名为 healthBoxSpawner,并打开它。我们将使用三个变量,分别用于引用健康盒预设的游戏对象,我们场景中地面游戏对象的转换,并保存一个值,该值将定义以何种间隔生成健康盒游戏对象,与 enemySpawner 脚本类似。

public GameObject healthBox;
public Transform ground;
public float delay = 3.0f;

就像 enemySpawner 脚本中的Start函数一样,我们必须每隔 x 秒调用一次函数,正如 delay 变量中定义的那样,以实例化一个 healthBox 游戏对象。

void Start() {
    InvokeRepeating("SpawnHealthBox", 0.0f, delay);
}

由于我们的地面游戏对象沿其 x 和 z 轴的比例为 150,原点(0,0,0)位于其中心,因此任何位于其边缘的游戏对象的 x 和/或 z 位置都必须为 75 或-75。因此,我们将在 75 到-75 之间为 x 和 y 值选择一个随机值,我们希望在SpawnHealthBox函数中实例化一个健康盒。

例如,要获得沿 x 轴位置的随机值,我们将使用以下代码:

float xPos = Random.Range(-1.0f, 1.0f) * (ground.localScale.x / 2);

游戏对象的比例总是局部表示的。除以 2 将得到 75,我们只需将其乘以-1 到 1 之间的一个随机值。

我们将做同样的事情来获得 z 轴的值。y 轴值将为 1。然后,只需要创建一个Vector3变量,并在那个位置实例化一个 healthBox。Quaternion.identity可以解释为沿所有轴 0°旋转。

void SpawnHealthBox() {
    float xPos = Random.Range(-1.0f, 1.0f) * (ground.localScale.x / 2);
    float zPos = Random.Range(-1.0f, 1.0f) * (ground.localScale.z / 2);

    Vector3 spawnPos = new Vector3(xPos, 1, zPos);

    Instantiate(healthBox, spawnPos, Quaternion.identity);
}

这是完整的脚本,以防你漏掉了什么。保存它并将其拖动到场景中的 SpawnPoints 游戏对象上,因为我们希望它只在我们玩游戏时运行(而不是在玩家单击“玩”、暂停游戏或失败之前)。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class healthBoxSpawner : MonoBehaviour {

   public GameObject healthBox;
   public Transform ground;
   public float delay = 3.0f;

   void Start() {
       InvokeRepeating("SpawnHealthBox", 0.0f, delay);
   }

   void SpawnHealthBox() {
       float xPos = Random.Range(-1.0f, 1.0f) * (ground.localScale.x / 2);
       float zPos = Random.Range(-1.0f, 1.0f) * (ground.localScale.z / 2);

       Vector3 spawnPos = new Vector3(xPos, 1, zPos);

       Instantiate(healthBox, spawnPos, Quaternion.identity);
   }
}

将生命盒预置拖放到生命盒区域,将地面游戏对象拖放到地面区域。您可以为延迟设置另一个值,而不是 3(图 6-37 )。

img/491558_1_En_6_Fig37_HTML.jpg

图 6-37

作为组件的 healthBoxSpawner 脚本

6.7 将游戏导出为。apk 文件

我们现在有了一个全功能的游戏!让我们做一个。apk 文件,这样我们就可以把它安装在我们的 Android 手机上,并向我们的朋友炫耀!

如果你按照这本书,从我们从 Hub 下载一个版本的 Unity 编辑器并安装 Android 模块开始,你的编辑➤首选项➤外部工具标签应该是这样的(图 6-38 ):

img/491558_1_En_6_Fig38_HTML.jpg

图 6-38

用于构建 Android 平台的工具

保存场景和项目。进入构建设置窗口(文件➤构建设置,或 Ctrl+Shift+B)。要做的第一件事是添加您希望出现在中的所有场景。apk(图 6-39 )。我们只有一个场景,所以单击添加开放场景。如果我们有多个场景,我们会把它们都拖进来,第一个场景是我们希望玩家在游戏打开时看到的场景(图 6-39 )。

img/491558_1_En_6_Fig39_HTML.jpg

图 6-39

“构件设置”窗口

点按该窗口左下角的“播放器设置”按钮。在点击 Build 之前,我们将首先修改一些设置。项目设置窗口将出现,你将在播放器标签。玩家是我们为 Unity 构建的最终游戏定制各种选项的地方。我们将只关注构建 Android 游戏最常用的选项。我们可以做的第一件事是设置一个公司名(也许是你作为开发者的名字),一个产品名(我们游戏的名字),并设置一个版本号(比如 1,2,3.5 等。).这些可以设置成你想要的任何值。

您已经知道如何从我们为暂停按钮导入纹理的步骤中导入纹理(第 6.3.3 节,“暂停菜单”)。然后我们可以给游戏分配一个图标和一个光标。默认情况下,如果留空,图标将是 Unity 的标志,光标将是空白的(图 6-40 )。

img/491558_1_En_6_Fig40_HTML.jpg

图 6-40

播放器选项卡中的第一个选项

我不会讨论图标下的设置,但是,简而言之,你可以指定不同分辨率的纹理来匹配不同手机上的最终图标大小。它会相应地缩放我们在默认图标属性中添加的图像,如果没有调整的话,所以这不是你必须做的事情。

在分辨率和显示(图 6-41 )下,您可以个性化您希望游戏在手机上显示的方式。这些选项非常简单易懂。我们不会改变这一部分的任何东西,除了我们希望我们的游戏只能在风景模式下玩。我们可以将默认分辨率设置为自动旋转,但取消下面的纵向模式,或者只选择另一个选项而不是自动旋转。通过将鼠标光标停留在一个设置上几秒钟,您也可以了解许多设置的作用。

img/491558_1_En_6_Fig41_HTML.jpg

图 6-41

播放器设置中的分辨率和演示

在 Splash Image 标签页(图 6-42 )下,你可以选择在用户进入你的游戏时显示一些东西,比如你的游戏开发商公司的封面图片等等。您不能将其设置为在个人版中不显示由 Unity 制造的徽标。您可以设置闪屏样式或动画,甚至设置背景(而不是纯色),以定制您的闪屏。

img/491558_1_En_6_Fig42_HTML.jpg

图 6-42

播放器设置中的启动画面

如果您想要显示其他图像,请将它们添加到徽标列表中,并注明您希望它们在屏幕上显示的时间。

在其他设置下,我们首先有一些关于渲染和图形的设置,我们真的不用乱来(图 6-43 )。我们也有我们游戏的包名,这将是我们手机上游戏的完整标识符。没有游戏或应用应该有相同的包名。虽然版本是用来识别你的游戏版本的,但你对你的游戏所做的每一次更新都应该有一个比你的 Android 手机上一次更新更高的捆绑版本号,以避免出现错误并安装它。最低和目标 API 级别代表了游戏可以安装的 Android 手机版本的范围。如果您尝试安装。Android 版本不在此范围内的 Android 手机上的 apk 将会失败。

img/491558_1_En_6_Fig43_HTML.jpg

图 6-43

其他环境中的识别和配置

至于配置部分,您必须切换到 IL2CPP 脚本后端,以具有 ARM64 架构的目标设备(在目标架构下;请参见图 6-43 )如果您希望您的游戏稍后被接受(如果您提交)到 Google 的 Play Store。我们的游戏将会有很好的表现;没有必要调整任何其他选项。

最后,我们可以制作一个 Keystore 并“签名”我们的游戏,这样它就有了制作它的人的身份。手机和 Play Store 也将拒绝更新游戏/应用,如果使用的密钥不是提交的第一个版本所用的密钥。

单击 Keystore Manager 按钮,创建一个新的 keystone,填写详细信息,然后单击 Add Key 按钮(图 6-44 )。

img/491558_1_En_6_Fig44_HTML.jpg

图 6-44

密钥库管理器窗口

然后,在播放器的发布设置部分输入相同的详细信息(图 6-45 )。

img/491558_1_En_6_Fig45_HTML.jpg

图 6-45

在播放器中发布设置

如果以后你做的游戏建成后有。占用超过 100MB 的 apk 文件,您必须勾选 Split Application Binary,这将创建一个额外的扩展文件(OBB 文件)并减少文件的大小。apk 文件,以便 Google Play 接受它。

最后,在 Build Settings 窗口中单击 Build,并指定。将要生成的 apk 文件(图 6-46 )。然后,就等着吧。如果构建失败,检查控制台窗口中的错误,然后简单地搜索它们。

img/491558_1_En_6_Fig46_HTML.jpg

图 6-46

单击“构建”时运行的进程之一

Windows 上的一个常见错误与 Android SDK 模块的许可证没有被接受有关。要解决这个问题,您必须转到下载/安装 Android SDK 的位置,并在 CMD 窗口中执行以下命令。用您自己的版本替换 Unity Editor 版本。

cd "Program Files\Unity\Hub\Editor\2019.3.0b12\Editor\Data\PlaybackEngines\AndroidPlayer\SDK\tools\bin"

sdkmanager.bat --licenses

如果您得到任何其他错误,只需搜索并尝试找到解决方案。如果你仍然不能让你的游戏正常运行,下载并手动安装 Android SDK、NDK、JDK 和 Gradle,然后在编辑➤首选项窗口中将 Unity 指向它们的位置。

Note

下面是我的外部工具选项卡在 Linux 上的样子(图 6-47 )。我无法让它与我在 Hub 中随 Unity 下载的内置 Android 工具一起工作,所以我手动解压/安装了所有这些工具,现在我的游戏可以正常运行了。如果你进行到这一步,你可以跟着不太过时的论坛帖子,很容易找到文章。请注意,对于一些工具,如 JDK 和 NDK,Unity 支持非常具体的版本。

img/491558_1_En_6_Fig47_HTML.jpg

图 6-47

我的外部工具窗口

一旦你有了。apk 文件,发到你手机上。在手机上的文件管理器应用中,只需浏览到。apk 文件并安装它(图 6-48 )。你现在可以玩你做的游戏,并展示给你的朋友看!

img/491558_1_En_6_Fig48_HTML.jpg

图 6-48

决赛。apk 文件

如果你想建立你的技能,做出一个更好的游戏,在 Sphere Shooter 中还有很多事情可以做。这里有一个简短的列表:

  • 添加更多的音效和粒子系统。

  • 实现一个功能,允许玩家从地面游戏物体的纹理中选择一对外观。

  • 增加更多的敌人:一个更小更快的,一个在被杀死时产生更小的敌人,一个可以粘住玩家并降低他们的最大速度的。

  • 向游戏中添加硬币,允许玩家为他们的坦克购买更多的炮塔。

  • 玩家存活的时间越长,增加杀死的价值。

  • 添加更多带有激光、射弹和火焰喷射器的坦克。

  • 在游戏中实现类似挑战的东西,完成后有奖励。

你也可以参考一个叫球和炮塔的游戏,它的核心是球体射手。在 Google Play 上找到。

我希望你像我写这本书一样喜欢它!我们看了几个游戏开发的概念,以及如何使用 Unity 提供的许多功能。我们甚至制作了一个演示游戏,可以对其进行大量改进和构建。如果你在读完这本书后考虑从事游戏开发,我会很高兴。请不要犹豫,了解更多的主题,并建立伟大和有趣的游戏。与我分享。我很想看看你的作品。

posted @   绝不原创的飞龙  阅读(83)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示