
玩家角色:第 2 部分
Tutorial
Beginner
+0XP
60 mins
(793)
Unity Technologies

现在您已经花了很多时间开发游戏的玩家角色,接下来便可以创建一个自定义脚本来控制其动作了。
在本教程中,您将:
- 在 Unity 中创建新脚本资源
- 探索生成的默认脚本
- 创建一个可用于更改角色位置的矢量
- 对角色进行垂直和水平移动和旋转
- 检查您的更改是否对 Unity 的物理系统有效
完成后,John Lemon 将能够在鬼屋内爬行!
Languages available:
1. 玩家角色(续)
在上一教程中,您开始处理 JohnLemon 预制件,添加了一个对其进行动画处理的系统以及一些使角色与物理系统一起工作的组件。现在,您将创建一个自定义组件:您的第一个脚本!
脚本是什么?
脚本是一个包含一组计算机指令的文本文档。这些指令通常称为代码。指令以计算机可以理解的方式编写而成,在本教程中,我们使用称为 C# (C Sharp) 的编程语言。
C# 定义了指令的编写方式和一些使用的关键字。幸运的是,在 C# 中使用的关键字通常具有与英语中相似的含义。例如,我们在 C# 中遇到的第一个关键字是“using”,这意味着编写的脚本使用来自其他地方的代码。另一个示例是“public”,表示任何方法都可以进行某种访问。示例太多,这里无法一一介绍,但您将在这些教程中遇到各个示例时进行相应的探索。
您将为此项目编写的所有脚本均采用 MonoBehaviour 的形式。MonoBehaviour 是特殊类型的脚本,可以像组件一样附加到游戏对象。这是因为它们是您可以自己编写的特定组件实例。
脚本与预制件有一些细微的相似之处:
- 就像预制件一样,脚本被创建为资源。
- 将脚本作为组件添加到游戏对象实际上是实例化该脚本,就像将预制件添加到场景就是实例化该预制件一样。
但是,脚本在很多方面都非常不同。让我们直接创建脚本并了解更多信息!
2. 创建您的第一个脚本 (PlayerMovement)
首先,创建新脚本作为资源:
1.在 Project 窗口中找到 Assets > Scripts 文件夹。右键单击该文件夹,然后选择 Create > C# Script。将该脚本命名为“PlayerMovement”。
注意:要用作组件的脚本在资源上的名称必须与脚本本身中的类名称相同。Unity 创建脚本文件时,会为其提供一个与该资源最初命名的名称相匹配的类名称。但是,重命名资源时,类名称不会更改。
2.选择您的脚本,然后查看 Inspector 窗口。应该会看到以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
// 在第一次帧更新之前调用 Start
void Start()
{
}
// 每帧调用一次 Update
void Update()
{
}
}
找到以“public class PlayerMovement”开头的代码行。这就是定义类名称的语句。如果脚本未显示 PlayerMovement,请删除脚本资源,然后创建一个名为 PlayerMovement 的新脚本。
3.现在,您已经创建了脚本资源,请将其打开以进行编辑。您可以双击该资源,也可以单击 Inspector 窗口中的“Open...”按钮。
脚本编辑不是在 Unity 内部进行的,而是在另一个名为 Visual Studio 的程序中打开脚本进行编辑。打开脚本后,即可对其进行编辑。
探索默认脚本
让我们分解一下当前可以在 Visual Studio 中看到的默认脚本:
1.代码的前三行是:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
这些称为 using 指令。借助这些指令可以使用此脚本文件中其他地方实现的代码。例如,如果此处没有“using UnityEngine;”,则下一行中的“MonoBehaviour”一词将不可用。在大多数情况下,Unity 默认包含的 using 指令就足够了,因此您不必担心这些问题。
2.下一行是:
public class PlayerMovement : MonoBehaviour
这是类声明的开始。类就像称为“对象”的实例的计划。当实例化一个类时(例如,通过将脚本作为组件附加到游戏对象),该实例称为对象。对象是代码的基石,存在于您已经完成的全部工作中!
您已经遇到过的类包括 Animator、Rigidbody、GameObject 和 Transform。这些类已经存在于您的游戏中。从另一方面,可以将类视为工厂建筑:工厂接受输入,对其进行一些操作,然后再产生输出。我们稍后将与其他代码元素一起回到这个工厂比喻。
3.下一行只有一个左花括号/大括号:{
在本系列教程中,我们称之为花括号。花括号用于定义代码块,是 C# 语言的基本组成部分。代码块是存在于左花括号和右花括号之间的代码行。花括号必须始终成对存在。
对于类声明代码块,右花括号在底部,但类声明中还有两个其他代码块。类声明中包含的两个代码块存在缩进。缩进在技术上不是必需的,但是对于定义代码块的开始和结束位置很有用。
4.下一行是:
// 在第一次帧更新之前调用 Start任何以这种双正斜杠开头的文本都称为注释。注释是您希望计算机完全忽略的任何内容。在大多数情况下,注释充当一种标签,用于解释有关其附近代码的某些信息。本示例中的注释对下面声明的内容进行了非常简短的解释。
5.在注释之后是第一个方法声明的开始:
void Start()让我们回到工厂的比喻。如果类是工厂,则方法就是该工厂内的机器。方法可以接收数据,执行操作,然后给出(或返回)数据。
所有方法声明都具有相同的格式:
- 首先,它们声明返回类型。这是方法完成后将要给出的数据类型。在此示例中,返回类型为 void。这是一个特殊的 C# 关键字,表示“无”,即该方法实际上不返回任何值。
- 返回类型之后是方法的名称,在本例中为 Start。
- 名称之后有一对左括号和右括号:()。在本系列教程中,我们称之为圆括号。在这些圆括号内,方法可以声明要接收的数据类型。这些数据称为参数。由于圆括号中没有任何内容,因此表示没有声明任何参数。
这三段信息(返回类型、名称和参数)构成方法的签名。在大多数情况下,方法可以具有您喜欢的任何签名。但是,MonoBehaviour 类可以使用一些需要特定签名的特殊方法。这些特殊方法不需要从代码中被调用,而是由 Unity 在特定时间调用。Start 便是特殊方法的一个示例。只要该方法所在的游戏对象启动(通常是场景启动后立即启动),就会立即调用该方法。因此,该方法非常适合执行您不希望重复的设置等。
6.Start 方法签名之后是一个代码块。此处定义了每次调用该方法时执行的所有代码。调用某个方法是指您如何使用该方法,稍后您将自己进行此操作。让我们回到工厂的比喻:方法声明相当于工厂中机器的运作方式,而调用该方法相当于实际使用该机器。
方法签名声明与其后的代码块一起被称为方法定义。在谈论 C# 时,“方法声明”和“方法定义”这两个术语通常互换使用,因为它们同时发生。它们的区别仅在其他语言(例如 C++)中才真正重要。
7.现在适合讨论在类中编写代码的顺序。与工厂和机器的比喻类似,机器在工厂中的位置并不重要,但是机器执行操作的顺序非常重要。
在 C# 中,在类中声明方法的顺序无关紧要,但是方法执行操作的顺序很重要。
8.脚本的最后两部分是另一条注释,然后是另一个方法定义。
Update 方法是 MonoBehaviour 的另一个特殊方法。在每一帧中将内容渲染到屏幕之前都会调用该方法。
现在,您对默认脚本的某些部分有了基本的了解,接下来让我们自定义该脚本以使其适合您的游戏。
3. 为水平轴和垂直轴创建变量
您的 PlayerMovement 脚本需要接受用户输入并将其转换为角色移动。
您要做的第一件事就是从 Unity 的输入系统中获取一些数据。该脚本总是需要检查输入的内容,由于在每一帧中都要调用 Update,因此在该处检查输入非常合理。但是,具体需要检查什么?
使用箭头键或 WASD 来移动字符很合理,因此脚本需要检查键盘上这些特定键的值。该脚本可以检查每个键是否被按住,并确定角色应该做什么(或不应该做什么),但是有一种替代方法可以使这个过程变得简单一些。
Unity 有一个输入管理器用来定义可按名称找到的各种按钮和轴。例如,其中有一个称为 Horizontal 的轴,由 A 和 D 键以及向左和向右键表示。因此,通过该检查,玩家的计算机可以决定角色应该向左还是向右移动。
编写代码以创建变量
让我们开始吧!在 Update 方法花括号内添加以下行:
float horizontal = Input.GetAxis ("Horizontal");
现在应如下所示:
// 每帧调用一次 Update
void Update()
{
float horizontal = Input.GetAxis ("Horizontal");
}
这行新代码有什么作用?
简而言之,这行新代码告诉计算机:“创建一个新的 float 变量并将其称为 horizontal;将该变量设置为等于此方法调用的结果。”
如果类是工厂,而方法是这些工厂中的机器,则变量是这些工厂中包含东西的盒子。换句话说,变量是一种存储数据的方式。需要存储的数据是水平输入轴的值。在 Unity 中,输入轴返回的数字介于 -1 和 1 之间,这种数据类型称为 float。float 表示带小数位的数字。
这行代码中有一些重要的语法(结构):
- 在 C# 中,等号表示将右边的任何内容(方法的结果)分配给左边的变量(新创建的 float 变量)。
- Input 和 GetAxis 之间的句点允许计算机访问上一个对象内的某些内容(GetAxis 是 Input 中的一个方法,因此为了从 Input 访问到 GetAxis,此处使用一个句点)。
- C# 代码由语句组成。每条语句可以包含一条或多条提供给计算机的指令,可以视为句子。分号标志着语句的结尾,因此其功能就像句末的句号一样。
4. 您是如何创建该变量的?
让我们进一步详细探讨您的代码实现的功能:
您具有 Input 类,并且正在进一步深入查找称为 GetAxis 的方法。然后,通过在名称后加上圆括号来调用该方法。但是,与 Start 和 Update 方法不同,GetAxis 有一个参数 — 这是执行任务所需的一段数据。具体来说,GetAxis 正在尝试查找轴的值,因此需要轴的名称。此处要查找水平 (Horizontal) 轴的值,因此给出该参数。
这段信息的数据类型称为字符串 (string)。这是指一串字符,例如单词或句子。通过在 Horizontal 一词两边加上引号,可以指示计算机应将其视为字符串。
一旦计算机检索到轴的值,便需要将其存储在某个位置。在方法调用的左侧,您引入了一个称为 horizontal 的 float 新变量,并将其设置为等于从 GetAxis 中找到的值。
接下来,添加另一行代码以找到名为 Vertical 的轴的值,并将其存储在名为 vertical 的变量中。
Update 方法应如下所示:
// 每帧调用一次 Update
void Update()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
}
现在,您有了水平轴和垂直轴的值。下一步是将它们组合为矢量,以便可以将其用于更改位置。
5. 创建矢量
在 Unity 中,3D 空间由三个坐标(共同形成一个矢量)表示。在 Unity 中表示矢量的数据类型称为 Vector3。游戏对象的位置便是由这种数据类型所表示,因此您需要创建一个 Vector3 来表示该变化。由用户输入所表示的移动对于该类至关重要,您可能还需要将其用于其他用途。
鉴于此,必须考虑您需要创建的变量的作用域。
变量的作用域是可以使用该变量的代码区域。通常,这和声明该变量的代码块一样简单。例如,您刚刚声明的两个 float 变量(horizontal 和 vertical)都在整个 Update 方法的作用域内,因为这是声明它们的代码块。这些变量便是 Update 方法的所谓本地变量。
如果要在多个不同的方法中使用变量,则可以在方法的作用域之外创建变量。这些变量则是类的本地变量。
在方法定义上方,添加以下行:
Vector3 m_Movement;
您的脚本现在应如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
Vector3 m_Movement;
// 在第一次帧更新之前调用 Start
void Start()
{
}
// 每帧调用一次 Update
void Update()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
}
}
新的代码行指示计算机创建一个名为 m_Movement 的 Vector3 变量,您可以在 PlayerMovement 类中的任意位置使用这个变量。
命名约定
但是名称开头的 m_ 是什么意思?这是命名约定的一部分。命名约定用于标识特定对象或对象类。在此项目中,您将使用 Unity 的内部命名约定。所有变量均以小写字母开头,但随后的单词以大写字母开头(这称为 camelCase)。
例外情况是非公共成员变量,这种变量以 m_ 前缀开头,所有单词均以大写字母开头(这称为 PascalCase)。成员变量是属于类而不是属于特定方法的变量。非公共成员变量的 m_ 部分起源于它们是“成员 (member)”变量。
6. 设置变量的值
现在您已经有了一个变量来存储角色的移动,接下来需要设置它的值。由于这可能会更改每一帧,因此需要在每一帧进行该设置 — 您应该在 Update 方法中进行该设置。
在 Update 方法中,创建 horizontal 和 vertical 变量后,添加以下行:
m_Movement.Set(horizontal, 0f, vertical);
3D 空间中的矢量具有三个值 — 此 Set 方法为每一个分配一个值。该方法有三个参数,矢量的每个坐标对应一个参数。现在,移动矢量的值在 x 轴上为水平输入,在 y 轴上为 0,在 z 轴上为垂直输入。第二个参数的 0 后面还有一个 f,用于指示计算机将该数字视为浮点数。
现在您需要解决一个小问题。移动矢量由两个数字组成,这两个数字的最大值可以为 1。如果它们两者的值都为 1,则矢量的长度(称为其大小)将大于 1。这便是勾股定理描述的三角形的边之间的关系。

