八、菜单系统类
在这一章中,我们将回顾游戏的菜单系统类,但是在此之前,我想回顾一下游戏类的结构。在这一章中,我们将介绍一个新的基类,它为其他菜单屏幕类提供核心菜单屏幕支持。由于事情变得有点复杂,我们最好在这里回顾一下游戏的职业结构。
图 8-1
Hover Racers 类继承图 1A Hover Racers 类图显示基类和独立类的扩展
正如您在前面列出的图表中所看到的,Hover Racers 代码库中的大多数类都扩展了BaseScript
类,并通过扩展扩展扩展了MonoBehaviour
类。这意味着所有那些列在BaseScript
类条目下的树形结构中的类都是可以附加到 Unity 游戏对象上的脚本组件。到目前为止,您可以在任何演示场景中看到这一点。注意,有几个菜单系统类扩展了一个新的二阶基类,即BasePromptScript
类。在上图中,有两个二阶基类:BasePromptScript
和WaterResetScript
类。
我们不会在这里讨论任何 Unity 编辑器菜单系统的细节,但是我们会在后面的正文中讨论。实际上有三个类比其他菜单系统类更相似。我们将会详细讨论这些课程。我把它们列在这里。
-
GamePauseMenu
-
GameOverMenu
-
GameExitMenu
游戏还使用了一些其他的菜单系统类,其中一些我们会在本章中稍微讨论一下。
-
游戏帮助菜单
-
GameHUDNewScript
-
GameHUDNewUiScript
-
游戏开始菜单
我们将从前面列出的第一组类扩展的基类开始,即BasePromptScript
类。这个脚本很简洁,所以我们将跳过更复杂的课程回顾,只列出完整的课程。
课堂复习:BasePromptScript
如前面列出的图表所示,BasePromptScrip
t 类是GameExitMenu
、GameHelpMenu
、GameOverMenu
、GamePauseMenu
和GameStartMenu
类的基类。这个基类扩展了我们熟知的另一个基类BaseScript
。因此,任何扩展类都是MonoBehaviour
类,通过继承,在它们可用的两个基类中都定义了功能。因为“提示”菜单屏幕都有相似的功能,两个按钮、一个声音效果和一个文本提示等。将功能、字段和方法集中到一个基类中是有意义的。我们将使用以下复习步骤来涵盖本课程。
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
就这样,我们开始吧,好吗?第一部分是类字段。
类字段:BasePromptScript
BasePromptScript
类有许多类字段,用于跟踪键盘、鼠标、控制器或触摸输入与菜单屏幕的交互。听起来我们做了很多,但是让我们考虑一下。Unity 游戏引擎支持鼠标,这意味着所有的 UI 按钮都会响应鼠标点击事件。此外,Unity 支持触摸输入,因此触摸屏设备本身将支持按钮点击交互。所以我们可以免费得到这么多。还不错。我们必须关心的输入是键盘和控制器输入,你会看到这反映在类的字段和方法中。
public bool keyBrdInput = false;
public int keyBrdInputIdx = 0;
public int keyBrdInputPrevIdx = -1;
public int keyBrdInputIdxMax = 2;
public int keyBrdInputIdxMin = 0;
public Text txt = null;
public bool btnPressed = false;
public Button btnOne;
public Button btnTwo;
Listing 8-1BasePromptScript Class Fields 1
keyBrdInput
字段用于确定输入映射“MenuSelectUp”或“MenuSelectDown”是否被使用。这样,控制器输入和键盘输入可以映射到前面列出的关键字,从而为不同的输入源创建无缝的抽象。这意味着,我们将游戏配置为将某些键盘按键(上下箭头)路由到与控制器方向板的菜单输入上下箭头相同的输入映射。列表中的下一个字段keyBrdInputIdx
用于跟踪该类当前高亮显示的 UI 元素。keyBrdInputPrevIdx
用于跟踪先前高亮显示的 UI 元素。与这些字段相关的是接下来的两个类字段,keyBrdInputIdxMin
和keyBrdInputIdxMax
。这些字段用于指定当前菜单屏幕上可用的 UI 元素的最小和最大索引。
扩展了BasePromptScript
类的屏幕主要是带有文本提示的 yes 或 no 菜单屏幕。在这种情况下,最大索引设置为 2。这些字段之后是 Unity UI Text
类实例txt
。这个类用于在菜单屏幕上显示文本提示。布尔标志btnPressed
用于跟踪菜单按钮是否被按下。最后,btnOne
和btnTwo
字段是 Unity UI 类Button
的实例,用于显示菜单屏幕的按钮选项。本复习部分到此结束。接下来,我们将看看这个类的相关方法大纲。
相关的方法大纲/类头:BasePromptScript
BasePromptScript
类有一些主要的和支持的方法供我们回顾,在这里列出。
//Main Methods
public void Update();
public void InvokeClick(int current);
//Support Methods
public void SetBtnTextColor(int prev, int current);
Listing 8-2BasePromptScript Pertinent Method Outline/Class Headers 1
该类的导入语句和头文件如下。注意,BasePromptScript
类扩展了我们之前提到的BaseScript
类。另外,花点时间注意一下导入语句,特别是UnityEngine.UI
名称空间。
using UnityEngine;
using UnityEngine.UI;
public class BasePromptScript : BaseScript {}
Listing 8-3BasePromptScript Pertinent Method Outline/Class Headers 2
这就把我们带到了本节的结尾。接下来让我们从类的支持方法开始方法回顾。
支持方法详细信息:BasePromptScript
这个类只有一个支持方法供我们讨论,列在这里。
01 public void SetBtnTextColor(int prev, int current) {
02 if (prev == 0) {
03 txt = btnOne.transform.GetChild(0).GetComponent<Text>();
04 txt.color = Color.white;
05 } else if (prev == 1) {
06 txt = btnTwo.transform.GetChild(0).GetComponent<Text>();
07 txt.color = Color.white;
08 }
09
10 if (current == 0) {
11 txt = btnOne.transform.GetChild(0).GetComponent<Text>();
12 txt.color = Color.red;
13 } else if (current == 1) {
14 txt = btnTwo.transform.GetChild(0).GetComponent<Text>();
15 txt.color = Color.red;
16 }
17 }
Listing 8-4BasePromptScript Support Method Details 1
SetBtnTextColor
方法用于改变菜单屏幕的按钮文本颜色,以指示通过使用键盘或控制器输入哪个按钮被高亮显示。在这个方法中,在第 2–8 行,我们将先前突出显示的 UI 元素的文本颜色重置为白色。注意,我们获取了对Text
组件的引用,然后调整了它的color
字段,第 4 行和第 7 行。类似地,我们需要调整当前突出显示的 UI 元素,以表明它是选中的元素。第 10–16 行的代码与前面的代码块相同,只是在这种情况下,我们将文本更改为红色。红色表示突出显示的 UI 元素。在下一节中,我们将介绍这个类的主要方法。
主要方法详细信息:BasePromptScript
BasePromptScript
类有两个主要的方法让我们复习。我们将在随后的清单中详细介绍这两者。
01 public void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (keyBrdInput == false) {
07 if (Input.GetButtonUp("MenuSelectUp")) {
08 keyBrdInput = true;
09 keyBrdInputIdx = 0;
10 keyBrdInputPrevIdx = -1;
11 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
12 } else if (Input.GetButtonDown("MenuSelectDown")) {
13 keyBrdInput = true;
14 keyBrdInputIdx = (keyBrdInputIdxMax - 1);
15 keyBrdInputPrevIdx = -1;
16 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
17 }
18 } else {
19 if (Input.GetButtonUp("MenuSelectUp")) {
20 if (keyBrdInputIdx + 1 < keyBrdInputIdxMax) {
21 keyBrdInputPrevIdx = keyBrdInputIdx;
22 keyBrdInputIdx++;
23 } else {
24 keyBrdInputPrevIdx = (keyBrdInputIdxMax - 1);
25 keyBrdInputIdx = 0;
26 }
27 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
28 } else if (Input.GetButtonDown("MenuSelectDown")) {
29 if (keyBrdInputIdx - 1 >= keyBrdInputIdxMin) {
30 keyBrdInputPrevIdx = keyBrdInputIdx;
31 keyBrdInputIdx--;
32 } else {
33 keyBrdInputPrevIdx = keyBrdInputIdx;
34 keyBrdInputIdx = (keyBrdInputIdxMax - 1);
35 }
36 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
37 } else if (Input.GetButtonDown("Submit")) {
38 InvokeClick(keyBrdInputIdx);
39 }
40 }
41 }
01 public void InvokeClick(int current) {
02 if (current == 0) {
03 btnOne.onClick.Invoke();
04 } else if (current == 1) {
05 btnTwo.onClick.Invoke();
06 }
07 }
Listing 8-5BasePromptScript Main Method Details 1
我们要看的第一个方法是类的’Update
方法。每个游戏帧都会调用这个方法,它负责调整菜单屏幕的状态以响应用户输入。在第 2–4 行,如您所料,如果类配置失败,方法会被转义而不做任何工作。让我描述一下菜单屏幕是怎么回事,以及它是如何处理键盘和控制器输入的。这将使审查下一个代码块更加有效。当菜单屏幕第一次显示时,没有选定的 UI 元素。如果检测到正确的键盘或控制器输入、箭头键和方向板按钮,则菜单通过选择第一个 UI 元素并高亮显示它来做出反应。从这一点开始,这种类型的进一步输入将改变所选择的 UI 元素。如果突出显示的元素是菜单中的最后一个元素,则第一个 UI 元素会突出显示,反之亦然。
记住这一点,让我们看看负责实现我们刚刚描述的功能的方法。第 6–18 行的代码用于处理初始键盘或控制器输入,并选择一个 UI 元素。如果检测到“MenuSelectUp”输入,则keyBrdInput
标志设置为真,第一个菜单按钮高亮显示,第 8–11 行。或者,如果检测到“MenuSelectDown ”,那么keyBrdInput
标志也被设置为真,但是我们选择最后一个菜单按钮,第 13–16 行。如果keyBrdInput
标志为真,则执行第 18–40 行的下一个代码块。在这种情况下,如果检测到“MenuSelectUp”输入,我们将选定的 UI 元素上移一位,如果我们移过最后一个 UI 元素(第 20–27 行),则循环回到第一个元素。以类似的方式,如果检测到“MenuSelectDown”输入,我们将选定的 UI 元素下移一个,如果我们移过第一个 UI 元素,则循环回到最后一个元素,第 29-36 行。
最后,在第 37–39 行,如果检测到“提交”输入,我们通过调用第 38 行的InvokeClick
方法调用当前选中按钮上的点击事件来提交菜单屏幕。集合中的最后一个方法是 Invoke 方法。此方法用于提供一种方式,以编程方式在菜单屏幕的两个按钮中的任何一个上引发单击事件。本复习部分到此结束。在下一节中,我们将看看如何演示这个类的功能。
演示:BasePromptScript
要清楚地演示BasePromptScript
类的运行有点困难,因为它是一个由游戏的一些菜单屏幕使用的基类。也就是说,我们当然可以看到一些菜单屏幕在运行。我们最好的选择是运行完整的游戏。如果你在 Unity 编辑器中打开这个项目,并注意到“项目”面板,你应该会看到一个名为“场景”的条目。打开这个文件夹,找到名为“Main13”或“Main14”的场景。这两个场景将开始整个游戏。一旦游戏运行,开始一场比赛,然后在倒计时完成后点击游戏左下角的退出按钮。您应该会看到GameExitMenu
屏幕。使用键盘或控制器与它交互,以可视化我们刚刚查看的代码。接下来,我们将看看一些特定的菜单屏幕。
课堂回顾:GamePauseMenu
GamePauseMenu
类是一个双按钮提示屏幕的例子,它扩展了我们刚刚讨论过的BasePromptScript
类。因此,它从BasePromptScript
和BaseScript
基类中获得了很多功能。我们将使用以下复习步骤来涵盖本课程。
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
我们只有四个审查步骤的原因是因为我们已经扩展了多个基类并继承了它们的功能,从而简化了类的实现。接下来让我们看看课程大纲。
相关的方法大纲/类头:GamePauseMenu
这个类有一个主方法和一些支持方法供我们检查,没有其他的了。让我们看一看,好吗?
//Main Methods
void Start();
//Support Methods
public void PerformResumeGameUI();
public void PerformResumeGame();
public void PerformEndGameUI();
public void PerformEndGame();
Listing 8-6GamePauseMenu Pertinent Method Outline/Class Headers 1
该类的导入语句和声明如下。
using UnityEngine;
using UnityEngine.SceneManagement;
using static GameState;
public class GamePauseMenu : BasePromptScript {}
Listing 8-7GamePauseMenu Pertinent Method Outline/Class Headers 2
注意,GamePauseMenu
类扩展了BasePrompScript
类,正如我们之前提到的。另外,注意这个类比我们之前看到的有更多的导入,特别是UnityEngine.SceneManager
名称空间和GameState
导入。“using static GameState
”行允许这个类从GameState
类中访问枚举。你会看到它们被用在类的方法中,例如,在PerformEndGame
方法中使用GameStateIndex.NONE
值。
支持方法详细信息:GamePauseMenu
GamePauseMenu
类有许多支持方法供我们回顾。让我们开始写代码吧!
01 public void PerformResumeGameUI() {
02 PerformResumeGame();
03 }
01 public void PerformResumeGame() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05 gameState.PlayMenuSound();
06 gameState.HidePauseMenu();
07 }
01 public void PerformEndGameUI() {
02 PerformEndGame();
03 }
01 public void PerformEndGame() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05 PlayerPrefs.SetInt("GameStateIndex", (int)GameStateIndex.NONE);
06 PlayerPrefs.Save();
07 gameState.PlayMenuSound();
08 gameState.ResetGame();
09 SceneManager.LoadScene(gameState.sceneName);
10 }
Listing 8-8GamePauseMenu Support Method Details 1
我们要回顾的这组支持方法负责处理点击输入事件,因为 UI 系统被设计成允许不同的输入来激活按钮点击事件。为了支持这一点,按钮的功能从输入事件处理程序中抽象出一个层次,我们很快就会看到。我们细读的第一个方法是PerformResumeGameUI
法。该方法直接连接到GamePauseMenu
屏幕的按钮上。当点击菜单屏幕按钮时,PerformResumeGameUI
方法调用PerformResumeGame
方法来完成恢复游戏的实际工作。
这允许我们直接调用PerformResumeGame
方法来完成相同的任务,以响应键盘和控制器输入。列出的下一个方法是PerformResumeGame
,负责在游戏暂停后恢复游戏。在第 2–4 行,我们有标准的转义码,如果类的配置有问题,它会阻止方法做任何工作。在第 5 行,我们播放了一个菜单声音,来自GameState
类实例,表明用户输入已经收到,在第 6 行,我们隐藏了暂停菜单,继续游戏。请注意,菜单屏幕非常依赖于由GameState
类提供的集中功能。
这个条目后面是PerformEndGameUi
方法。该方法直接连接到菜单屏幕上的按钮,并在响应用户输入时被调用。它调用PerformEndGame
方法来完成结束游戏的实际工作。请注意,在这两种情况下,直接用户输入事件处理程序都必须再进行一次方法调用来执行必要的工作。这是我前面提到的一个抽象层次。
标准转义码在PerformEndGame
方法的第 2–4 行。为了正确地结束游戏,我们需要重置游戏的“GameStateIndex”玩家偏好并保存更改,第 5–6 行。接下来,我们播放一个菜单声音效果来指示用户输入被接收,第 7 行,并在第 8 行重置游戏。我们需要做的最后一件事是重置整个场景,这是通过第 9 行的方法调用完成的。这个调用将要求场景管理器为我们重新加载当前场景。在下一节,我们将看看这个类的主要方法。
主要方法详细信息:GamePauseMenu
GamePauseMenu
类有一个主要的方法供我们回顾。在文本的这一点上,它应该看起来非常熟悉。不过,我们还是会谈到的。最好是彻底的。
01 void Start() {
02 base.Prep(this.GetType().Name);
03 if (BaseScript.IsActive(scriptName) == false) {
04 Utilities.wrForce(scriptName + ": Is Deactivating...");
05 return;
06 }
07 }
Listing 8-9GamePauseMenu Main Method Details 1
这是一个简单的Start
方法的例子。该类在第 2 行准备好,如果有任何地方出错,该方法会写一些日志并退出,第 4–5 行。除此之外就没什么了。接下来的复习部分,看看我们能不能拿出一个像样的这个类的演示。
演示:游戏暂停
在行动中演示GamePauseMenu
的最好方式是运行主游戏。找到“项目”面板,找到“场景”文件夹,并打开它。寻找名为“Main13”或“Main14”的场景。打开场景并运行它。您需要通过点击“赛道 1”或“赛道 2”按钮来开始一场由玩家控制的比赛。比赛倒计时开始后,点击 Unity 编辑器以外的其他应用。请注意,游戏暂停菜单屏幕弹出,游戏停止。
返回 Unity 编辑器,使用向上和向下键盘按钮更改所选的暂停菜单按钮。请注意,您可以使用鼠标或回车键单击按钮。这是我们工作中的抽象层。GameOverMenu
和GameExitMenu
类与我们刚刚复习的类非常相似。我让你自己去看。在你继续学习之前,确保你已经很好的理解了这些课程。
课堂回顾:游戏助手菜单
GameHelpMenu
用于显示一系列帮助屏幕,描述关于 Hover Racers 游戏的不同细节。这个菜单屏幕比我们之前看到的要复杂一些。我们将使用以下步骤来复习这门课。
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
我们将看到一些与 UI 交互、选择、点击等类似的代码,就像我们之前看到的一样,但是稍微复杂一些。
类字段:游戏帮助菜单
GameHelpMenu
有许多用于控制用户与帮助菜单交互的类字段。不同的帮助菜单屏幕用于显示不同的图像,这些图像提供了关于如何玩游戏的信息。
//***** Class Fields: Images *****
public Image help1 = null;
public Image help2 = null;
public Image help3 = null;
public Image help4 = null;
public Image help5 = null;
public Image help6 = null;
public Image help7 = null;
public Image help8 = null;
//***** Internal Variables *****
private Image img = null;
private int idx = 0;
private int MAX_INDEX = 8;
//***** Class Fields *****
public Button btnPrev = null;
public Button btnNext = null;
public Button btnThree = null;
Listing 8-10GameHelpMenu Class Fields 1
前八个条目都是 Unity Image
实例,用于显示关于如何玩游戏的不同帮助屏幕。这些图像在 Unity 编辑器中使用“检查器”面板进行配置,而不是以编程方式进行配置。下面的条目img
用作临时占位符,帮助更改菜单不同按钮的文本颜色。MAX_INDEX
字段用于指示该菜单屏幕显示的帮助图像的最大数量。其余的类字段是用于导航帮助菜单屏幕的按钮。接下来我们将查看相关的方法大纲。
相关的方法大纲/类头:GameHelpMenu
GameHelpMenu
类的相关方法概述如下。
//Main Methods
void Start();
new void Update();
public new void InvokeClick(int current);
public new void SetBtnTextColor(int prev, int current);
//Support Methods
public void EnablePrev();
public void DisablePrev();
public void EnableNext();
public void DisableNext();
private void ShowHelpScreen(int i);
//Support Methods: Input Handlers
public void PerformMainMenuUI();
public void PerformMainMenu()
public void PerformNextUI();
public void PerformNext();
public void PerformPrevUI();
public void PerformPrev();
Listing 8-11GameHelpMenu Pertinent Method Outline/Class Headers 1
在这个节骨眼上,实际上有几个要点我想讨论一下。首先,请注意,有些方法条目用 new 关键字进行了修饰。这是因为这些方法是由GameHelpMenu
类扩展的基类之一定义的。new 关键字用于告诉编译器该方法正在被重定义。我想提到的另一件事是,我们有一个新的方法部分,即“Support Methods: Input Handlers
”。这样做的原因是有很多输入处理程序,我认为我们应该把它们分开,因为它们的功能和用途是相似的。接下来,我们将看看下面清单中详细列出的类的导入和声明。
using UnityEngine;
using UnityEngine.UI;
public class GameHelpMenu : BasePromptScript {}
Listing 8-12GameHelpMenu Pertinent Method Outline/Class Headers 2
这个类使用来自UnityEngine
和UnityEngine.UI
名称空间的导入。注意,GameHelpMenu
类扩展了BasePromptScript
基类。我们以前见过这个类的使用。它向扩展它的菜单类添加了一些默认字段和功能。
支持方法详情:GameHelpMenu
GameHelpMenu
类有许多支持方法。这些方法有两种风格:支持方法和输入处理程序方法。我们将从下面列出的标准支持方法开始。
01 public void EnablePrev() {
02 if (btnPrev != null) {
03 btnPrev.interactable = true;
04 }
05 }
01 public void DisablePrev() {
02 if (btnPrev != null) {
03 btnPrev.interactable = false;
04 }
05 }
01 public void EnableNext() {
02 if (btnNext != null) {
03 btnNext.interactable = true;
04 }
05 }
01 public void DisableNext() {
02 if (btnNext != null) {
03 btnNext.interactable = false;
04 }
05 }
01 private void ShowHelpScreen(int i) {
02 if (help1 != null) {
03 help1.gameObject.SetActive(false);
04 }
05
06 if (help2 != null) {
07 help2.gameObject.SetActive(false);
08 }
09
10 if (help3 != null) {
11 help3.gameObject.SetActive(false);
12 }
13
14 if (help4 != null) {
15 help4.gameObject.SetActive(false);
16 }
17
18 if (help5 != null) {
19 help5.gameObject.SetActive(false);
20 }
21
22 if (help6 != null) {
23 help6.gameObject.SetActive(false);
24 }
25
26 if (help7 != null) {
27 help7.gameObject.SetActive(false);
28 }
29
30 if (help8 != null) {
31 help8.gameObject.SetActive(false);
32 }
33
34 if (i == 0) {
35 help1.gameObject.SetActive(true);
36 DisablePrev();
37 EnableNext();
38 } else if (i == 1) {
39 help2.gameObject.SetActive(true);
40 EnablePrev();
41 EnableNext();
42 } else if (i == 2) {
43 help3.gameObject.SetActive(true);
44 EnablePrev();
45 EnableNext();
46 } else if (i == 3) {
47 help4.gameObject.SetActive(true);
48 EnablePrev();
49 EnableNext();
50 } else if (i == 4) {
51 help5.gameObject.SetActive(true);
52 EnablePrev();
53 EnableNext();
54 } else if (i == 5) {
55 help6.gameObject.SetActive(true);
56 EnablePrev();
57 EnableNext();
58 } else if (i == 6) {
59 help7.gameObject.SetActive(true);
60 EnablePrev();
61 EnableNext();
62 } else if (i == 7) {
63 help8.gameObject.SetActive(true);
64 EnablePrev();
65 DisableNext();
66 }
67 }
Listing 8-13GameHelpMenu Support Method Details 1
前几个方法用于启用或禁用表单的一些 UI 元素。在这种情况下,我们有方法来控制启用或禁用next
和prev
按钮。这些方法非常简单,本质上是相似的。在继续下一步之前,花点时间仔细检查一下,确保你理解了它们。我们要看的下一个方法是ShowHelpScreen
方法。
ShowHelpScreen
方法类似于我们刚刚看到的启用/禁用方法,除了它被设计为与所有菜单的帮助图像一起工作,它负责禁用所有图像,然后只启用指定的图像。在该方法的第一个代码块(第 2–32 行)中,检查每个图像字段的空值,如果定义了空值,则随后将其禁用。第 34–66 行的下一个代码块用于根据图像列表中的当前位置启用指定的图像以及相关的“上一页”和“下一页”按钮。第一个和最后一个图像分别禁用“上一个”和“下一个”按钮。这就结束了该类的基本支持方法。在下一节中,我们将看看这个类的输入处理程序支持方法。
输入处理程序支持方法详细信息:GameHelpMenu
我们要介绍的第二组支持方法是输入处理程序支持方法。这些方法遵循与我们之前在两个按钮提示菜单屏幕中看到的相似的模式。一个例子是GamePauseMenu
类。我在这里详细介绍一下方法。
01 public void PerformMainMenuUI() {
02 PerformMainMenu();
03 }
01 public void PerformMainMenu() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05 gameState.PlayMenuSound();
06 gameState.ShowStartMenu();
07 gameState.HideHelpMenu();
08 }
01 public void PerformNextUI() {
02 PerformNext();
03 }
01 public void PerformNext() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05 gameState.PlayMenuSound();
06 if ((idx + 1) < MAX_INDEX) {
07 idx++;
08 }
09 ShowHelpScreen(idx);
10 }
01 public void PerformPrevUI() {
02 PerformPrev();
03 }
01 public void PerformPrev() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05 gameState.PlayMenuSound();
06 if ((idx - 1) >= 0) {
07 idx--;
08 }
09 ShowHelpScreen(idx);
10 }
Listing 8-14GameHelpMenu Input Handler Support Method Details 1
GameHelpMenu
菜单有三个按钮,所以我们将有三组两个方法,每个都遵循我们在GamePauseMenu
类的输入处理程序中看到的相同抽象。在这种情况下,我们有主菜单、上一个和下一个按钮。请注意,在每种情况下,实际的工作都是由本地类方法完成的,除非正确配置了类,否则每个这样的方法都不会做任何工作。仔细检查这些方法,确保它们对你有意义,然后再继续。
主要方法详细信息:GameHelpMenu
这个类有四个方法供我们阅读。有两种MonoBehaviour
生命周期回调方法,Start
和Update
,以及两种 UI 管理方法,InvokeClick
和SetBtnTextColor
。
01 void Start() {
02 keyBrdInputIdxMax = 3;
03 base.Prep(this.GetType().Name);
04 if (BaseScript.IsActive(scriptName) == false) {
05 Utilities.wrForce(scriptName + ": Is Deactivating...");
06 return;
07 }
08 }
01 new void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (keyBrdInput == false) {
07 if (Input.GetButtonUp("MenuSelectUp")) {
08 keyBrdInput = true;
09 if (idx == 0) {
10 keyBrdInputIdx = 1;
11 keyBrdInputPrevIdx = -1;
12 } else {
13 keyBrdInputIdx = 0;
14 keyBrdInputPrevIdx = -1;
15 }
16 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
17 } else if (Input.GetButtonDown("MenuSelectDown")) {
18 keyBrdInput = true;
19 if (idx == MAX_INDEX - 1) {
20 keyBrdInputIdx = (keyBrdInputIdxMax - 1);
21 keyBrdInputPrevIdx = -1;
22 } else {
23 keyBrdInputIdx = 1;
24 keyBrdInputPrevIdx = -1;
25 }
26 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
27 }
28 } else {
29 if (Input.GetButtonDown("MenuSelectUp")) {
30 if (keyBrdInputIdx + 1 < keyBrdInputIdxMax) {
31 keyBrdInputPrevIdx = keyBrdInputIdx;
32 keyBrdInputIdx++;
33 } else {
34 keyBrdInputPrevIdx = (keyBrdInputIdxMax - 1);
35 keyBrdInputIdx = 0;
36 }
37
38 if (idx == 0 && keyBrdInputIdx == 0) {
39 keyBrdInputIdx++;
40 } else if (idx == (MAX_INDEX - 1) && keyBrdInputIdx == (keyBrdInputIdxMax - 1)) {
41 keyBrdInputIdx = 0;
42 }
43
44 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
45 } else if (Input.GetButtonDown("MenuSelectDown")) {
46 if (keyBrdInputIdx - 1 >= keyBrdInputIdxMin) {
47 keyBrdInputPrevIdx = keyBrdInputIdx;
48 keyBrdInputIdx--;
49 } else {
50 keyBrdInputPrevIdx = keyBrdInputIdx;
51 keyBrdInputIdx = (keyBrdInputIdxMax - 1);
52 }
53
54 if (idx == 0 && keyBrdInputIdx == 0) {
55 keyBrdInputIdx = (keyBrdInputIdxMax - 1);
56 } else if (idx == (MAX_INDEX - 1) && keyBrdInputIdx == (keyBrdInputIdxMax - 1)) {
57 keyBrdInputIdx--;
58 }
59
60 SetBtnTextColor(keyBrdInputPrevIdx, keyBrdInputIdx);
61 } else if (Input.GetButtonDown("Submit")) {
62 InvokeClick(keyBrdInputIdx);
63 }
64 }
65 }
Listing 8-15GameHelpMenu Main Method Details 1
我们要看的第一个方法是Start
方法。第一行代码(第 2 行)用于设置菜单的最大选择索引,以便在使用键盘或控制器导航帮助菜单的按钮时控制突出显示的 UI 元素。剩下的代码行,第 3–7 行,执行我们已经多次看到的标准类配置。我们要看的下一个方法是类的’Update
方法。
正如所料,第 2–4 行的代码用于在类的配置遇到问题时阻止方法执行。第 6 行 if 语句的第一个分支(从第 7 行到第 27 行)意在将菜单高亮显示的 UI 元素从初始状态设置为 false。来自键盘或控制器的用户输入将导致第一个或最后一个可选 UI 元素被高亮显示。从第 29 行到第 63 行的主 if 语句的第二个分支用于在选择初始 UI 元素后处理键盘或控制器输入。这段代码支持向前或向后循环可选的 UI 元素。最后,在第 61–63 行,检测到“提交”输入,调用InvokeClick
方法,将当前选择的菜单项索引作为参数。该方法将调用目标按钮上的 click 事件。
01 public new void InvokeClick(int current) {
02 if (current == 0) {
03 if (idx > 0) {
04 btnOne.onClick.Invoke();
05 }
06 } else if (current == 1) {
07 btnTwo.onClick.Invoke();
08 } else if (current == 2) {
09 if (idx < MAX_INDEX - 1) {
10 btnThree.onClick.Invoke();
11 }
12 }
13 }
01 public new void SetBtnTextColor(int prev, int current) {
02 if (prev == 0) {
03 img = btnOne.GetComponent<Image>();
04 img.color = Color.white;
05 } else if (prev == 1) {
06 txt = btnTwo.transform.GetChild(0).GetComponent<Text>();
07 txt.color = Color.black;
08 } else if (prev == 2) {
09 img = btnThree.GetComponent<Image>();
10 img.color = Color.white;
11 }
12
13 if (current == 0) {
14 img = btnOne.GetComponent<Image>();
15 img.color = Color.red;
16 } else if (current == 1) {
17 txt = btnTwo.transform.GetChild(0).GetComponent<Text>();
18 txt.color = Color.red;
19 } else if (current == 2) {
20 img = btnThree.GetComponent<Image>();
21 img.color = Color.red;
22 }
23 }
Listing 8-16GameHelpMenu Main Method Details 2
前面列出的第二组主要方法包含两个方法。这些方法用于调用按钮单击事件,并设置菜单按钮的文本颜色以指示选定的 UI 元素。我们要研究的第一个方法是InvokeClick
方法。我们以前在BasePromptScript
和GamePauseMenu
类中见过这个方法。注意方法声明中的new
关键字。这向编译器表明,这个继承自BasePromptScript
类的方法在这里被重新定义。如果我们被要求点击第一个按钮,方法的第 2 行,这对应于“上一步”按钮。请注意,只有当帮助菜单屏幕索引大于 0 时,我们才调用 click 事件。这意味着我们不在第一个屏幕上,所以我们可以返回。
在第 6–8 行,如果当前选择的按钮索引是 1,对应于“主菜单”按钮,我们不加判断地处理点击事件。如果有更多的帮助菜单屏幕要查看,第 8–12 行的最后一段代码执行“下一步”按钮单击。下一个方法SetBtnTextColor
负责设置指定按钮上的按钮文本颜色。它还重置上一个按钮的文本颜色。文本颜色的变化用于突出显示菜单按钮,表示它已被选中。第 2–11 行的小代码块用于恢复先前选择的按钮的文本颜色,而第 13–22 行的代码块用于设置当前选择的按钮的文本颜色。这就是本复习部分的结论。在下一节中,我们将看一看班级的示范。
演示:GameHelpMenu
为了演示GameHelpMenu
类的运行,我们最好运行完整的游戏,场景“Main13”或“Main14 ”,点击开始菜单上的“help”按钮启动帮助菜单。在 Unity 编辑器中打开游戏项目,注意“项目”面板,找到“场景”文件夹。打开一个主场景,按下 Unity 编辑器的播放按钮运行游戏。当游戏开始时,点击上面提到的“帮助”按钮,启动帮助菜单屏幕。尝试使用键盘、控制器或鼠标与菜单的 UI 元素进行交互。当你这样做的时候,记住你刚刚检查的代码。
剩余菜单类别
其余的菜单系统类如下所示:
-
游戏开始菜单
-
GameHUDNewScript
-
GameHUDNewUiScript
我不会在这里详细回顾这些类。剩下的三个类与我们刚刚复习的类非常相似。复习它们并不能获得任何新的常识;然而,你应该仔细阅读它们,确保你熟悉它们。请务必这样做。通过使用本章演示部分概述的方法,尝试在实际游戏中查看每个菜单屏幕。
就 Unity 编辑器中的对象和组件而言,我们没有涉及到设置菜单屏幕,但是我们将在稍后的文本中介绍。这就引出了本章的结论。在我们继续之前,我想回顾一下我们讲过的内容。
第二章结论
在这一章,我们设法涵盖了游戏菜单系统的主要方面。在这个过程中,我们从游戏规约列表中去掉了几个点。让我们总结一下本章所讲的内容。
-
BasePromptScript:大部分游戏的菜单系统使用的基类。该类包含核心的共享功能,以简化扩展类的实现。
-
GamePauseMenu:这个类是一个双选项菜单屏幕的具体实现,它使用
BasePromptScript
作为基类。我们还回顾了 UI 事件抽象,通过类调用菜单屏幕按钮上的点击事件的能力来证明。 -
这个类是一个更复杂的菜单屏幕实现的例子。虽然这个类也扩展了
BasePromptScript
类,但它覆盖了基类的大部分功能来支持三按钮菜单屏幕。
虽然我们没有涵盖游戏中的每个菜单屏幕,但我们涵盖了一组核心示例,这些示例带我们了解了菜单系统实现的关键公共方面。请务必查看一下我们在这里没有介绍的课程。仔细阅读它们,并跟随完整游戏场景的实际菜单屏幕,“Main13”或“Main14”。我们几乎完成了文本的代码部分,但是我们还有很多重要的内容要讲,所以坚持一下。在下一章,我们将开始回顾游戏的核心,管理玩家和游戏状态的类。
九、玩家和游戏状态类:第一部分
在这一章中,我们将看看负责跟踪玩家和游戏状态的类。有很多代码需要我们回顾,所以我把回顾分成了两章。这两章复习所涉及的课程如下:
列出的前两个类别PlayerInfo
和TrackScript
附属于随后的两个类别PlayerState
和GameState
。因为前两个类非常简单和直接,我们将从它们开始。我们开始吧!
课堂回顾:PlayerInfo
正如我们之前见过几次的,类负责松散地关联一个 Unity 游戏对象和一个玩家。为此,该类在存储在GameState
类中的可用播放器数组中保存相关播放器的索引。这个课比较短,我就在这里枚举一下吧。
01 using UnityEngine;
02
03 public class PlayerInfo : MonoBehaviour {
04 public int playerIndex = 0;
05 }
Listing 9-1PlayerInfo Class Review 1
是的,就这么简单。这其实非常简单。因为PlayerInfo
类扩展了MonoBehaviour
类,所以它是一个脚本组件,可以附加到 Unity 游戏对象上。当在 Unity 编辑器中分配给一个游戏对象时,你可以设置playerIndex
字段的值。这些信息可以从父游戏对象中找到,并用来查找存储在游戏主类GameState
中的玩家的PlayerState
对象。在我们回顾交互类的时候,我们已经看到过这个用例。接下来,我们将看看TrackScript
类。这个类是另一个用于保存游戏状态相关信息的MonoBehaviour
,更具体地说是与当前赛道相关的设置。
课堂回顾:音轨脚本
TrackScript
类是另一个简单的状态类,用于保存当前赛道的基本配置信息。这堂课又短又甜,所以我们就把它完整地列在这里。
1 using UnityEngine;
2
3 public class TrackScript : MonoBehaviour {
4 public int index = 0;
5 public bool headLightsOn = false;
6 public int laps = 3;
7 public string sceneName = "";
8 }
Listing 9-2TrackScript Class Review 1
预计TrackScript
将驻留在GameState
Unity 游戏对象上,紧挨着GameState
和PlayerState
组件的实例,如此处所示。
图 9-1
GameState Unity 游戏对象配置描述 GameState 游戏对象设置的屏幕截图。它显示了 GameState、PlayerState 和 TrackScript MonoBehaviours 脚本组件,根据需要附加到相同的父游戏对象
GameState
类负责加载和处理存储在关联的TrackScript
组件中的数据。职业的第一个领域,index
,目前没有被游戏使用。随意实现一些索引特定的代码,如大气效果或不同的背景音乐。列出的下一个字段headLightsOn
是一个布尔标志,用于指示当前赛道是否应该打开悬停赛车的前灯。随后,laps
字段用于显示当前赛道的圈数。我说建议是因为赛道难度和模式也会影响给定赛道的圈数。最后,sceneName
字段可用于为当前轨道/场景提供名称。我们要看的下一个类是PlayerState
类。这个类是一个怪物,所以要准备好一个冗长的类视图。
课堂回顾:玩家状态
PlayerState
类是通过BaseScript
类的扩展得到的MonoBehaviour
类。它用于在整个游戏过程中跟踪玩家的状态。因此,该类有大量字段跟踪与汽车运动和状态相关的各种值。因为PlayerState
类非常复杂,我们将使用更结构化的审查过程,并按照以下步骤来审查它:
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
没有相关的枚举可言,所以我们将省略这一部分。我应该花点时间提一下,有很多,几乎太多,类字段要回顾。不要对此感到不知所措,我们会慢慢地、详细地介绍一切。不要以为第一遍读完就一定要全记在脑子里。在处理游戏代码时,你很可能需要参考几次这个类的评论。在大多数情况下,您根本不需要调整这些字段,所以就把它看作是我们的彻底。就这样,让我们开始吧。
静态/常量/只读类成员:PlayerState
PlayerState
类有许多静态和只读的类字段。这些字段用于查找资源、跟踪某些修饰符以及定义正在进行的比赛类型的值。
public static bool SHOW_AI_LOGIC = true;
public static readonly int TYPE_SPEED_SLOW = 0;
public static readonly int TYPE_SPEED_NORM = 1;
public static readonly int TYPE_SPEED_BOOST = 2;
public static readonly int TYPE_PLAYER_HUMAN = 0;
public static readonly int TYPE_PLAYER_COMPUTER = 1;
public static readonly int TYPE_CAR_HOVER_GREEN = 0;
public static readonly int TYPE_CAR_HOVER_BLACK = 3;
public static readonly int TYPE_CAR_HOVER_RED = 4;
public static readonly int TYPE_CAR_HOVER_PURPLE = 5;
public static readonly int DEFAULT_POSITION = 0;
public static readonly float DEFAULT_MAX_SPEED = 200.0f;
public static readonly float DEFAULT_GRAVITY = 11.0f;
public static float LIMIT_MAX_SPEED = 300.0f;
Listing 9-3PlayerState Static/Constants/Read-Only Class Members 1
前面列出了我们要查看的第一组静态类字段。第一个条目SHOW_AI_LOGIC
,用于控制 AI 控制的汽车驾驶计算的显示。这实际上是一个非常酷的功能。在运行游戏的完整版本,场景“Main13”或“Main14”之前,将该字段的值设置为 true。打开场景并运行它。当 AI 悬浮赛车在赛道上奔跑时,切换到 Unity 编辑器的“场景”面板。确保你有一个自上而下的赛道视图,并缩小以便你能看到赛道的大部分。当盘旋赛车在赛道上前进时,跟随他们,你会注意到从赛车到下几个路点的小绿线。这些线是汽车的 AI 驾驶逻辑计算的一部分。
既然我们已经讨论了这个问题,让我们回到课堂上来。接下来是三个条目,用于表示慢速、正常或加速速度。接下来的两个条目,以“TYPE_PLAYER_
”开头的,用来表示当前玩家的类型,AI 还是人类控制的。随后,有四个条目用于指示给定玩家使用的赛车类型。DEFAULT_POSITION
字段用于为当前玩家提供默认位置值。接下来的两个条目,DEFAULT_MAX_SPEED
和DEFAULT_GRAVITY
,用于为悬停赛车的重力和最大速度值提供默认值。这个集合中列出的最后一个条目用于表示对玩家最大速度的限制。让我们继续查看下一组字段。
public readonly int MAX_IS_MISS_TIME = 2;
public readonly int MAX_IS_SHOT_TIME = 2;
public readonly int MAX_IS_HIT_TIME = 2;
public readonly int MAX_IS_LAP_COMPLETE_TIME = 6;
public static readonly int DEFAULT_LIFE_AMOUNT = 3;
public readonly int INVINC_SECONDS = 10;
public static readonly int DEFAULT_DAMAGE_AMOUNT = 0;
public readonly float MIN_STUCK_DISTANCE = 30.0f;
public readonly float MIN_STUCK_TIME = 3.0f;
public readonly float MIN_WAYPOINT_DISTANCE = 30.0f;
public readonly int MAX_SPEED_BONUS_DRAFTING = 4;
public readonly int MAX_SPEED_BONUS_PASSING = 20;
public readonly float MAX_SMOKE_TIME = 1000.0f;
public readonly int MAX_GAINED_LIFE_TIME = 2;
public static readonly string AUDIO_CLIP_CAR_SOUND1 = "CarAirNonGapless";
public static readonly string AUDIO_CLIP_CAR_SOUND2 = "car_idle_lp_01";
public static readonly string AUDIO_CLIP_GUN_SHOT = "explosion_short_blip_rnd_01";
Listing 9-4PlayerState Static/Constants/Read-Only Class Members 2
前面列出的静态/只读类字段集以四个最大计时值开始。这些字段以文本“MAX_IS_
”开头。这些字段保存战斗模式通知未命中、命中、命中和单圈完成时间通知的最大计时值。接下来,我们有DEFAULT_LIFE AMOUNT
字段,它为玩家设置了总生命值。INVINC_SECONDS
字段被用来设置无敌修改器激活的最大时间限制。在这个区域之后是DEFAULT_DAMAGE_AMOUNT
区域,用来准备每个玩家的伤害点。
随后,我们有一组最小值只读字段。MIN_STUCK_DISTANCE
字段表示用于确定当前玩家的悬停赛车是否没有移动足够的距离并且现在被卡住的距离值。类似地,MIN_STUCK_TIME
字段用于设置玩家的悬停赛车没有移动到被标记为卡住的最短时间。这两个字段都用于确定玩家是否被卡住。最后一个最小值输入,MIN_WAYPOINT_DISTANCE
,是在被认为是航迹上的“下一个”航路点之前,你离一个航路点的最小距离。
接下来我们要看的四个字段是最大值,用于设置悬停赛车手一些行为的限制。这些条目的前两个用于设置从起草和通过另一个悬停赛车获得的速度奖励的限制。下一个最大值字段MAX_SMOKE_TIME
,用于设置射击烟雾效果持续时间的限制。这个粒子效果已经被注释掉了,留给读者去实现。类似地,MAX_GAINED_LIFE_TIME
字段用于设置寿命延长通知持续时间的限制。
这组静态字段中的最后三个条目用于定位具有指定名称的AudioSource
组件。这就是静态类成员回顾部分的结论。在下一部分,我们将开始复习该课程的剩余字段。
类字段:PlayerState
PlayerState
类有许多字段留给我们回顾。他们有很多人。它们用于控制、跟踪和模拟人工智能和人类控制的玩家的悬停赛车行为的所有方面。我们开始吧!
//***** Class Fields *****
public int index = 0;
public GameObject player;
public bool active = true;
public float offTrackSeconds = 6.0f;
public float wrongDirectionSeconds = 6.0f;
//***** Input Class Fields *****
public CharacterController controller
;
public CharacterMotor cm;
public MouseLookNew mouseInput;
public FPSInputController fpsInput;
public Transform home;
private bool hasSetHome = false;
public bool pause = false;
//***** Car Descriptor Class Fields *****
//0 = slow, 1 = norm, 2 = boost
public int speedType = TYPE_SPEED_SLOW;
//0 = human, 1 = computer
public int playerType = TYPE_PLAYER_HUMAN;
//0 = green hover, 1 = red hover, 2 = black hover,
//3 = black hover, 4 = red hover, 5 = purple hover
public int carType = TYPE_CAR_HOVER_GREEN;
//***** Speed Class Fields *****
public float speed = 0.0f;
public float speedPrct = 0.0f;
public float speedPrctLimit = 0.0f;
public float maxSpeed = DEFAULT_MAX_SPEED;
public int position = DEFAULT_POSITION;
public float gravity = DEFAULT_GRAVITY;
Listing 9-5PlayerState Class Fields 1
我们要查看的第一组字段从一组通用类字段开始。第一个字段是最重要的index
字段。该字段用于指示该玩家状态实例与可用玩家数组中的哪个玩家相关联。下一个字段是一个GameObject
实例player
,用于保存对代表玩家的 Unity 游戏对象的引用。在这种情况下,这是悬停赛车模型。
布尔标志active
用于指示玩家是否活动。该字段之后是offTrackSeconds
字段。该字段描述在显示脱离轨道通知之前,车辆脱离轨道的秒数。随后,wrongDirectionSeconds
表示在显示错误方向通知之前,汽车必须朝错误方向行驶的秒数。
我们要查看的下一组字段是输入类字段。controller
字段是当前玩家的一个CharacterController
实例。接下来,有一个CharacterMotor
实例,cm
。我们还有MouseLookNew
、mouseInput
和FPSInputController
、fpsInput
的实例来完善我们的控制类。home
字段是一个Transform
实例,用于记录玩家的家或起始位置。
布尔标志hasSetHome
用于指示是否已经设置了初始位置。接下来,pause
字段是指示播放器是否暂停的布尔标志。下面一组类字段是汽车描述符类字段。第一个这样的字段是speedType
字段。该字段的值为 0、1 或 2,表示速度缓慢、正常或加速。类似地,playerType
字段是一个整数值,0 代表人类玩家,1 代表计算机玩家。
该组中的最后一个字段是carType
字段。该字段用于描述玩家驾驶的汽车类型。在我们的例子中,它描述了悬停赛车的颜色。这些值由前面列出的字段定义记录在注释中。我们必须查看的最后一组字段是速度等级字段。这些字段用于表示不同的速度相关特性、值和限制。speed
字段代表玩家汽车的速度。speedPrct
字段用于表示汽车当前行驶速度占悬停赛车最大速度的百分比。speedPrctLimit
字段类似,只是它表示汽车当前行驶的LIMIT_MAX_SPEED
值的百分比。
maxSpeed
字段用于保存汽车的当前最大速度。接下来,position
字段表示赛车在比赛中的当前位置,gravity
字段用于表示赛车的重力。这将我们带到下一组要查看的类字段。我把它们列在这里。
//***** Time Class Fields *****
public string time;
public int timeNum;
public float totalTime = 0;
public float hour = 0;
public float min = 0;
public float s = 0;
public float ms = 0;
//***** Off Track Class Fields *****
public bool offTrack = false;
public float offTrackTime = 0.0f;
//***** Wrong Direction Class Fields *****
public bool wrongDirection = false;
public float wrongDirectionTime = 0.0f;
//***** Skipped Waypoint Class Fields *****
public bool skippedWaypoint = false;
public float skippedWaypointTime = 0.0f;
//***** Cameras and Objects Class Fields *****
public GameObject gun;
public GameObject gunBase;
public new Camera camera;
public Camera rearCamera;
public GameObject car;
public GameObject carSensor;
Listing 9-6PlayerState Class Fields 2
我们要查看的下一组类字段是当前玩家在赛道上比赛的时间。第一个条目time
,是玩家的圈速的字符串表示。接下来的字段timeNum
,是一个单一的大整数表示玩家的圈速。接下来,totalTime
字段代表给定圈上的总持续时间,以毫秒为单位。随后会列出当前曲目时间的各个组成部分。第一个条目hour
表示在当前曲目上花费的小时数。希望不要太多。
接下来,min
类字段记录在当前赛道上花费的分钟数,而接下来的两个字段,s
和ms
,用于指示在当前赛道上花费的时间的秒和毫秒部分。接下来的三组字段用于跟踪玩家的偏离轨迹、错误方向和跳过的路点状态。下一个要审查的是跑道外的场地。该组中的第一个条目是offTrack
字段。该字段是一个布尔标志,用于指示玩家离开了跑道。该组中的第二个字段表示当前玩家离开赛道的时间。
这组字段之后是错误方向类字段。与偏离轨道字段的模式类似,错误方向字段由布尔标志和定时值、wrongDirection
和wrongDirectionTime
字段组成。下一组字段,即跳过的路点类字段,也由布尔标志和时间追踪字段组成。该组包含wrongDirection
和wrongDirectionTime
字段。类似地,下一组字段,即跳过的路点类字段,也由布尔标志和时间跟踪字段组成。这些是skippedWaypoint
和skippedWaypointTime
类字段。这种模式经常用于测量某些功能的持续时间,然后切换一个布尔字段来打开或关闭该功能。
接下来是一组字段,表示与玩家的悬停赛车相关联的相机和对象。第一个这样的字段是GameObject
实例gun
。该字段是对汽车的枪对象的引用。这个物体在游戏的战斗模式中出现。下一个字段是GameObject
实例gunBase
。这也是对一个游戏对象的引用,在这种情况下,gun
依赖的基础模型。接下来我们列出了一个特别重要的字段,camera
。这个摄像头安装在悬停赛车的驾驶舱内。在玩游戏时,它被用作主摄像头。
下一个字段是rearCamera
字段。这款相机被用作哈弗赛车的后视相机。接下来,我们有非常重要的car
字段。该字段引用代表比赛中玩家的游戏对象。他们的悬停赛车。该组中的下一个也是最后一个字段是代表汽车前视传感器的carSensor
游戏对象。我们已经在前面的高级交互课程复习第五章 ?? 中讨论过这个问题。让我们转到下一组类字段。
//***** Car Status Class Fields *****
public int ammo = 0;
public bool gunOn = false;
public bool isBouncing = false;
public bool isJumping = false;
public bool isDrafting = false;
public bool isShot = false;
public float isShotTime = 0.0f;
public bool isHit = false;
public float isHitTime = 0.0f;
public bool isMiss = false;
public float isMissTime = 0.0f;
public bool lapComplete = false;
public float lapCompleteTime = 0.0f;
public bool armorOn = false;
public bool boostOn = false;
public bool invincOn = false;
public float invincTime = 0.0f;
public int lifeTotal = DEFAULT_LIFE_AMOUNT;
public int damage = DEFAULT_DAMAGE_AMOUNT;
public int points = 0;
public bool alive = true;
Listing 9-7PlayerState Class Fields 3
这组类字段与悬停赛车的当前状态有关。第一个字段显示玩家拥有的弹药数量。下一个字段gunOn
用于指示枪修改器是否激活。接下来的三个字段(isBouncing
、isJumping
和isDrafting
)的使用方式与我们刚刚查看的gunOn
字段相同。该组中接下来的六个字段遵循我们之前看到的关于轨迹和航路点指示器的相同模式。
在每种情况下,都有一个布尔标志和一个时间跟踪字段,用于射击、命中和未命中事件。请注意,这些事件仅在游戏的战斗模式中可用。接下来的两个字段lapComplete
和lapCompleteTime
,遵循我们刚刚看到的完全相同的模式。下一个字段lapComplete
表示该圈已经完成,而下一个字段lapCompleteTime
记录该圈的持续时间。armorOn
和boostOn
字段是布尔标志,用于指示给定的修改器是活动的还是非活动的。invinvOn
和invincTime
字段遵循我们看到的相同模式,包括一个布尔标志和一个计时字段。
lifeTotal
栏显示玩家的总生命值。damage
字段是一个整数值,表示当前汽车受到的损坏量。游戏并没有主动使用points
区域,但是你可以在你认为合适的时候使用。类似地,布尔标志alive
被用来指示当前玩家是否还活着,但是它并没有被游戏主动使用。我们要看的下一组类字段是速度和航路点相关的字段。我把它们列在这里。
//***** Speed Class Fields *****
public int maxForwardSpeedSlow = 50;
public int maxSidewaysSpeedSlow = 12;
public int maxBackwardsSpeedSlow = 5;
public int maxGroundAccelerationSlow = 25;
public int maxForwardSpeedNorm = 200;
public int maxSidewaysSpeedNorm = 50;
public int maxBackwardsSpeedNorm = 20;
public int maxGroundAccelerationNorm = 100;
public int maxForwardSpeedBoost = 250;
public int maxSidewaysSpeedBoost = 60;
public int maxBackwardsSpeedBoost = 30;
public int maxGroundAccelerationBoost = 120;
//***** Waypoint Class Fields *****
public ArrayList waypoints = null;
public float waypointDistance = 0.0f;
public float waypointDistancePrev = 0.0f;
Listing 9-8PlayerState Class Fields 4
我们要查看的下一组类字段,如前面所列,包含两组字段。这些字段用于跟踪玩家的速度和路点交互。不同的速度场分为三类:慢速、正常和加速。每个类别有四个条目。关于慢速,我们有一个maxForwardSpeedSlow
字段,它保存一个用于正向慢速的值。在此之后,我们有慢速类别的横向和向后速度。这一类别的最后一个字段是地面加速速度慢字段。
“正常”和“加速”类别也有相同的条目。标准速度用于在轨运行。慢速用于偏离轨道的运动。最后,当汽车有一个激活的加速修改器时,加速速度用于运动。接下来的三个字段与航迹的航路点系统相关联。第一个条目是一个ArrayList
实例,用于保存对所有路点的引用。
以下两个字段用于跟踪汽车和下一个航路点之间的距离,waypointDistance
和waypointDistancePrev
。正如你可能已经猜到的,先前的航路点距离计算存储在waypointDistancePrev
字段中。这些计算被用作航路点计算和人工智能系统的一部分,这将我们带到下一组职业领域供我们回顾,人工智能领域。
//***** AI Class Fields Part 1 *****
public bool aiOn = false;
public int aiWaypointTime = 0;
public int aiWaypointLastIndex = -1;
public int aiWaypointIndex = 0; //0 = first node
public int aiWaypointRoute = 0;
public float aiTurnStrength = 1.0f;
public float aiSpeedStrength = 1.0f;
public float aiStrafeStrength = 0.0f;
public float aiSlide = 0.0f;
//0 = looking, 1 = testing, 2 = acting
public int aiIsStuckMode = 0;
public bool aiIsStuck = false;
public float aiWaypointDistance = 0f;
public Vector3 aiRelativePoint = Vector3.zero;
public float aiTime1 = 0.0f;
public float aiTime2 = 0.0f;
public float aiSlowDownTime = 0.0f;
public float aiSlowDown = 0.0f;
public bool aiSlowDownOn = false;
Listing 9-9PlayerState Class Fields 5
我们要复习的下一组字段是第一组 AI 类字段。游戏的人工智能系统使用这些字段来移动、计算人工智能控制的悬停赛车的移动或报告其状态。第一个条目aiOn
,是一个布尔标志,指示悬停赛车的 AI 模式是否开启。aiWaypointTime
字段保存一个整数表示,它是当前玩家的当前timeNum
字段的副本。接下来的两个字段,aiWaypointpointLastIndex
和aiWaypointIndex
,用于跟踪玩家之前和当前的航点索引。这些由玩家的悬停赛车触发,与航路点对象交互。我应该注意到,虽然我说的是“玩家的”,这也意味着一个 AI 对手玩家,而不仅仅是一个人类玩家。
aiWaypointRoute
字段用于加载指定路线的航路点。这个特性在游戏中实际上并没有使用;相反,路由总是被设置为零。如果您愿意,可以随意扩展这个功能。下一个字段aiTurnStrength
,目前游戏还没有实现,但是可以作为人工智能转向计算的一部分。下面的字段aiSpeedStrength
,被游戏的人工智能计算用来控制悬停赛车手的速度矢量。
类似地,aiStrafeStrength
字段用于控制 AI 控制的悬停赛车的计算速度矢量的扫射分量。aiSlide
字段为 AI 提供了一个用于速度计算的滑动组件。在处理“卡住”的悬停赛车时,我们会随着时间的推移进行一些检查,以表明赛车被卡住了。aiIsStuckMode
字段有助于跟踪正在执行的停滞检查。
最终,“停滞”计算的结果存储在aiIsStuck
字段中。接下来,我们有aiWaypointDistance
字段,用于在 AI 模式下跟踪到下一个航路点的距离。aiRelativePoint
字段用于确定当 AI 驾驶悬停赛车时应该使用什么aiStrafeStrength
。接下来的两个字段aiTime1
和aiTime2
用于跟踪时间间隔,例如在检查不同的aiIsStuckMode
时。该组中的最后两个字段是aiSlowDownTime
和aiSlowDown
字段。这些字段用于控制盘旋赛车在转弯时的速度。aiSlowDownTime
字段测量当前减速的持续时间。下一个字段aiSlowDown
是一个指示汽车应该减速多少的值。该值由轨迹的航路点标记决定。最后,aiSlowDownOn
字段用于指示减速修改器当前打开。我们还有几个人工智能领域要复习。我在这里列出下一套。
//***** AI Class Fields Part 2 *****
public float aiSlowDownDuration = 100.0f;
public bool aiIsPassing = false;
public float aiPassingTime = 0.0f;
public int aiPassingMode = 0;
public bool aiHasTarget = false;
public float aiHasTargetTime = 0.0f;
public bool aiIsReloading = false;
public float aiIsReloadingTime = 0.0f;
public bool aiHasGainedLife = false;
public float aiHasGainedLifeTime = 0.0f;
public bool aiIsLargeTurn = false;
public float aiIsLargeTurnSpeed = 0.0f;
public float aiLastLookAngle = 0.0f;
public float aiNextLookAngle = 0.0f;
public float aiNext2LookAngle = 0.0f;
public float aiMidLookAngle = 0.0f;
public float aiMid2LookAngle = 0.0f;
public bool aiCanFire = false;
public float aiBoostTime = 0.0f;
public int aiBoostMode = 0;
public int aiWaypointJumpCount = 0;
public int aiWaypointPassCount = 0;
Listing 9-10PlayerState Class Fields 6
我们已经讨论了一大堆课程领域,并且几乎就要完成了,所以坚持住。我们还有两组字段要复习。其余的 AI 字段已在前面列出。第一个条目把我们带回到减速领域。aiSlowDownDuration
字段用于跟踪悬停赛车的减速。这个值是在 AI 控制的车上由赛道的路点标记设定的。接下来的三个条目是与 AI 悬停赛车的通过修改器有关的字段。aiIsPassing
字段是一个布尔标志,表示 AI 汽车处于超车模式。然后,aiPassingTime
字段用于测量悬停赛车超过当前赛车的持续时间。与这两个字段相关的是设置传球尝试模式的aiPassingMode
字段。
接下来的两个变量遵循我们之前见过的模式。aiHasTarget
字段指示目标已经设置,而aiHasTargetTime
字段用于测量目标已经激活的持续时间。在这一对之后是另外两个布尔标志和持续时间字段集。看看他们,重装和生命增益。从人工智能控制的汽车的转弯角度可以检测到大转弯。角度决定了aiLargeTurnSpeed
字段的值。这种机制用于帮助控制汽车在大角度转弯时的速度。
查看接下来的五个字段。这些都是浮点值,旨在保持基于悬停赛车方向和到下一个航路点的距离之间的计算的角度。aiCanFire
布尔用于指示汽车是否能够开火。这只有在游戏的战斗模式变异中才有。下一对字段aiBoostMode
和aiBoostTime
,遵循与之前看到的布尔标志、持续时间字段集类似的结构。在这种情况下,模式决定行为,时间字段跟踪持续时间。aiWaypointPassCount
字段以一种有点愚蠢的方式跟踪经过的路点数量。它不能很好地跟踪重复和跳转,但是它可以用于根据其值的变化做出决定。还有一组剩余的类字段可供查看。
//***** Other Class Fields *****
public GameObject gunExplosion = null;
public GameObject gunHitSmoke = null;
public bool gunSmokeOn = false;
public float gunSmokeTime = 0.0f;
//public ParticleEmitter gunExplosionParticleSystem = null;
//public ParticleEmitter gunHitSmokeParticleSystem = null;
public GameObject flame = null;
public int totalLaps = 3;
public int currentLap = 0;
public bool prepped = false;
public GameObject lightHeadLight = null;
public AudioListener audioListener = null;
public AudioSource audioGunHit = null;
public AudioSource audioCarSound1 = null;
public AudioSource audioCarSound2 = null;
Listing 9-11PlayerState Class Fields 7
我们要查看的最后一组类字段是“其他”类字段。gunExplosion
和gunHitSmoke
字段是游戏对象引用,指向玩家的悬停赛车中的对象,特别是汽车的统一层次GameObjects
。接下来的两个字段应该看起来很熟悉。它们遵循我们之前见过的相同模式。这一对场,gunSmokeOn
和gunSmokeTime
,用来控制枪械的烟雾效果。该功能目前在游戏中被禁用。我会解释的。这个特性是以一种遗留的方式实现的,但现在已经过时了。我们保留了为它提供动力的代码,但是注释掉了,这样你就可以用它来为游戏和支持代码添加新的最新粒子效果。下一个字段是引用了 hover racer 模型结构的一部分的GameObject flame
。totalLaps
字段的值表示当前比赛的圈数。
类似地,currentLap
字段指示当前玩家在第几圈。布尔标志prepped
用于表示玩家已经准备好,可以出发了。正如你可能已经想到的,lightHeadLight
场是用来打开或关闭汽车的头灯。这组字段和字段查看部分是玩家的悬停赛车使用的音频监听器和声音资源字段。祝贺您,您已经完成了本复习部分的学习。在下一节中,我们将看看这个类的相关方法大纲。
相关的方法大纲/类头:PlayerState
PlayerState
类让我们复习的方法很少。我把它们列在这里。
//Main Methods
void Start();
public bool PerformGunShotHit();
public Vector3 UpdateAiFpsController();
public void MoveToCurrentWaypoint();
public void MoveToWaypoint(int index);
public void UpdateAiMouseLook();
public void Update();
public void Reset();
//Support Methods
public void LoadAudio();
public void PauseSound();
public void UnPauseSound();
public bool IsValidWaypointIndex(int index);
public void StampWaypointTime();
public void PlayGunShotHitAudio();
public WaypointCheck GetCurrentWaypointCheck();
public void PerformLookAt(WaypointCheck wc);
public void ResetTime();
public int GetLife();
public int GetLifeHUD();
public int GetLapsLeft();
public void SetDraftingBonusOn();
public void SetDraftingBonusOff();
public void SetBoostOn();
public void SetBoostOff();
public void SetCurrentSpeed();
public void ShowInvinc();
public void HideInvinc();
public void ShowGun();
public void HideGun();
public void SetSlow();
public void SetNorm();
public void SetBoost();
private int GetPastWaypointIndex(int wpIdx);
Listing 9-12PlayerState Pertinent Method Outline/Class Headers 1
当然有很多支持方法需要我们回顾,但是由于大多数方法都非常简单和直接,我们将很快完成它们。
using System.Collections;
using UnityEngine;
public class PlayerState : BaseScript {}
Listing 9-13PlayerState Pertinent Method Outline/Class Headers 2
注意,PlayerState
类扩展了BaseScript
类,继承了一些我们在之前的类回顾中看到的标准功能。在下一个复习部分,我们将看看类的支持方法。
支持方法详细信息:PlayerState
PlayerState
类有许多支持方法供我们研究。这些方法中有几个非常简单,所以我在这里列出来,但是由于它们的简单性,我们不会对它们做深入的回顾。请花时间仔细阅读。除非你了解这些方法的作用和使用方法,否则不要继续。
01 public void PauseSound() {
02 if (audioCarSound1 != null) {
03 audioCarSound1.Stop();
04 }
05
06 if (audioCarSound2 != null) {
07 audioCarSound2.Stop();
08 }
09 }
01 public void UnPauseSound() {
02 if (audioCarSound1 != null) {
03 audioCarSound1.Play();
04 }
05
06 if (audioCarSound2 != null) {
07 audioCarSound2.Play();
08 }
09 }
01 public bool IsValidWaypointIndex(int index) {
02 if (waypoints == null) {
03 waypoints = gameState.GetWaypoints(aiWaypointRoute);
04 }
05
06 if (waypoints != null && index >= 0 && index <= (waypoints.Count - 1)) {
07 return true;
08 } else {
09 return false;
10 }
11 }
01 public void StampWaypointTime() {
02 aiWaypointTime = timeNum;
03 }
01 public void PlayGunShotHitAudio() {
02 if (audioGunHit != null) {
03 if (audioGunHit.isPlaying == false) {
04 audioGunHit.Play();
05 }
06 }
07 }
01 public WaypointCheck GetCurrentWaypointCheck() {
02 if (waypoints != null) {
03 return (WaypointCheck)waypoints[aiWaypointIndex];
04 } else {
05 return null;
06 }
07 }
01 public void PerformLookAt(WaypointCheck wc) {
02 wcVpla = wc.transform.position;
03 wcVpla.y = player.transform.position.y;
04 player.transform.LookAt(wcVpla);
05 }
01 public void ResetTime() {
02 totalTime = 0f;
03 }
01 public int GetLife() {
02 return (lifeTotal - damage);
03 }
01 public int GetLifeHUD() {
02 return (lifeTotal - damage);
03 }
01 public int GetLapsLeft() {
02 return (totalLaps - currentLap);
03 }
01 public void SetDraftingBonusOn() {
02 isDrafting = true;
03 SetCurrentSpeed();
04 }
01 public void SetDraftingBonusOff() {
02 isDrafting = false;
03 SetCurrentSpeed();
04 }
01 public void SetBoostOn() {
02 boostOn = true;
03 SetBoost();
04 }
01 public void SetBoostOff() {
02 boostOn = false;
03 SetNorm();
04 }
01 public void SetCurrentSpeed() {
02 if (speedType == 0) {
03 SetSlow();
04 } else if (speedType == 1) {
05 SetNorm();
06 } else if (speedType == 2) {
07 SetBoost();
08 }
09 }
01 public void ShowInvinc() {
02 invincOn = true;
03 invincTime = 0f;
04 }
01 public void HideInvinc() {
02 invincOn = false;
03 invincTime = 0f;
04 }
01 public void ShowGun() {
02 gunOn = true;
03 if (gun != null) {
04 gun.SetActive(true);
05 }
06
07 if (gunBase != null) {
08 gunBase.SetActive(true);
09 }
10 }
01 public void HideGun() {
02 gunOn = false;
03 if (gun != null) {
04 gun.SetActive(false);
05 }
06
07 if (gunBase != null) {
08 gunBase.SetActive(false);
09 }
10 }
Listing 9-14PlayerState Support Method Details 1
这些支持方法本质上很简单。大多数只有几行代码。我不会在这里详细介绍它们。请通读一遍,确保你理解了它们,并且在你继续下一步之前,它们对你有意义。我们将继续讨论更复杂的支持方法。让我们来看看!
01 public void LoadAudio() {
02 audioSetLa = player.GetComponents<AudioSource>();
03 if (audioSetLa != null) {
04 lLa = audioSetLa.Length;
05 for (iLa = 0; iLa < lLa; iLa++) {
06 aSLa = (AudioSource)audioSetLa[iLa];
07 if (aSLa != null) {
08 if (aSLa.clip.name == AUDIO_CLIP_GUN_SHOT) {
09 audioGunHit = aSLa;
10 } else if (aSLa.clip.name == AUDIO_CLIP_CAR_SOUND1) {
11 audioCarSound1 = aSLa;
12 } else if (aSLa.clip.name == AUDIO_CLIP_CAR_SOUND2) {
13 audioCarSound2 = aSLa;
14 }
15 }
16 }
17 }
18 }
01 public void SetSlow() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (cm == null) {
07 return;
08 }
09
10 speedType = 0;
11 cm.movement.maxForwardSpeed = maxForwardSpeedSlow;
12 if (isDrafting == true) {
13 cm.movement.maxForwardSpeed += MAX_SPEED_BONUS_DRAFTING;
14 }
15
16 if (aiIsPassing == true) {
17 cm.movement.maxForwardSpeed += MAX_SPEED_BONUS_PASSING;
18 }
19 cm.movement.maxSidewaysSpeed = maxSidewaysSpeedSlow;
20 cm.movement.maxBackwardsSpeed = maxBackwardsSpeedSlow;
21 cm.movement.maxGroundAcceleration = maxGroundAccelerationSlow;
22 }
01 public void SetNorm() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (cm == null) {
07 return;
08 }
09
10 speedType = 1;
11 cm.movement.maxForwardSpeed = maxForwardSpeedNorm;
12 if (isDrafting == true) {
13 cm.movement.maxForwardSpeed += MAX_SPEED_BONUS_DRAFTING;
14 }
15
16 if (aiIsPassing == true) {
17 cm.movement.maxForwardSpeed += MAX_SPEED_BONUS_PASSING;
18 }
19 cm.movement.maxSidewaysSpeed = maxSidewaysSpeedNorm;
20 cm.movement.maxBackwardsSpeed = maxBackwardsSpeedNorm;
21 cm.movement.maxGroundAcceleration = maxGroundAccelerationNorm;
22 }
01 public void SetBoost() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (cm == null) {
07 return;
08 }
09
10 speedType = 2;
11 cm.movement.maxForwardSpeed = maxForwardSpeedBoost;
12 if (isDrafting == true) {
13 cm.movement.maxForwardSpeed += MAX_SPEED_BONUS_DRAFTING;
14 }
15
16 if (aiIsPassing == true) {
17 cm.movement.maxForwardSpeed += MAX_SPEED_BONUS_PASSING;
18 }
19 cm.movement.maxSidewaysSpeed = maxSidewaysSpeedBoost;
20 cm.movement.maxBackwardsSpeed = maxBackwardsSpeedBoost;
21 cm.movement.maxGroundAcceleration = maxGroundAccelerationBoost;
22 }
01 private int GetPastWaypointIndex(int wpIdx) {
02 if (wpIdx - 5 >= 0) {
03 wpIdx -= 5;
04 } else if (wpIdx - 4 >= 0) {
05 wpIdx -= 4;
06 } else if (wpIdx - 3 >= 0) {
07 wpIdx -= 3;
08 } else if (wpIdx - 2 >= 0) {
09 wpIdx -= 2;
10 } else if (wpIdx - 1 >= 0) {
11 wpIdx -= 1;
12 } else {
13 wpIdx = 0;
14 }
15 return wpIdx;
16 }
Listing 9-15PlayerState Support Method Details 2
前面列出的第一种更复杂的支持方法是LoadAudio
方法。此方法用于加载音频资源,以用作悬停赛车的某些声音效果。在该方法的第 2 行,我们获得了一个附加到 player 对象的AudioSource
组件列表。在生成的数组中循环,我们寻找三个特定的声音文件,并将引用存储在类字段中,第 9、11 和 13 行。
前面列出的下面三种方法用于更新当前玩家汽车的速度。第一个条目SetSlow
,用于在 hover racer 脱离赛道时将其设置为慢速。第 2–4 行非常熟悉的代码(或者应该是这样的代码)会阻止这个方法在类没有正确配置的情况下执行任何工作。在第 6–8 行,如果字符运动字段cm
未定义,我们退出该方法。
接下来,用第 10 行上代表慢速、正常或加速速度的值更新speedType
字段。计算新的前进速度时考虑了 11–18 行上的牵引和通过。在该方法的最后,在第 19–21 行更新了悬停赛车的其余速度相关字段。我在SetSlow
方法后面列出了SetNorm
和SetBoost
方法。这些方法与SetSlow
方法几乎相同,所以我们在这里不再赘述。相反,我们会把他们的评论留给你。请确保您在继续之前了解该方法。
本节列出的最后一种方法是GetPastWaypointIndex
方法。这个方法负责寻找过去的路点。它试图在玩家当前航点索引后面五个索引处找到一个有效的航点。如果所确定的索引值无效,则检查玩家当前航点索引后面四个索引的航点,依此类推。这就是本复习部分的结论。接下来,我们将看看这个类的主要方法。
主要方法细节:PlayerState
PlayerState
类有几个主要的方法,负责配置和更新类字段。让我们来看看第一组主要的方法。
01 void Start() {
02 base.Prep(this.GetType().Name);
03 if (BaseScript.IsActive(scriptName) == false) {
04 Utilities.wrForce(scriptName + ": Is Deactivating...");
05 return;
06 }
07 }
01 public bool PerformGunShotHit() {
02 if (armorOn == true) {
03 armorOn = false;
04 isShot = true;
05 gunSmokeOn = true;
06 gunSmokeTime = 0.0f;
07 gunHitSmoke.SetActive(true);
08 //gunHitSmokeParticleSystem.Emit();
09 return true;
10 } else {
11 if (invincOn == true) {
12 return false;
13 } else {
14 damage++;
15 isShot = true;
16 gunSmokeOn = true;
17 gunSmokeTime = 0.0f;
18 gunHitSmoke.SetActive(true);
19 //gunHitSmokeParticleSystem.Emit();
20 PlayGunShotHitAudio();
21
22 if (GetLife() <= 0) {
23 aiWaypointIndex = GetPastWaypointIndex(aiWaypointIndex);
24 damage = 0;
25 if (aiWaypointIndex >= 0 && aiWaypointIndex < waypoints.Count) {
26 MoveToCurrentWaypoint();
27 }
28 }
29 return true;
30 }
31 }
32 }
01 public void MoveToWaypoint(int index) {
02 aiWaypointIndex = index;
03 MoveToCurrentWaypoint();
04 }
01 public void MoveToCurrentWaypoint() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 pause = true;
07 WaypointCheck wc = (WaypointCheck)waypoints[aiWaypointIndex];
08 Vector3 wcV = wc.transform.position;
09 wcV.y = wc.waypointStartY;
10
11 cm.movement.velocity = Vector3.zero;
12 player.transform.position = wcV;
13 isDrafting = false;
14 isJumping = false;
15 isBouncing = false;
16 SetNorm();
17 ShowInvinc();
18
19 if (aiWaypointIndex + 1 >= 0 && aiWaypointIndex + 1 < waypoints.Count) {
20 wc = (WaypointCheck)waypoints[aiWaypointIndex + 1];
21 }
22 aiWaypointJumpCount++;
23 PerformLookAt(wc);
24 pause = false;
25 }
Listing 9-16PlayerState Main Method Details 1
我们要看的第一个主要方法是Start
方法。这个方法的实现遵循我们以前见过很多次的标准过程。对Prep
方法的调用加载了一组标准变量,然后进行测试以查看该类是否正确初始化。下面列出的方法,PerformGunShotHit
,用于对当前玩家应用射击命中结果。
第 2-9 行的第一个代码块处理当前玩家有主动护甲调整值时的命中。请注意,装甲修改器被设置为假,isShot
标志被设置为真,一些效果字段被重置为显示一股烟的枪击。我应该再次提到,这些粒子效果已经被禁用,留给你来实现。在第 11 行到第 30 行的下一大块代码中,该方法处理没有装甲修改器激活时的射击命中。
这个大代码块的第一部分,第 11-13 行,处理当前玩家的无敌属性设置为真时的枪击事件。这段代码的第二部分从第 14 行到第 29 行。这段代码处理枪击事件。当前玩家的伤害和isShot
旗在 14-15 行调整。枪击效果在第 16-19 行准备。在第 20 行,播放音频声音效果,表示有效的、破坏性的击打。
如果玩家没有更多的生命点,第 22 行,那么玩家在赛道上被重置,与击中水障碍的玩家被重置的方式相同。这在第 23–27 行处理。这个过程的第一步是找到一个先前的路点让玩家返回,作为受到致命一击的惩罚。我应该提一下,这个游戏的这些特性只有在战斗模式下才有。
对GetPastWaypointIndex
方法的调用决定了我们可以将当前玩家向后移动多远。在第 24 行,当前玩家的伤害被设置为零,hover racer 通过调用第 26 行的MoveToCurrentWaypoint
方法被重新定位。列出的下一个方法MoveToWaypoint
,是方法集中第一个玩家重新定位的方法。这是一个传递方法,它更新第 2 行的aiWaypointIndex
字段,然后通过调用MoveToCurrentWaypoint
方法来移动播放器。我们现在来看看这个方法。
列出的第二个玩家重新定位方法MoveToCurrentWaypoint
,实际上是移动玩家的悬停赛车。如果类没有正确配置,前几行代码(2–4)会阻止该方法执行任何工作。第 6–24 行的代码负责重新定位播放器。第一,玩家暂停,第 6 行;然后新玩家的位置由当前航点和第 9 行航点对象的waypointStartY
字段的值决定。
第 11–17 行的代码设置玩家的速度、位置和修改器值。注意玩家在第 17 行收到无敌修正值。我们需要弄清楚给定玩家的新位置,玩家应该面向哪个方向。为此,我们找到第 19–21 行确定的下一个航路点。玩家的跳跃计数字段增加,玩家的方向在第 22-23 行调整。最后但同样重要的是,玩家在第 24 行没有暂停。在下一组要回顾的主要方法中,我们将看看Update
和Reset
方法。
001 public void Update() {
002 if (BaseScript.IsActive(scriptName) == false) {
003 return;
004 }
005
006 if (prepped == false || cm == null) {
007 return;
008 } else if (hasSetHome == false && player != null) {
009 home = player.transform;
010 hasSetHome = true;
011 }
012
013 //speed calculations
014 speed = cm.movement.velocity.magnitude;
015 if (boostOn == true || aiIsPassing == true) {
016 speed = LIMIT_MAX_SPEED;
017 }
018 speedPrct = (speed / maxSpeed);
019 speedPrctLimit = (speed / LIMIT_MAX_SPEED);
020
021 position = gameState.GetPosition(index, position);
022
023 //timing values
024 totalTime += Time.deltaTime;
025 ms = Mathf.RoundToInt((totalTime % 1) * 1000);
026 s = Mathf.RoundToInt(Mathf.Floor(totalTime));
027 min = Mathf.RoundToInt(Mathf.Floor((s * 1f) / 60f));
028 s -= (min * 60f);
029 hour = Mathf.RoundToInt(Mathf.Floor((min * 1f) / 60f));
030 min -= (hour * 60f);
031 time = string.Format("{0:00}:{1:00}:{2:000}", min, s, ms);
032 timeNum = int.Parse(string.Format("{0:00}{1:00}{2:00}{3:000}", hour, min, s, ms));
033
034 //waypoint distance calculations
035 if (waypoints != null && waypoints.Count > 0) {
036 wc = (WaypointCheck)waypoints[aiWaypointIndex];
037 if (wc != null) {
038 wcV = wc.transform.position;
039 wcV.y = player.transform.position.y;
040 waypointDistancePrev = waypointDistance;
041 waypointDistance = Vector3.Distance(wcV, player.transform.position);
042 }
043 }
044
045 //invincibility modifier
046 if (invincOn == true) {
047 invincTime += Time.deltaTime;
048 } else {
049 invincTime = 0f;
050 }
051
052 if (invincOn == true && invincTime >= INVINC_SECONDS) {
053 invincOn = false;
054 }
055
056 //has gained life
057 if (aiHasGainedLife == true) {
058 aiHasGainedLifeTime += Time.deltaTime;
059 } else {
060 aiHasGainedLifeTime = 0f;
061 }
062
063 if (aiHasGainedLife == true && aiHasGainedLifeTime >= MAX_GAINED_LIFE_TIME) {
064 aiHasGainedLife = false;
065 }
066
067 //gun smoke effect
068 if (gunSmokeOn == true) {
069 gunSmokeTime += Time.deltaTime * 100f;
070 } else {
071 gunSmokeTime = 0f;
072 }
073
074 if (gunSmokeOn == true && gunSmokeTime >= MAX_SMOKE_TIME) {
075 gunSmokeOn = false;
076 gunSmokeTime = 0f;
077 gunHitSmoke.SetActive(false);
078 //gunHitSmokeParticleSystem.emit = false;
079 }
080
081 //is shot time
082 if (isShot == true) {
083 isShotTime += Time.deltaTime;
084 } else {
085 isShotTime = 0f;
086 }
087
088 if (isShot == true && isShotTime >= MAX_IS_SHOT_TIME) {
089 isShot = false;
090 }
091
092 //is hit time
093 if (isHit == true) {
094 isHitTime += Time.deltaTime;
095 } else {
096 isHitTime = 0f;
097 }
098
099 if (isHit == true && isHitTime >= MAX_IS_SHOT_TIME) {
100 isHit = false;
101 }
102
103 //is miss time
104 if (isMiss == true) {
105 isMissTime += Time.deltaTime;
106 } else {
107 isMissTime = 0f;
108 }
109
110 if (isMiss == true && isMissTime >= MAX_IS_SHOT_TIME) {
111 isMiss = false;
112 }
113
114 //lap complete time
115 if (lapComplete == true) {
116 lapCompleteTime += Time.deltaTime;
117 } else {
118 lapCompleteTime = 0f;
119 }
120
121 if (lapComplete == true && lapCompleteTime >= MAX_IS_LAP_COMPLETE_TIME) {
122 lapComplete = false;
123 }
124
125 //off track checks
126 if (offTrack == true) {
127 offTrackTime += Time.deltaTime;
128 } else {
129 offTrackTime = 0f;
130 }
131
132 if (offTrack == true && offTrackTime >= offTrackSeconds) {
133 if (waypoints != null && waypoints.Count > 0) {
134 //move car to waypoint center
135 aiWaypointIndex = GetPastWaypointIndex(aiWaypointIndex);
136 if (aiWaypointIndex >= 0 && aiWaypointIndex < waypoints.Count) {
137 MoveToCurrentWaypoint();
138 }
139 offTrack = false;
140 offTrackTime = 0f;
141 }
142 }
143
144 //wrong direction checks
145 if (wrongDirection == true) {
146 wrongDirectionTime += Time.deltaTime;
147 } else {
148 wrongDirectionTime = 0f;
149 }
150
151 if (wrongDirection == true && wrongDirectionTime >= wrongDirectionSeconds) {
152 if (waypoints != null && waypoints.Count > 0) {
153 //move car to waypoint center
154 aiWaypointIndex = GetPastWaypointIndex(aiWaypointIndex);
155 if (aiWaypointIndex >= 0 && aiWaypointIndex < waypoints.Count) {
156 MoveToCurrentWaypoint();
157 }
158 wrongDirection = false;
159 wrongDirectionTime = 0;
160 }
161 }
162 }
001 public void Reset() {
002 totalTime = 0f;
003 min = 0f;
004 s = 0f;
005 ms = 0f;
006 hour = 0f;
007 ammo = 0;
008 damage = 0;
009 points = 0;
010
011 boostOn = false;
012 invincOn = false;
013 invincTime = 0.0f;
014 gunOn = false;
015 armorOn = false;
016 offTrack = true;
017 gunSmokeOn = false;
018 gunSmokeTime = 0f;
019
020 prepped = false;
021 offTrack = false;
022 offTrackTime = 0.0f;
023 wrongDirection = false;
024 wrongDirectionTime = 0.0f;
025 skippedWaypoint = false;
026 skippedWaypointTime = 0.0f;
027 position = 6;
028 currentLap = 0;
029 waypointDistance = 0.0f;
030 waypointDistancePrev = 0.0f;
031 alive = true;
032
033 isBouncing = false;
034 isJumping = false;
035 isDrafting = false;
036 isShot = false;
037 isShotTime = 0.0f;
038 isHit = false;
039 isHitTime = 0.0f;
040 isMiss = false;
041 isMissTime = 0.0f;
042
043 aiIsStuck = false;
044 aiIsPassing = false;
045 aiPassingTime = 0.0f;
046 aiPassingMode = 0;
047 aiHasTarget = false;
048 aiHasTargetTime = 0.0f;
049 aiIsReloading = false;
050 aiIsReloadingTime = 0.0f;
051
052 aiIsLargeTurn = false;
053 aiIsLargeTurnSpeed = 0.0f;
054 aiCanFire = false
;
055 aiBoostTime = 0.0f;
056 aiBoostMode = 0;
057 aiWaypointTime = 0;
058 aiWaypointLastIndex = -1;
059 aiWaypointIndex = 0;
060 aiWaypointJumpCount = 0;
061 aiWaypointPassCount = 0;
062 }
Listing 9-17PlayerState Main Method Details 2
前面列出的下一组主要方法,有两个重要的方法,我们现在来看看。我们要研究的第一个方法是非常重要的Update
方法。这个方法为我们处理一些不同的职责。它负责计算当前速度、到航路点的距离,并跟踪不同修改器的持续时间。值得注意的是PlayerState
类为人类和人工智能控制的玩家做着同样的工作。
Update
方法的主要目的是跟踪悬停赛车的状态,并监控不同修改器所涉及的计时持续时间。修改器通过悬停赛车与其他汽车的交互以及跳跃和加速标记等轨迹功能来设置。注意,这个类不插入或驱动游戏的 HUD。这个过程由GameState
类处理,我们马上就会看到。现在让我们来看一下Update
方法的细节。
如果类没有正确配置,前几行代码保护方法不被执行,第 2-4 行。接下来,在第 6–7 行,如果该类没有被标记为已正确初始化,或者没有正确定义字符马达字段cm
,则该方法返回。第 8–11 行的代码根据初始位置设置悬停赛车的初始位置。速度计算在第 14-19 行进行。
悬停赛车的速度被设置为等于汽车速度矢量的大小。如果汽车处于加速模式或者aiIsPassing
标志为真,那么汽车的速度被设置为LIMIT_MAX_SPEED
值。在第 18–19 行,速度百分比值被更新。第 24 行增加了总单圈时间,第 21 行更新了赛车在比赛中的当前位置。Update
方法中的下一个代码块处理分段计时值,第 25–32 行。毫秒、秒和分钟由totalTime
字段决定。请注意,在第 28 行,我们减去了所有可以用分钟表示的秒。在第 30 行执行类似的计算,减去所有可以用小时表示的分钟。
第 31 和 32 行更新了两个重要的类字段time
和timeNum
。time
字段是当前圈速的字符串表示。timeNum
字段是一个特殊的编码,它将当前的圈速保存为一个整数。下一个代码块是第 35–43 行的航路点距离计算。此代码负责获取当前航路点的中心位置和推荐的 Y 位置。
当前和先前的航路点距离设置在第 40-41 行。虽然人类玩家不使用这些值,但人工智能玩家使用它们来控制悬停赛车。在这段代码之后是无敌修改代码,第 46–54 行。这段代码遵循一个简单的模式,我们将在这个方法中多次看到。如果无敌修改器是激活的,第 46 行,那么我们增加修改器的计时值,第 47 行。如果不是,修改量时间被设置为零,第 49 行。第 52–54 行的最后一位代码重置了无敌修改器,如果它的活动时间超过了指定的时间。
下一个代码块,第 57–65 行,即“已经获得生命”部分,其功能与我们刚刚查看的代码相同。看一看它,并确保在继续之前理解它。包括这一段代码,剩下的代码块都很相似,你应该自己快速复习一下。代码块如下所示:
-
枪烟效果:68–79
-
拍摄时间:82–90 分钟
-
击中时间:93–101
-
错过时间是:104–112
-
单圈完成时间:115–123
代码非常直接。你会毫不费力地跟上它。尽管如此,还是要花点时间仔细看看。这种方法剩下的两个责任是偏离轨道检查和错误方向检查。这两个代码块遵循相同的模式,所以我将首先检查一个代码块,并将第二个代码块留给您来检查。请注意第 126–142 行代码中的“非跟踪检查”部分。第 126–130 行的 if 语句遵循我们之前见过的相同模式。如果布尔标志为真,我们递增偏离轨道时间;否则,我们将其值设置为零。
在第 132 行,如果 off-track 标志设置为 true,并且我们已经到达了offTrackSeconds
时间,那么我们必须调整 hover racer 的位置,因为我们已经离开了赛道。如果有要处理的路点,第 133 行,那么我们将汽车移动到由第 135 行的方法调用确定的路点。如果确定的索引是有效的,我们移动汽车并重置偏离轨道标志和计时字段。
Update
方法中的最后一段代码“错误方向检查”非常接近我们刚刚检查过的代码,所以我将把它留给您来检查。这组主方法中的最后一个方法是Reset
方法。该方法只是将类字段重置为默认值。关于这个没什么好讨论的了。快速回顾一下,让我们继续研究剩下的最后两个主要方法,人工智能控制方法。
001 public Vector3 UpdateAiFpsController() {
002 if (player == null || prepped == false || cm == null) {
003 return Vector3.zero;
004 }
005
006 if (waypoints == null) {
007 waypoints = gameState.GetWaypoints(aiWaypointRoute);
008 }
009
010 if (waypoints == null) {
011 return Vector3.zero;
012 }
013
014 //calculate strafe strength
015 aiStrafeStrength = 0.0f;
016 aiSpeedStrength = 1.0f;
017
018 if (waypoints != null) {
019 fpsWc = (WaypointCheck)waypoints[aiWaypointIndex];
020 aiRelativePoint = player.transform.InverseTransformPoint(fpsWc.transform.position);
021
022 if (aiRelativePoint.x <= -30.0f) {
023 aiStrafeStrength = -0.30f;
024 } else if (aiRelativePoint.x >= 30.0f) {
025 aiStrafeStrength = 0.30f;
026 } else if (aiRelativePoint.x <= -20.0f) {
027 aiStrafeStrength = -0.20f;
028 } else if (aiRelativePoint.x >= 20.0f) {
029 aiStrafeStrength = 0.20f;
030 } else if (aiRelativePoint.x <= -15.0f) {
031 aiStrafeStrength = -0.15f;
032 } else if (aiRelativePoint.x >= 15.0f) {
033 aiStrafeStrength = 0.15f;
034 } else if (aiRelativePoint.x <= -10.0f) {
035 aiStrafeStrength = -0.10f;
036 } else if (aiRelativePoint.x >= 10.0f) {
037 aiStrafeStrength = 0.10f;
038 } else if (aiRelativePoint.x <= -5.0f) {
039 aiStrafeStrength = -0.05f;
040 } else if (aiRelativePoint.x >= 5.0f) {
041 aiStrafeStrength = 0.05f;
042 } else if (aiRelativePoint.x <= -1.0f) {
043 aiStrafeStrength = -0.01f;
044 } else if (aiRelativePoint.x >= 1.0f) {
045 aiStrafeStrength = 0.01f;
046 }
047 }
048
049 //calculate side, above, collisions
050 sidesUfp = (int)(cm.movement.collisionFlags & CollisionFlags.Sides);
051 aboveUfp = (int)(cm.movement.collisionFlags & CollisionFlags.Above);
052
053 if (sidesUfp == 0) {
054 collSidesUfp = false;
055 } else {
056 collSidesUfp = true;
057 }
058
059 if (aboveUfp == 0) {
060 collAboveUfp = false;
061 } else {
062 collAboveUfp = true;
063 }
064
065 //calculate is stuck data
066 if (aiTime2 > 1 && cm.movement.collisionFlags == CollisionFlags.None) {
067 aiTime2 = 0;
068 aiIsStuckMode = 0;
069 aiTime1 = 0;
070 aiIsStuck = false;
071 } else if (aiTime2 > 1 && Mathf.Abs(waypointDistance - aiWaypointDistance) > MIN_STUCK_DISTANCE && !(collAboveUfp || collSidesUfp)) {
072 aiTime2 = 0;
073 aiIsStuckMode = 0;
074 aiTime1 = 0;
075 aiIsStuck = false;
076 } else if (collAboveUfp || collSidesUfp) {
077 aiTime2 = 0;
078 aiIsStuckMode = 1;
079 aiWaypointDistance = waypointDistance;
080 aiIsStuck = true;
081 }
082
083 //test and apply is stuck data
084 if (aiIsStuckMode == 1 && aiTime1 >= MIN_STUCK_TIME && cm.movement.velocity.magnitude <= 30 && Mathf.Abs(waypointDistance - aiWaypointDistance) <= MIN_STUCK_DISTANCE) {
085 aiIsStuckMode = 2;
086 aiTime2 = 0f;
087 aiTime1 = 0f;
088 aiIsStuck = true;
089 } else if (aiIsStuckMode == 1 && aiTime1 > MIN_STUCK_TIME) {
090 aiIsStuckMode = 0;
091 aiTime2 = 0f;
092 aiTime1 = 0f;
093 aiIsStuck = false;
094 }
095
096 //process aiIsStuckMode
097 if (aiIsStuckMode == 1) {
098 aiTime1 += Time.deltaTime;
099 } else if (aiIsStuckMode == 2) {
100 if (waypoints != null && waypoints.Count > 0) {
101 //move car to waypoint center
102 aiWaypointIndex = GetPastWaypointIndex(aiWaypointIndex);
103 if (!(aiWaypointIndex >= 0 && aiWaypointIndex < waypoints.Count)) {
104 fpsV = new Vector3(0, 0, 0);
105 return fpsV;
106 }
107 MoveToCurrentWaypoint();
108 aiIsStuckMode = 0;
109 aiIsStuck = false;
110 aiTime2 = 0f;
111 aiTime1 = 0f;
112 aiStrafeStrength = 0f;
113 }
114
115 fpsV = new Vector3(0, 0, 0);
116 return fpsV;
117 }
118
119 if (aiIsStuckMode != 0) {
120 aiTime2 += Time.deltaTime;
121 }
122
123 //apply waypoint slow down
124 if ((aiSlowDownOn == true && aiSlowDown < 1.0f && speedPrct > 0.3f) || (aiSlowDown >= 1.0f)) {
125 aiSlowDownTime += (Time.deltaTime * 100);
126 aiSpeedStrength = aiSlowDown;
127 if (aiSlowDownTime > aiSlowDownDuration) {
128 aiSlowDownOn = false;
129 aiSlowDownTime = 0.0f;
130 }
131 }
132
133 //handle large turn
134 if (aiIsLargeTurn == true) {
135 if (aiSpeedStrength > aiIsLargeTurnSpeed) {
136 aiSpeedStrength = aiIsLargeTurnSpeed;
137 }
138 }
139
140 fpsV = new Vector3(aiStrafeStrength, 0, aiSpeedStrength);
141 return fpsV;
142 }
001 public void UpdateAiMouseLook() {
002 if (BaseScript.IsActive(scriptName) == false) {
003 return;
004 }
005
006 if (waypoints == null) {
007 waypoints = gameState.GetWaypoints(aiWaypointRoute);
008 }
009
010 if (waypoints == null || player == null || prepped == false || !(aiWaypointIndex >= 0 && aiWaypointIndex < waypoints.Count)) {
011 return;
012 }
013
014 wc1 = (WaypointCheck)waypoints[aiWaypointIndex];
015 if (SHOW_AI_LOGIC) {
016 Debug.DrawRay(player.transform.position, (wc1.transform.position - player.transform.position), Color.green);
017 }
018
019 umlA = 0.0f;
020 umlForward = (player.transform.TransformDirection(Vector3.forward) * 20);
021
022 if (SHOW_AI_LOGIC) {
023 Debug.DrawRay(player.transform.position, umlForward, Color.magenta);
024 }
025
026 if (waypointDistance >= MIN_WAYPOINT_DISTANCE) {
027 wcV1 = wc1.transform.position;
028 wcV1.y = player.transform.position.y;
029 umlA = Vector3.Angle(umlForward, (wcV1 - player.transform.position));
030 aiLastLookAngle = umlA;
031
032 umlTmpIdx = 0;
033 if (aiWaypointIndex + 1 >= 0 && aiWaypointIndex + 1 < waypoints.Count) {
034 umlTmpIdx = (aiWaypointIndex + 1);
035 } else {
036 umlTmpIdx = 0;
037 }
038
039 wc2 = (WaypointCheck)waypoints[umlTmpIdx];
040 wcV2 = wc2.transform.position;
041 wcV2.y = player.transform.position.y;
042 umlA = Vector3.Angle(umlForward, (wcV2 - player.transform.position));
043 aiNextLookAngle = umlA;
044
045 if (SHOW_AI_LOGIC) {
046 Debug.DrawRay(player.transform.position, (wc2.transform.position - player.transform.position), Color.green);
047 }
048
049 umlTmpIdx = 0;
050 if (aiWaypointIndex + 2 >= 0 && aiWaypointIndex + 2 < waypoints.Count) {
051 umlTmpIdx = (aiWaypointIndex + 2);
052 } else {
053 umlTmpIdx = 0;
054 }
055
056 wc5 = (WaypointCheck)waypoints[umlTmpIdx];
057 wcV5 = wc5.transform.position;
058 wcV5.y = player.transform.position.y;
059 umlA = Vector3.Angle(umlForward, (wcV5 - player.transform.position));
060 aiNext2LookAngle = umlA;
061
062 if (SHOW_AI_LOGIC) {
063 Debug.DrawRay(player.transform.position, (wc5.transform.position - player.transform.position), Color.green);
064 }
065
066 if (speedPrct > 0.2f) {
067 umlAngle = Mathf.Abs(aiNextLookAngle);
068
069 if (umlAngle > 80) {
070 aiIsLargeTurn = true;
071 aiIsLargeTurnSpeed = 0.65f;
072
073 } else if (umlAngle >= 65 && umlAngle <= 80) {
074 aiIsLargeTurn = true;
075
076 if (speedPrct >= 0.95f) {
077 aiIsLargeTurnSpeed = 0.05f;
078 } else if (speedPrct >= 0.85f) {
079 aiIsLargeTurnSpeed = 0.10f;
080 } else {
081 aiIsLargeTurnSpeed = 0.15f;
082 }
083
084 } else if (umlAngle >= 60) {
085 aiIsLargeTurn = true;
086
087 if (speedPrct >= 0.95f) {
088 aiIsLargeTurnSpeed = 0.10f;
089 } else if (speedPrct >= 0.85f) {
090 aiIsLargeTurnSpeed = 0.15f;
091 } else {
092 aiIsLargeTurnSpeed = 0.25f;
093 }
094
095 } else if (umlAngle >= 45) {
096 aiIsLargeTurn = true;
097
098 if (speedPrct >= 0.95f) {
099 aiIsLargeTurnSpeed = 0.20f;
100 } else if (speedPrct >= 0.85f) {
101 aiIsLargeTurnSpeed = 0.25f;
102 } else {
103 aiIsLargeTurnSpeed = 0.35f;
104 }
105
106 } else if (umlAngle >= 30) {
107 aiIsLargeTurn = true;
108
109 if (speedPrct >= 0.95f) {
110 aiIsLargeTurnSpeed = 0.40f;
111 } else if (speedPrct >= 0.85f) {
112 aiIsLargeTurnSpeed = 0.45f;
113 } else {
114 aiIsLargeTurnSpeed = 0.55f;
115 }
116
117 } else if (umlAngle >= 15) {
118 aiIsLargeTurn = true;
119
120 if (speedPrct >= 0.95f) {
121 aiIsLargeTurnSpeed = 0.60f;
122 } else if (speedPrct >= 0.85f) {
123 aiIsLargeTurnSpeed = 0.65f;
124 } else {
125 aiIsLargeTurnSpeed = 0.75f;
126 }
127
128 } else {
129 aiIsLargeTurn = false;
130 }
131 } else {
132 aiIsLargeTurn = false;
133 }
134
135 tr = Quaternion.LookRotation(wcV1 - player.transform.position);
136 player.transform.rotation = Quaternion.Slerp(player.transform.rotation, tr, Time.deltaTime * 5.0f);
137 } else {
138 aiLastLookAngle = 0.0f;
139 aiNextLookAngle = 0.0f;
140 aiMidLookAngle = 0.0f;
141 }
142 }
Listing 9-18PlayerState Main Method Details 3
我们要看的第一个人工智能方法是UpdateAiFpsController
方法。这种方法负责控制悬停赛车的水平速度矢量。换句话说,它控制 X 和 Z 轴速度。前几行代码构成了我们之前见过很多次的标准保护代码,第 2-4 行。接下来,第 6–12 行的代码用于确保类路点设置正确。注意,该方法在转义时返回一个空的Vector3
实例。
我们要看的第一段代码是一段相当长的代码,第 15–47 行。它负责决定悬停赛车的扫射,左右移动,力量。在第 15–16 行,新速度向量的分量aiStrafeStrength
和aiSpeedStrength
分别默认为 0 和 1。这相当于全速前进,没有横向速度。如果定义了路点,第 18 行,那么我们得到一个对当前路点的引用,我们调用InverseTransformPoint
方法来从汽车的当前位置确定相对点aiRelativePoint
。这让我们知道我们是否需要对悬停赛车应用扫射速度。在第 22–46 行,根据相对点的距离设置aiStrafeStrength
字段的值。
该方法的下一个职责是计算与悬停赛车相关的侧面和上方碰撞。在第 50 行和第 51 行,通过检查角色运动运动字段的碰撞标志来确定侧面和上面的碰撞。一个简单的检查导致设置类字段collSidesUfp
和collAboveUfp
的值,第 53–57 行和第 59–63 行。该方法负责的下一个计算由第 66–81 行的“计算停滞数据”部分处理。
检查的前两个条件,第 66–71 行和第 71–76 行,重置了停滞数据。在第一种情况下,没有检测到冲突。在第二种情况下,悬停赛车远离航路点,并且没有检测到侧面或上方碰撞。如果发现侧面或上方碰撞,检查的第三个条件将启动“停滞 AI”修改器。将类字段aiIsStuckMode
设置为 1 开始该过程,第 78 行。
如果当前模式是 1 并且悬停赛车的速度很慢,则 AI 驱动的 is-stuck 过程中的下一个计算将把 is-stuck 模式升级到 2。它还会考虑汽车是否仍在当前航路点附近,以及MIN_STUCK_TIME
持续时间是否已过,第 84–89 行。在检查的下一个条件中,如果停滞模式为 1 并且aiTime1
的值已经超过了MIN_STUCK_TIME
持续时间,我们通过在第 90 行将aiIsStuckMode
的值设置为 0 来重置 AI 停滞模式过程。与 is-stuck AI 修饰符相关的最后几行代码“process aiIsStuckMode”从第 97 行运行到第 117 行。
在第一种情况下,我们检查停滞模式的值是否为 1,然后第 98 行的aiTime1
字段增加帧时间。如果可能的话,这部分过程会给汽车一点时间来摆脱困境。我们之前看到,如果经过了足够长的时间,字段aiIsStuckMode
被设置为值 2。第 99–117 行处理了aiIsStuckMode
的值为 2 的情况。在这段代码中,如果可能的话,悬停赛车会被移动,并且所有的停滞修改器字段都会被重置,第 107–112 行。注意,如果某些值没有意义,该方法返回一个零Vector3
值,第 105 和 116 行。
第 119–121 行的代码负责增加aiTime2
类字段的值。在使用这种方法之前,我们还需要承担一些责任。下一个代码片段从第 124 行运行到第 131 行,负责应用航路点减速。这些是当 AI 控制的玩家的悬停赛车通过某些路点时设置的减速提示。如果航路点的aiSlowDownOn
字段设置为真,由WaypointCheck
类处理,并且存在某些减速和速度值,则应用 AI 减速修改器。
在第 125 行,减速时间跟踪器递增,而在第 126 行,根据当前减速值设置aiSpeedStrength
值。在第 127-130 行,如果经过了足够的时间,减速字段将被重置。最后但同样重要的是,我们还有最后一个责任要考虑,即“处理大转弯”的计算。代码从第 134 行运行到第 138 行。如果aiLongTurn
标志被设置为真,则盘旋参赛者的计算速度强度被设置为aiIsLargeTurnSpeed
字段的当前值,第 136 行。该方法中的最后一段代码基于第 140–141 行的aiStrafeStength
和aiSpeedStrength
字段的值创建一个新的Vector3
实例,并返回它。
这就把我们带到了PlayerState
类的一组主要方法中的最后一个方法,即UpdateAiMouseLook
方法。这个方法的第一行,2–4 行,是我们多次看到的标准类配置检查。在第 6–8 行,如果 class’ waypoints
’字段为空,则初始化该字段。接下来,我们检查以确保所有必填字段和值都设置正确,第 10–12 行。方法变量wc1
基于当前航路点索引(第 14 行)设置,并且umlA
和umlForward
字段在第 19 和 20 行初始化。请注意,umlForward
字段是一个指向悬停赛车前方的向量。
第 15–17 行的调试代码负责绘制一个从汽车中心到下一个路点中心的箭头。如果SHOW_AI_LOGIC
字段设置为真,那么当游戏在 Unity 编辑器中运行时,“场景”面板中将显示一个绿色箭头。如果 AI 逻辑调试打开,正向指示向量umlForward
在第 22–24 行上绘制为洋红色线条。接下来,我们检查当前的航路点距离是否大于第 26 行的MIN_WAYPOINT_DISTANCE
字段的值。如果是,则执行第 26–136 行的代码。如果没有,悬停赛车的转弯角度在第 138–140 行被重置为零。
看一下第 27–65 行;从悬停赛车的当前位置到当前和接下来两个路点的中心的角度在这里计算。第 45-47 行和第 62-64 行有一个调试调用,以绿线的形式显示 AI 逻辑,将汽车连接到我们在这里处理的两个路点。在第 66–130 行,如果汽车移动得足够快,并且转弯角度足够大,则aiIsLargeTurn
标志被设置为真。第 135–136 行计算最终旋转值。这种方法实质上是根据悬停赛车与接下来的三个航路点的相对距离和所涉及的角度来驾驶悬停赛车。
演示:玩家状态
我已经为PlayerState
类建立了一个非常详细的演示。打开 Unity 编辑器,进入“项目”面板。找到并打开“场景”文件夹。接下来,找到并打开名为“Main13Demonstration”的场景。在 Unity 编辑器中播放场景,你会在屏幕上注意到大量的汽车状态调试信息。如果您单击“开始”菜单上的任何轨迹按钮,演示场景将自动运行。我建议让街机演示模式 AI race 一边运行,一边监控屏幕上显示的调试值。这对于了解汽车的人工智能如何计算该做什么非常有用。
这个演示场景也是为了显示汽车的人工智能计算转弯,路点和速度。如果你让比赛在人工智能模式下运行,并点击“场景”面板,你会注意到有绿色的线从盘旋的参赛者发出,还有一条紫色的线表示前进的方向向量。这些线表示基于当前和即将到来的路点的人工智能计算。这就是我们这节课复习的结论。如果你第一次没有吸收所有的东西,不要担心。这里发生了很多事。慢慢来。
第二章结论
在这一章中,我们回顾了玩家和游戏状态类回顾的第一部分。这一章都是关于跟踪或捕捉游戏状态的类。让我们花点时间来总结一下本章中我们复习过的内容。
-
PayerInfo:一个微妙的状态类。这个
MonoBehaviour
用于在活动玩家数组中提供相关玩家的索引,这有助于从主GameState
对象实例中查找PlayerState
数据。这个类用于连接游戏中的对象和游戏中的玩家。 -
TrackScript:这个类是一个驻留在
GameState
对象上的MonoBehaviour
,这个 Unity game 对象保存了对重要游戏和玩家数据的引用。它定义了当前赛道的一些属性。 -
PlayerState:一个非常重要的状态类,
PlayerState
类是一个MonoBehaviour
,用于跟踪与它相关的玩家状态的各种信息。
因为这只是游戏和玩家状态回顾的第一部分,我们没有看到GameState
类。这门课是另一门非常重要的课。它本质上是整个游戏的大脑。因此,它相当复杂,我认为最好用一整章来回顾它。你几乎已经完成了游戏中每个职业的详细复习!一旦我们完成了GameState
的回顾,我们将回顾一些关于如何让你的下一个游戏变得“专业”的提示然后我们会找点乐子,建一条新的赛道来比赛。敬请关注!