这意味着您的角色沿对角线移动的速度将比沿单个轴的移动速度更快。为了确保不会发生这种情况,您需要确保移动矢量始终具有相同的大小。为此,可对其进行标准化。对矢量进行标准化意味着保持矢量的方向相同,但是将其大小更改为 1。
在您先前写的代码行下面添加以下脚本,以便在矢量本身上调用方法:
m_Movement.Normalize ();
您的完整脚本现在应如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
Vector3 m_Movement;
// 在第一次帧更新之前调用 Start
void Start()
{
}
// 每帧调用一次 Update
void Update()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize ();
}
}
此示例包括获取输入与设置移动矢量之间的间隙。这并不重要,只是为了让脚本看起来更整洁。
7. 设置 Animator 组件
现在您已经创建移动矢量,接下来还需要在每帧完成其他一些工作。您需要指示计算机执行以下操作:
- 告诉 Animator 组件该角色是否在行走
- 从玩家输入中获取角色的旋转(类似于获取其移动的方式)
- 将移动和旋转应用于角色
让我们从 Animator 组件开始。
识别是否有玩家输入
如果有任何玩家输入,则角色应该为行走状态,如果没有,则应该处于空闲状态。
1.首先,您需要编写一行代码来确定是否有水平输入。在用于标准化移动矢量的代码行之后添加以下代码:
bool hasHorizontalInput = !Mathf.Approximately (horizontal, 0f);在这里,您将创建一个布尔变量(可以为 true 或 false 的变量),并将其命名为 hasHorizontalInput。然后,将该变量设置为等于一个方法的返回值。此方法名为 Approximately,来自 Mathf 类。这个方法接受两个 float 参数,并返回布尔值;如果两个 float 数值大致相等,则返回 true,否则返回 false。因此,在本示例中,如果 horizontal 变量近似为零,则该方法将返回 true。
但是等一下!此行中有另一个您以前从未遇到过的字符:方法调用前面的感叹号。这是逻辑否定运算符,用于将布尔值反转:将 true 设置为 false,而将 false 设置为 true。这意味着,当 horizontal 不近似等于 0 时,hasHorizontalInput 将设置为 true。换句话说,当 horizontal 为非零值时,hasHorizontalInput 为 true。
2.不过,需要关心的不只是水平轴。为垂直轴添加一行类似代码:
bool hasVerticalInput = !Mathf.Approximately (vertical, 0f);
这行代码的作用完全相同,只是用于垂直轴。
3.现在,您获取了轴上的输入,接下来需要将它们组合成一个布尔值。添加以下代码行:
bool isWalking = hasHorizontalInput || hasVerticalInput;
这行代码将创建另一个名为 isWalking 的新布尔变量,该变量设置为 hasHorizontalInput 或 hasVerticalInput。两条竖线是逻辑 OR 运算符。这个运算符将比较两侧的布尔值。如果其中一个或两个都为 true,则结果等于 true,否则为 false。这意味着,如果 hasHorizontalInput 或 hasVerticalInput 为 true,则 isWalking 为 true,否则为 false。
8. 创建一个变量来存储对 Animator 组件的引用
接下来,您需要使用刚创建的布尔变量告诉 Animator 组件该角色是否应该处于行走状态。为此,需要访问 Animator 组件。
但是,请稍等一下,为什么需要做一些特殊的事情来访问 Animator 组件,但实际上您不需要这样做来调用 Input 或 Mathf 中的方法?这是由于 Input 和 Mathf 中的方法是静态的。
静态方法是在类的类型上调用而不是在该类的实例上调用的方法。因为输入更大程度上是全局概念,所以不需要为了确定轴值而实现 Input 类的单个实例。鉴于这样的原因,用来获得这些值的方法已被设为静态。同样,Mathf 类中有很多 helper 方法(用于帮助另一个方法执行其任务的方法)不涉及 Mathf 特定实例的任何特定数据,因此这些方法也已设为静态。
但是,请考虑一下 m_Movement 变量。您需要设置该特定 Vector3 实例特有的值,因此这些方法不是静态的。要记住的重要一点是,需要使用类型名称调用静态方法,而使用实例名称调用非静态方法(或“实例”方法)。
9. 获取对 Animator 组件的引用
在访问 Animator 组件之前,需要对该组件的引用,而这是使用名为 GetComponent 的方法获得的。该引用将在整个类中使用,而不仅仅是在单个方法中使用。将该引用保留为成员变量(如移动矢量)是合理的,因此其作用域为类的本地。
在类的顶部,即移动矢量声明的上方但类声明的下方,添加以下行:
Animator m_Animator;
脚本现在应如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
Animator m_Animator;
Vector3 m_Movement;
void Start ()
{
}
void Update ()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize ();
bool hasHorizontalInput = !Mathf.Approximately (horizontal, 0f);
bool hasVerticalInput = !Mathf.Approximately (vertical, 0f);
bool isWalking = hasHorizontalInput || hasVerticalInput;
}
}
注意:在此示例中,Start 和 Update 方法上方的注释已被删除。如果愿意,您也可以这样做,但是不必这样做,因为注释不会对代码产生任何影响。
10. 设置对 Animator 组件的引用
您已经创建了一个变量来存储对 Animator 组件的引用,但是到目前为止尚未将此变量设置为任何值,而是为空。在 C# 中,当变量像这样为空时,表示其值为 null。只要可以进行引用而没有引用时,则引用为 null。
您不想在此处使用 null 引用,因此现在您需要正确设置引用。您还需要能够以任何方法访问 Animator 组件,因此尽快设置引用很重要。
最早在 MonoBehaviour 上调用的方法之一便是在本教程前面提到的 Start 方法,因此在该处设置引用非常合适。将以下行添加到 Start 方法中:
m_Animator = GetComponent<Animator> ();
该方法现在应如下所示:
void Start ()
{
m_Animator = GetComponent<Animator>();
}
这行代码使用了一些熟悉的语法和一些新语法:
- 您要分配给的变量在左侧。
- 方法的名称在右侧(但该方法在前面没有写过任何内容)。
- 在您以前遇到过的圆括号之前,Animator 两边有一对尖括号。
- 该行以分号结尾。
此引用是什么意思?
让我们来看看您的代码:
首先,为什么在 GetComponent 之前没有类?先前添加该方法时,您正在访问其他对象上的方法(例如,移动矢量上的 Normalize 方法)。但是,GetComponent 是您已经可以访问的方法:这个方法是 MonoBehaviour 的一部分,因此,由于您的类是 MonoBehaviour,所以您也可以访问这个方法了。
接下来是尖括号。添加尖括号的原因是 GetComponent 方法为泛型。泛型方法具有两组不同的参数:普通参数和类型参数。尖括号之间列出的参数是类型参数。
在本示例中,GetComponent 需要知道您要查找的组件类型。您正在查找 Animator 组件,因此类型参数为 Animator。这行代码的含义是:“获取对‘Animator’类型组件的引用,并将其分配给名为 m_Animator 的变量”。
设置 isWalking Animator 参数
现在,您已经有了对 Animator 组件的引用,接下来便可以将其用于设置在上一教程中创建的 IsWalking Animator 参数。
在 Update 方法中创建 isWalking 变量的下方添加以下新代码行:
m_Animator.SetBool ("IsWalking", isWalking);该代码使用刚刚设置的 Animator 组件引用来调用 SetBool 方法。第一个参数是需要设置值的 Animator 参数的名称,第二个参数是需要设置为的值。必须确保第一个参数的拼写和大小写完全正确,否则该方法将不知道要设置哪个 Animator 参数的值。
脚本现在应如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
Animator m_Animator;
Vector3 m_Movement;
void Start ()
{
m_Animator = GetComponent<Animator>();
}
void Update ()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize ();
bool hasHorizontalInput = !Mathf.Approximately (horizontal, 0f);
bool hasVerticalInput = !Mathf.Approximately (vertical, 0f);
bool isWalking = hasHorizontalInput || hasVerticalInput;
m_Animator.SetBool("IsWalking", isWalking);
}
}
就是这样!您已经设置了 Animator 参数的值。
11. 为角色创建旋转
让我们看看下一项需要在每帧完成的工作:为角色创建旋转。
在此游戏中,角色应当只能向前走,因此其面向的方向需要与移动的方向相同。但是,如果 John Lemon 迅速转向所需的方向,看起来会很奇怪,因此您需要将其放慢一点。但是,应该放慢多少呢?
为了解决这个问题,稍微简单一点的方法是考虑角色应该旋转多快。
若要设置该速度,您需要创建一个新变量。
创建 turnSpeed 变量
将以下行添加到类顶部的 Animator 成员变量上方:
public float turnSpeed;
让我们来看看这行代码。
您已在变量声明之前添加了 public 关键字。在 Unity 中,公共成员变量出现在 Inspector 窗口中,因此可以进行调整。
您还使用了 camelCase(而不是带有 m_ 前缀的 PascalCase)。这是因为该变量为公共变量,而 Unity 命名约定会对公共成员变量使用这种格式。命名约定非常有用,但这种格式并没有什么技术原因。
计算角色的前向矢量
请记住:您需要让角色面对的方向与其移动的方向相同。所有的 Transform 组件都有一个前向矢量,因此一个很好的中间步骤是计算您希望角色具有的前向矢量。
在 Update 方法的底部添加以下行:
Vector3 desiredForward = Vector3.RotateTowards (transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);这是一行很长且复杂的代码,但其中包含许多熟悉的内容。让我们对其进行分解说明:
- 此代码创建一个名为 desiredForward 的 Vector3 变量。
- 该变量设置为名为 RotateTowards 的方法的返回值,这个方法是 Vector3 类中的一个静态方法。RotateTowards 接受四个参数:前两个是 Vector3,分别是旋转时背离和朝向的矢量。
- 该代码以 transform.forward 开头,目标是 m_Movement 变量。transform.forward 是访问 Transform 组件并获取其前向矢量的快捷方式。
- 接下来的两个参数是起始矢量和目标矢量之间的变化量:首先是角度变化(以弧度为单位),然后是大小变化。此代码中的角度变化为 turnSpeed * Time.deltaTime,而大小变化为 0。
Time.deltaTime 是距上一帧的时间(也可以将其视为两帧之间的时间)。那么,为什么需要将 turnSpeed 乘以该数值?
每帧都会调用一次 Update 方法,如果您的游戏以 60 帧/秒的速度运行,则意味着该方法将在一秒钟内被调用 60 次。每次调用都会有很小的变化,这样在 60 帧内您可以得到想要的一秒钟内的变化。但是,以 30 帧/秒的速度运行的游戏呢?只有一半的方法调用将在同一时间进行,因此只会发生一半的旋转。您不希望每秒的帧数影响角色的旋转速度,因为这是不对的!
如果不是每帧进行更改而是每秒进行更改呢?这样,事情就变得容易得多了。为此,您需要将每秒所需的任何更改乘以一帧花费的时间。这段代码正是这样做的。
12. 调整 turnSpeed 变量
turnSpeed 变量是您希望角色每秒旋转的角度(以弧度为单位)。然后,此变量乘以 Time.deltaTime 即可得出角色在此帧应旋转的量。弧度是角度的另一种度量方式;与度相似,但更自然。一个圆为 2π 弧度,即大约为 6 弧度。您的角色总是选取最短的旋转长度,因此,角色转过的最大角度约为 3 弧度。
据此可知,turnSpeed 为 3 意味着角色转一整圈大约需要一秒钟。这样的速度实际上很慢。turnSpeed 为 6 意味着转一圈大约需要半秒,这仍然很慢。让我们尝试将值设置为 20,然后看看效果如何。如果需要,稍后随时可以更改此值。
将在类顶部声明 turnSpeed 变量的代码行更改为:
public float turnSpeed = 20f;现在我们有了一个矢量,对应于您可以让角色面对的方向!
创建和存储旋转
接下来,您需要使用矢量来获取并存储旋转,以便可以在任何需要的地方使用。您将像存储移动矢量一样存储旋转,因此在该位置声明其变量很合理。
在声明名为 m_Movement 的 Vector3 的代码行下面,添加以下行:
Quaternion m_Rotation = Quaternion.identity;四元数 (Quaternion) 是存储旋转的一种方式,可用于解决将旋转存储为 3D 矢量时遇到的一些问题。本教程不会详细探讨四元数,现在只需要知道它们是一种存储旋转的方式即可。
您已为 Quaternion 指定了默认值 Quaternion.identity。通常情况下,创建类的实例时,会将属于类的变量(成员变量)而不是属于特定方法的变量设置为默认值。例如,Vector3 的默认值是将 x、y 和 z 都设置为 0。四元数也是如此。但是,虽然零矢量是合理的(因为没有移动),零四元数却不太合理。这里设置的 Quaternion.identity 值就是为其赋予无旋转的值,这是一个更合理的默认值。
您已经创建了旋转变量,现在可以对其进行设置。在创建 desiredForward 变量的代码行下面,添加以下行:
m_Rotation = Quaternion.LookRotation (desiredForward);
该行代码仅调用 LookRotation 方法,并在给定参数的方向上创建旋转。
您的脚本现在应如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public float turnSpeed = 20f;
Animator m_Animator;
Vector3 m_Movement;
Quaternion m_Rotation = Quaternion.identity;
void Start ()
{
m_Animator = GetComponent<Animator>();
}
void Update ()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize ();
bool hasHorizontalInput = !Mathf.Approximately (horizontal, 0f);
bool hasVerticalInput = !Mathf.Approximately (vertical, 0f);
bool isWalking = hasHorizontalInput || hasVerticalInput;
m_Animator.SetBool("IsWalking", isWalking);
Vector3 desiredForward = Vector3.RotateTowards (transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
m_Rotation = Quaternion.LookRotation (desiredForward);
}
}
13. 将移动和旋转应用于角色
任务几乎快完成了!最后一步是将移动和旋转应用于角色。您可以通过多种方式来执行此操作,但是由于角色需要成为物理系统的一部分,因此您需要移动刚体而不是使用任何其他方法。
为此,您需要具有对 Rigidbody 组件的引用。获取该引用的方式与 Animator 组件完全相同。
1.在声明 Animator 变量之后,立即将以下行添加到脚本中:
Rigidbody m_Rigidbody;
2.在设置 Animator 变量的引用之后,添加另一行代码:
m_Rigidbody = GetComponent<Rigidbody> ();现在,您已经具有对刚体的引用,接下来让我们考虑一下移动动画化角色的细节。该角色有一段有趣的 Walk 动画,最好为此使用根运动。但是,该动画中没有任何旋转,如果您尝试在 Update 方法中旋转刚体 (Rigidbody),则动画可能会覆盖该刚体(这可能导致角色在应该旋转的时候不旋转)。
您实际需要的是动画的一部分根运动,但不是全部的根运动;具体来说,您需要应用移动而不是旋转。那么如何更改从 Animator 中应用根运动的方式呢?幸运的是,MonoBehaviour 有一种特殊的方法可用于更改从 Animator 中应用根运动的方式。
在 Update 方法下面,声明一个新方法:
void OnAnimatorMove ()
{
}
此方法允许您根据需要应用根运动,这意味着可以分别应用移动和旋转。
14. 移动
让我们先从移动开始吧。将以下行添加到新的 OnAnimatorMove 方法中:
m_Rigidbody.MovePosition (m_Rigidbody.position + m_Movement * m_Animator.deltaPosition.magnitude);这一行代码看起来同样也很复杂,但对您来说,这里几乎没有什么全新的元素。
首先,您要使用对 Rigidbody 组件的引用来调用其 MovePosition 方法,并传入唯一的参数:其新位置。新位置从刚体的当前位置开始,然后您在此基础上添加一个更改 — 移动矢量乘以 Animator 的 deltaPosition 的大小。但是,这是什么意思?
Animator 的 deltaPosition 是由于可以应用于此帧的根运动而导致的位置变化。您将其大小(即长度)乘以我们希望角色移动的实际方向上的移动向量。
15. 旋转
接下来,应用旋转。在 OnAnimatorMove 方法中的 MovePosition 调用下面添加以下行:
m_Rigidbody.MoveRotation (m_Rotation);这与 MovePosition 调用非常相似,但它适用于旋转。这次您无需对旋转进行更改,而只是直接设置旋转。
这便是您第一个脚本的最后一行代码!但是,您还需要进行一项调整。
16. 更改您的 Update 方法
在上一教程中,您了解了 Update 循环(用于渲染)和 FixedUpdate 循环(用于运行物理操作)。您已经确保 Animator 通过物理循环适时运行,从而避免物理与动画之间发生冲突。但是,现在您将使用 OnAnimatorMove 来覆盖根运动。这意味着 OnAnimatorMove 实际上将通过物理适时被调用,而不是像 Update 方法那样通过渲染被调用。
移动矢量和旋转在 Update 中加以设置。如果首先调用的是 OnAnimatorMove,则由于未设值的四元数没有任何意义,因此将遇到问题。
为了确保通过 OnAnimatorMove 适时设置移动矢量和旋转,请按如下所示将 Update 方法更改为 FixedUpdate 方法:
void FixedUpdate ()
这是 Unity 自动调用的另一个特殊方法,但是这个方法是通过物理适时调用的。FixedUpdate 不是在每个渲染的帧之前被调用,而是在物理系统处理所有碰撞和其他已发生的交互之前被调用。默认情况下,每秒正好调用 50 次这个方法。
就是这样!完成的脚本应如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public float turnSpeed = 20f;
Animator m_Animator;
Rigidbody m_Rigidbody;
Vector3 m_Movement;
Quaternion m_Rotation = Quaternion.identity;
void Start ()
{
m_Animator = GetComponent<Animator> ();
m_Rigidbody = GetComponent<Rigidbody> ();
}
void FixedUpdate ()
{
float horizontal = Input.GetAxis ("Horizontal");
float vertical = Input.GetAxis ("Vertical");
m_Movement.Set(horizontal, 0f, vertical);
m_Movement.Normalize ();
bool hasHorizontalInput = !Mathf.Approximately (horizontal, 0f);
bool hasVerticalInput = !Mathf.Approximately (vertical, 0f);
bool isWalking = hasHorizontalInput || hasVerticalInput;
m_Animator.SetBool ("IsWalking", isWalking);
Vector3 desiredForward = Vector3.RotateTowards (transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
m_Rotation = Quaternion.LookRotation (desiredForward);
}
void OnAnimatorMove ()
{
m_Rigidbody.MovePosition (m_Rigidbody.position + m_Movement * m_Animator.deltaPosition.magnitude);
m_Rigidbody.MoveRotation (m_Rotation);
}
}
请记住:在 C# 中,在类中声明方法的顺序无关紧要,因此您的方法排列顺序可以稍有不同。
保存您的脚本,然后好好褒奖一下自己!现在该回到 Unity 并测试您的工作了。
17. 测试所做的更改
将 PlayerMovement 脚本添加到 JohnLemon
返回到 Unity 后,需要将该脚本作为组件添加到 JohnLemon。
您可以通过单击 Inspector 窗口中的 Add Component 按钮来添加脚本组件,但也可以使用另一种方式:
1.选择 JohnLemon 游戏对象。
2.在 Inspector 窗口中,单击 Edit Prefab 以进入预制件模式 (Prefab Mode)。
3.在 Project 窗口中,选择 Assets > Scripts,然后找到 PlayerMovement 脚本。
4.将 PlayerMovement 脚本从 Project 窗口拖到 Inspector 窗口中。
5.如果您的 Prefab Mode 未设置为 Auto Save,请单击 Save 按钮
6.单击后退箭头沿着预制件的导航路径返回。
7.如果未加载 MainScene,请转到 Project 窗口,然后通过双击 Assets > Scenes 文件夹中的场景资源 (Scene Asset) 来加载该场景。
8.在场景中选择您的 JohnLemon 游戏对象,然后您将看到该游戏对象具有您为其预制件提供的所有组件和设置。
18. 调整 Game 视图设置
在测试预制件之前,您可能需要调整 Game 视图的某些设置。如果您有高分辨率的显示器,Unity 会自动缩放 Game 窗口以提高性能。这是通过设置 Game 窗口的缩放 (Scale) 来实现的。
目前,您的场景还不够复杂,不足以引起性能问题,因此,如果您遇到这样的问题,请立即修复。
1.选择 Game 窗口选项卡。您可以在顶部查看 Game 窗口是否有缩放。
2.单击当前显示为 Free Aspect 的宽高比下拉选单。

3.禁用 Low Resolution Aspect Ratios 复选框,然后将 Scale 滑动条更改为 1x。如果您无法禁用此复选框(或此复选框已被禁用),也不要担心,这并不会影响完成的游戏,您的显示器应该自动进行了该配置。
现在,Game 窗口看起来应该是正确的,因此可以测试 JohnLemon 了!单击 Play 按钮开始,然后使用箭头键四处移动 John Lemon。

注意:如果收到编译器错误消息,请不要惊慌!返回并根据示例仔细检查代码;代码中很容易出错,尤其是在刚编写第一个脚本时。务必保存所有修复,然后再次在播放模式下进行测试。
19. 总结
在本教程中,您编写了第一个脚本,并探讨了 Unity 编码的一些核心概念。如果您没有编码经验,但轻松理解了所有内容,那么您做得很好!但是,如果您在初次尝试时遇到任何困难,请不要担心,可能需要一些时间和实践才能理解这些概念。
在下一教程中,您将为游戏创建环境和光照,以便让 JohnLemon 从阴森恐怖的氛围逃离。