Unity 高级游戏开发教程(三)

原文:Advanced Unity Game Development

协议:CC BY-NC-SA 4.0

八、菜单系统类

在这一章中,我们将回顾游戏的菜单系统类,但是在此之前,我想回顾一下游戏类的结构。在这一章中,我们将介绍一个新的基类,它为其他菜单屏幕类提供核心菜单屏幕支持。由于事情变得有点复杂,我们最好在这里回顾一下游戏的职业结构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 8-1

Hover Racers 类继承图 1A Hover Racers 类图显示基类和独立类的扩展

正如您在前面列出的图表中所看到的,Hover Racers 代码库中的大多数类都扩展了BaseScript类,并通过扩展扩展扩展了MonoBehaviour类。这意味着所有那些列在BaseScript类条目下的树形结构中的类都是可以附加到 Unity 游戏对象上的脚本组件。到目前为止,您可以在任何演示场景中看到这一点。注意,有几个菜单系统类扩展了一个新的二阶基类,即BasePromptScript类。在上图中,有两个二阶基类:BasePromptScriptWaterResetScript类。

我们不会在这里讨论任何 Unity 编辑器菜单系统的细节,但是我们会在后面的正文中讨论。实际上有三个类比其他菜单系统类更相似。我们将会详细讨论这些课程。我把它们列在这里。

  1. GamePauseMenu

  2. GameOverMenu

  3. GameExitMenu

游戏还使用了一些其他的菜单系统类,其中一些我们会在本章中稍微讨论一下。

  1. 游戏帮助菜单

  2. GameHUDNewScript

  3. GameHUDNewUiScript

  4. 游戏开始菜单

我们将从前面列出的第一组类扩展的基类开始,即BasePromptScript类。这个脚本很简洁,所以我们将跳过更复杂的课程回顾,只列出完整的课程。

课堂复习:BasePromptScript

如前面列出的图表所示,BasePromptScrip t 类是GameExitMenuGameHelpMenuGameOverMenuGamePauseMenuGameStartMenu类的基类。这个基类扩展了我们熟知的另一个基类BaseScript。因此,任何扩展类都是MonoBehaviour类,通过继承,在它们可用的两个基类中都定义了功能。因为“提示”菜单屏幕都有相似的功能,两个按钮、一个声音效果和一个文本提示等。将功能、字段和方法集中到一个基类中是有意义的。我们将使用以下复习步骤来涵盖本课程。

  1. 类别字段

  2. 相关的方法大纲/类头

  3. 支持方法详细信息

  4. 主要方法详细信息

  5. 示范

就这样,我们开始吧,好吗?第一部分是类字段。

类字段: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 元素。与这些字段相关的是接下来的两个类字段,keyBrdInputIdxMinkeyBrdInputIdxMax。这些字段用于指定当前菜单屏幕上可用的 UI 元素的最小和最大索引。

扩展了BasePromptScript类的屏幕主要是带有文本提示的 yes 或 no 菜单屏幕。在这种情况下,最大索引设置为 2。这些字段之后是 Unity UI Text类实例txt。这个类用于在菜单屏幕上显示文本提示。布尔标志btnPressed用于跟踪菜单按钮是否被按下。最后,btnOnebtnTwo字段是 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类。因此,它从BasePromptScriptBaseScript基类中获得了很多功能。我们将使用以下复习步骤来涵盖本课程。

  1. 相关的方法大纲/类头

  2. 支持方法详细信息

  3. 主要方法详细信息

  4. 示范

我们只有四个审查步骤的原因是因为我们已经扩展了多个基类并继承了它们的功能,从而简化了类的实现。接下来让我们看看课程大纲。

相关的方法大纲/类头: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 编辑器,使用向上和向下键盘按钮更改所选的暂停菜单按钮。请注意,您可以使用鼠标或回车键单击按钮。这是我们工作中的抽象层。GameOverMenuGameExitMenu类与我们刚刚复习的类非常相似。我让你自己去看。在你继续学习之前,确保你已经很好的理解了这些课程。

课堂回顾:游戏助手菜单

GameHelpMenu用于显示一系列帮助屏幕,描述关于 Hover Racers 游戏的不同细节。这个菜单屏幕比我们之前看到的要复杂一些。我们将使用以下步骤来复习这门课。

  1. 类别字段

  2. 相关的方法大纲/类头

  3. 支持方法详细信息

  4. 主要方法详细信息

  5. 示范

我们将看到一些与 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

这个类使用来自UnityEngineUnityEngine.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 元素。在这种情况下,我们有方法来控制启用或禁用nextprev按钮。这些方法非常简单,本质上是相似的。在继续下一步之前,花点时间仔细检查一下,确保你理解了它们。我们要看的下一个方法是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生命周期回调方法,StartUpdate,以及两种 UI 管理方法,InvokeClickSetBtnTextColor

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方法。我们以前在BasePromptScriptGamePauseMenu类中见过这个方法。注意方法声明中的new关键字。这向编译器表明,这个继承自BasePromptScript类的方法在这里被重新定义。如果我们被要求点击第一个按钮,方法的第 2 行,这对应于“上一步”按钮。请注意,只有当帮助菜单屏幕索引大于 0 时,我们才调用 click 事件。这意味着我们不在第一个屏幕上,所以我们可以返回。

在第 6–8 行,如果当前选择的按钮索引是 1,对应于“主菜单”按钮,我们不加判断地处理点击事件。如果有更多的帮助菜单屏幕要查看,第 8–12 行的最后一段代码执行“下一步”按钮单击。下一个方法SetBtnTextColor负责设置指定按钮上的按钮文本颜色。它还重置上一个按钮的文本颜色。文本颜色的变化用于突出显示菜单按钮,表示它已被选中。第 2–11 行的小代码块用于恢复先前选择的按钮的文本颜色,而第 13–22 行的代码块用于设置当前选择的按钮的文本颜色。这就是本复习部分的结论。在下一节中,我们将看一看班级的示范。

演示:GameHelpMenu

为了演示GameHelpMenu类的运行,我们最好运行完整的游戏,场景“Main13”或“Main14 ”,点击开始菜单上的“help”按钮启动帮助菜单。在 Unity 编辑器中打开游戏项目,注意“项目”面板,找到“场景”文件夹。打开一个主场景,按下 Unity 编辑器的播放按钮运行游戏。当游戏开始时,点击上面提到的“帮助”按钮,启动帮助菜单屏幕。尝试使用键盘、控制器或鼠标与菜单的 UI 元素进行交互。当你这样做的时候,记住你刚刚检查的代码。

剩余菜单类别

其余的菜单系统类如下所示:

  1. 游戏开始菜单

  2. GameHUDNewScript

  3. GameHUDNewUiScript

    我不会在这里详细回顾这些类。剩下的三个类与我们刚刚复习的类非常相似。复习它们并不能获得任何新的常识;然而,你应该仔细阅读它们,确保你熟悉它们。请务必这样做。通过使用本章演示部分概述的方法,尝试在实际游戏中查看每个菜单屏幕。

就 Unity 编辑器中的对象和组件而言,我们没有涉及到设置菜单屏幕,但是我们将在稍后的文本中介绍。这就引出了本章的结论。在我们继续之前,我想回顾一下我们讲过的内容。

第二章结论

在这一章,我们设法涵盖了游戏菜单系统的主要方面。在这个过程中,我们从游戏规约列表中去掉了几个点。让我们总结一下本章所讲的内容。

  1. BasePromptScript:大部分游戏的菜单系统使用的基类。该类包含核心的共享功能,以简化扩展类的实现。

  2. GamePauseMenu:这个类是一个双选项菜单屏幕的具体实现,它使用BasePromptScript作为基类。我们还回顾了 UI 事件抽象,通过类调用菜单屏幕按钮上的点击事件的能力来证明。

  3. 这个类是一个更复杂的菜单屏幕实现的例子。虽然这个类也扩展了BasePromptScript类,但它覆盖了基类的大部分功能来支持三按钮菜单屏幕。

虽然我们没有涵盖游戏中的每个菜单屏幕,但我们涵盖了一组核心示例,这些示例带我们了解了菜单系统实现的关键公共方面。请务必查看一下我们在这里没有介绍的课程。仔细阅读它们,并跟随完整游戏场景的实际菜单屏幕,“Main13”或“Main14”。我们几乎完成了文本的代码部分,但是我们还有很多重要的内容要讲,所以坚持一下。在下一章,我们将开始回顾游戏的核心,管理玩家和游戏状态的类。

九、玩家和游戏状态类:第一部分

在这一章中,我们将看看负责跟踪玩家和游戏状态的类。有很多代码需要我们回顾,所以我把回顾分成了两章。这两章复习所涉及的课程如下:

  1. PlayerInfo(第 09 章:第一部分)

  2. TrackScript(第章第 9 :第一部分)

  3. PlayerState(第章第 9 :第一部分)

  4. 游戏状态(第十章第十章:第二部分)

列出的前两个类别PlayerInfoTrackScript附属于随后的两个类别PlayerStateGameState。因为前两个类非常简单和直接,我们将从它们开始。我们开始吧!

课堂回顾: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 游戏对象上,紧挨着GameStatePlayerState组件的实例,如此处所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1

GameState Unity 游戏对象配置描述 GameState 游戏对象设置的屏幕截图。它显示了 GameState、PlayerState 和 TrackScript MonoBehaviours 脚本组件,根据需要附加到相同的父游戏对象

GameState类负责加载和处理存储在关联的TrackScript组件中的数据。职业的第一个领域,index,目前没有被游戏使用。随意实现一些索引特定的代码,如大气效果或不同的背景音乐。列出的下一个字段headLightsOn是一个布尔标志,用于指示当前赛道是否应该打开悬停赛车的前灯。随后,laps字段用于显示当前赛道的圈数。我说建议是因为赛道难度和模式也会影响给定赛道的圈数。最后,sceneName字段可用于为当前轨道/场景提供名称。我们要看的下一个类是PlayerState类。这个类是一个怪物,所以要准备好一个冗长的类视图。

课堂回顾:玩家状态

PlayerState类是通过BaseScript类的扩展得到的MonoBehaviour类。它用于在整个游戏过程中跟踪玩家的状态。因此,该类有大量字段跟踪与汽车运动和状态相关的各种值。因为PlayerState类非常复杂,我们将使用更结构化的审查过程,并按照以下步骤来审查它:

  1. 静态/常量/只读类成员

  2. 类别字段

  3. 相关的方法大纲/类头

  4. 支持方法详细信息

  5. 主要方法详细信息

  6. 示范

没有相关的枚举可言,所以我们将省略这一部分。我应该花点时间提一下,有很多,几乎太多,类字段要回顾。不要对此感到不知所措,我们会慢慢地、详细地介绍一切。不要以为第一遍读完就一定要全记在脑子里。在处理游戏代码时,你很可能需要参考几次这个类的评论。在大多数情况下,您根本不需要调整这些字段,所以就把它看作是我们的彻底。就这样,让我们开始吧。

静态/常量/只读类成员: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_SPEEDDEFAULT_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。我们还有MouseLookNewmouseInputFPSInputControllerfpsInput的实例来完善我们的控制类。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类字段记录在当前赛道上花费的分钟数,而接下来的两个字段,sms,用于指示在当前赛道上花费的时间的秒和毫秒部分。接下来的三组字段用于跟踪玩家的偏离轨迹、错误方向和跳过的路点状态。下一个要审查的是跑道外的场地。该组中的第一个条目是offTrack字段。该字段是一个布尔标志,用于指示玩家离开了跑道。该组中的第二个字段表示当前玩家离开赛道的时间。

这组字段之后是错误方向类字段。与偏离轨道字段的模式类似,错误方向字段由布尔标志和定时值、wrongDirectionwrongDirectionTime字段组成。下一组字段,即跳过的路点类字段,也由布尔标志和时间追踪字段组成。该组包含wrongDirectionwrongDirectionTime字段。类似地,下一组字段,即跳过的路点类字段,也由布尔标志和时间跟踪字段组成。这些是skippedWaypointskippedWaypointTime类字段。这种模式经常用于测量某些功能的持续时间,然后切换一个布尔字段来打开或关闭该功能。

接下来是一组字段,表示与玩家的悬停赛车相关联的相机和对象。第一个这样的字段是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用于指示枪修改器是否激活。接下来的三个字段(isBouncingisJumpingisDrafting)的使用方式与我们刚刚查看的gunOn字段相同。该组中接下来的六个字段遵循我们之前看到的关于轨迹和航路点指示器的相同模式。

在每种情况下,都有一个布尔标志和一个时间跟踪字段,用于射击、命中和未命中事件。请注意,这些事件仅在游戏的战斗模式中可用。接下来的两个字段lapCompletelapCompleteTime,遵循我们刚刚看到的完全相同的模式。下一个字段lapComplete表示该圈已经完成,而下一个字段lapCompleteTime记录该圈的持续时间。armorOnboostOn字段是布尔标志,用于指示给定的修改器是活动的还是非活动的。invinvOninvincTime字段遵循我们看到的相同模式,包括一个布尔标志和一个计时字段。

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实例,用于保存对所有路点的引用。

以下两个字段用于跟踪汽车和下一个航路点之间的距离,waypointDistancewaypointDistancePrev。正如你可能已经猜到的,先前的航路点距离计算存储在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字段的副本。接下来的两个字段,aiWaypointpointLastIndexaiWaypointIndex,用于跟踪玩家之前和当前的航点索引。这些由玩家的悬停赛车触发,与航路点对象交互。我应该注意到,虽然我说的是“玩家的”,这也意味着一个 AI 对手玩家,而不仅仅是一个人类玩家。

aiWaypointRoute字段用于加载指定路线的航路点。这个特性在游戏中实际上并没有使用;相反,路由总是被设置为零。如果您愿意,可以随意扩展这个功能。下一个字段aiTurnStrength,目前游戏还没有实现,但是可以作为人工智能转向计算的一部分。下面的字段aiSpeedStrength,被游戏的人工智能计算用来控制悬停赛车手的速度矢量。

类似地,aiStrafeStrength字段用于控制 AI 控制的悬停赛车的计算速度矢量的扫射分量。aiSlide字段为 AI 提供了一个用于速度计算的滑动组件。在处理“卡住”的悬停赛车时,我们会随着时间的推移进行一些检查,以表明赛车被卡住了。aiIsStuckMode字段有助于跟踪正在执行的停滞检查。

最终,“停滞”计算的结果存储在aiIsStuck字段中。接下来,我们有aiWaypointDistance字段,用于在 AI 模式下跟踪到下一个航路点的距离。aiRelativePoint字段用于确定当 AI 驾驶悬停赛车时应该使用什么aiStrafeStrength。接下来的两个字段aiTime1aiTime2用于跟踪时间间隔,例如在检查不同的aiIsStuckMode时。该组中的最后两个字段是aiSlowDownTimeaiSlowDown字段。这些字段用于控制盘旋赛车在转弯时的速度。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布尔用于指示汽车是否能够开火。这只有在游戏的战斗模式变异中才有。下一对字段aiBoostModeaiBoostTime,遵循与之前看到的布尔标志、持续时间字段集类似的结构。在这种情况下,模式决定行为,时间字段跟踪持续时间。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

我们要查看的最后一组类字段是“其他”类字段。gunExplosiongunHitSmoke字段是游戏对象引用,指向玩家的悬停赛车中的对象,特别是汽车的统一层次GameObjects。接下来的两个字段应该看起来很熟悉。它们遵循我们之前见过的相同模式。这一对场,gunSmokeOngunSmokeTime,用来控制枪械的烟雾效果。该功能目前在游戏中被禁用。我会解释的。这个特性是以一种遗留的方式实现的,但现在已经过时了。我们保留了为它提供动力的代码,但是注释掉了,这样你就可以用它来为游戏和支持代码添加新的最新粒子效果。下一个字段是引用了 hover racer 模型结构的一部分的GameObject flametotalLaps字段的值表示当前比赛的圈数。

类似地,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方法后面列出了SetNormSetBoost方法。这些方法与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 行没有暂停。在下一组要回顾的主要方法中,我们将看看UpdateReset方法。

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 行更新了两个重要的类字段timetimeNumtime字段是当前圈速的字符串表示。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 行,新速度向量的分量aiStrafeStrengthaiSpeedStrength分别默认为 0 和 1。这相当于全速前进,没有横向速度。如果定义了路点,第 18 行,那么我们得到一个对当前路点的引用,我们调用InverseTransformPoint方法来从汽车的当前位置确定相对点aiRelativePoint。这让我们知道我们是否需要对悬停赛车应用扫射速度。在第 22–46 行,根据相对点的距离设置aiStrafeStrength字段的值。

该方法的下一个职责是计算与悬停赛车相关的侧面和上方碰撞。在第 50 行和第 51 行,通过检查角色运动运动字段的碰撞标志来确定侧面和上面的碰撞。一个简单的检查导致设置类字段collSidesUfpcollAboveUfp的值,第 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 行的aiStrafeStengthaiSpeedStrength字段的值创建一个新的Vector3实例,并返回它。

这就把我们带到了PlayerState类的一组主要方法中的最后一个方法,即UpdateAiMouseLook方法。这个方法的第一行,2–4 行,是我们多次看到的标准类配置检查。在第 6–8 行,如果 class’ waypoints’字段为空,则初始化该字段。接下来,我们检查以确保所有必填字段和值都设置正确,第 10–12 行。方法变量wc1基于当前航路点索引(第 14 行)设置,并且umlAumlForward字段在第 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 一边运行,一边监控屏幕上显示的调试值。这对于了解汽车的人工智能如何计算该做什么非常有用。

这个演示场景也是为了显示汽车的人工智能计算转弯,路点和速度。如果你让比赛在人工智能模式下运行,并点击“场景”面板,你会注意到有绿色的线从盘旋的参赛者发出,还有一条紫色的线表示前进的方向向量。这些线表示基于当前和即将到来的路点的人工智能计算。这就是我们这节课复习的结论。如果你第一次没有吸收所有的东西,不要担心。这里发生了很多事。慢慢来。

第二章结论

在这一章中,我们回顾了玩家和游戏状态类回顾的第一部分。这一章都是关于跟踪或捕捉游戏状态的类。让我们花点时间来总结一下本章中我们复习过的内容。

  1. PayerInfo:一个微妙的状态类。这个MonoBehaviour用于在活动玩家数组中提供相关玩家的索引,这有助于从主GameState对象实例中查找PlayerState数据。这个类用于连接游戏中的对象和游戏中的玩家。

  2. TrackScript:这个类是一个驻留在GameState对象上的MonoBehaviour,这个 Unity game 对象保存了对重要游戏和玩家数据的引用。它定义了当前赛道的一些属性。

  3. PlayerState:一个非常重要的状态类,PlayerState类是一个MonoBehaviour,用于跟踪与它相关的玩家状态的各种信息。

因为这只是游戏和玩家状态回顾的第一部分,我们没有看到GameState类。这门课是另一门非常重要的课。它本质上是整个游戏的大脑。因此,它相当复杂,我认为最好用一整章来回顾它。你几乎已经完成了游戏中每个职业的详细复习!一旦我们完成了GameState的回顾,我们将回顾一些关于如何让你的下一个游戏变得“专业”的提示然后我们会找点乐子,建一条新的赛道来比赛。敬请关注!

网传资源,如有侵权请联系/留言,资源过大上传乃是下载链接的ZIP文件。 目录: 1.Animal1 2.Animal2& ]9 p, D+ }, l6 e" U4 [+ F! R+ \4 I 3.Animal3 4.Animal4 5.Twist1+ T4 z* ?, N/ s$ ^* p* R 6.Twist2 7.Twist37 `* }8 p$ `6 y1 ^ 8.Twist4* o3 O) i) b6 X4 w5 U( l: M9 x 9.AssetBundle 原理1 10.AssetBundle 原理2 11.AssetBundle 原理3) _3 K- H5 u8 y# K. } 12.AssetBundle 原理4 13.AssetBundle初识1 14.AssetBundle初识2 15.AssetBundle初识3- c. d4 {3 z- Z3 y. P1 Y# M 16.byte操作1! p1 X1 p$ A& P o% J8 j- F! E" E 17.byte操作2$ {, e5 X* |2 y3 f: R 18.byte操作3 19.C#基础1. Z6 R8 o; w. I1 p$ i 20.C#基础2 21.C#基础3) B: I6 B! g/ A4 I$ {" X 22.C#基础41 V& X! B$ K' m# ^3 k# Z 23.oop1 24.oop2/ x! i8 b* a+ `* |# \7 e1 v 25.oop33 s) }. f3 d" d4 A5 h9 ]9 }: T8 { 26.oop4 27.方法调用1! Y' N, n, \+ y+ ~1 D, b7 {9 C 28.方法调用2 29.方法调用3 30.方法调用4 31.WWW1 32.WWW21 o! W# Q/ r& y7 z& K 33.WWW3, z4 u/ m B& s2 D0 j8 d4 Y 34.WWW4; Y" S6 M7 {6 q! a' e 35.贪吃蛇1# _6 O" {. y/ | 36.贪吃蛇2 37.贪吃蛇3 38.Buider1- @' a5 p7 H' y6 `/ K) R 39.Buider2' `. w5 j8 g8 q) W 40.Buider3 41.Buider4 42.CameraView1 43.CameraView2 44.CameraView3; b# }8 D) I4 J( O 45.Combine1 46.Combine25 L) `& E3 C# u2 ` 47.Combine3 48.设计模式01 49.设计模式02. }3 ~# Y' W A* L { 50.设计模式03 51.设计模式04 52.设计原则1 53.设计原则2 54.设计原则3 55.Frame01 56.Frame02 57.Frame03 58.Frame04 59.Frame056 m. o( _0 U9 T0 f8 t 60.Frame06 61.Frame07- u, ?- z" E7 l! q 62.Frame081 `# W$ g% p7 d* x. D 63.Frame099 ]/ l# D& S' _! l6 R( K4 h% V 64.LuaFrame010 p2 j* t+ c: C+ E' V 65.LuaFrame02* Y2 @# ?+ ]1 U' y @8 I: ^ 66.LuaFrame032 L: ]* ^, X4 o1 b1 P9 o" V 67.LuaFrame04 68.LuaFrame051 ?$ x3 B* \9 }+ B4 m, {9 U 69.LuaFrame06 70.LuaFrame07 71.Fsm1 72.Fsm2 73.Fsm30 e; n' F* t, z L4 U) K 74.Fsm4 75.lua01 76.lua02; h$ V% |, x2 S3 Z7 r 77.lua03 78.lua04 79.lua053 s/ q! c4 V1 a 80.lua06 81.lua07" q- j" }2 O( h- {/ b 82.tolua 使用1 83.tolua 使用25 ?7 L3 r/ X* p4 s' b* B 84.tolua1 85.tolua2 86.tolua3 87.UIEasyTouch1 88.UIEasyTouch2" a+ ~- \( u; f 89.UIEasyTouch3 m9 d( x1 d$ j3 }, j6 |; i 90.UIEasyTouch4% h) d% N! G! A% A% z; X 91.矩形攻击1 92.矩形攻击27 i6 z0 P, K3 I7 o3 d4 {: U 93.矩形攻击3 94.矩形攻击4! H7 w7 C" t1 U6 \) @ 95.矩形攻击5 96.TCP 沾包 拆包问题1/ t) X8 ?4 r% _! d1 a 97.TCP 沾包 拆包问题2 98.TCP 沾包 拆包问题39 A/ [; B/ H6 |1 _3 ]- K( K 99.TCP 沾包 拆包问题41 ~- }" G& k! a5 b 100.TCP 沾包 拆包问题5 101.tcp11 V) C2 ^" L3 i" P 102.tcp22 M. @1 }- P& h2 i4 H 103.tcp3 104.protobuffer1' o) E) N$ i" U/ m 105.protobuffer21 V" C9 y, F, ~3 n& H 106.protobuffer3 107.protobuffer4 108.UDP014 S8 r! X* {& o: i 109.UDP02 110.UDP032 b* _4 X% a; P. N" X. }' Y( }( B 111.UDP04 112.UDP05, g C8 F( M$ o$ D 113.UDP06& i. i- o( z8 h8 e' V3 }, J 114.UDP07 115.UDP086 ?4 S/ `! h# ]( s6 f$ N9 d 116.更新 M层 数据1 117.更新 M层 数据2 118.更新 M层 数据3: `- m) J7 D! X5 @. \) T 119.LitMap16 Q8 F' i, Q C2 M 120.LitMap21 ]3 u3 F6 z' X/ l* O5 u0 ~2 a 121.LitMap3) U) a& k; v8 x3 H8 ~9 z 122.LitMap47 d5 w1 |0 L) d1 u- c 123.Painter1* {7 f6 f% w0 d, V 124.Painter2 125.Painter3 126.Painter41 x0 {2 ?. F/ ~. s' @ 127.UIFrame01 128.UIFrame022 K- d6 D$ ^3 N3 f6 q r) d( e 129.UIFrame032 Y3 A, `! T q. ]5 |1 g' E z4 r1 R+ C 130.UIFrame04 131.UIFrame05 132.UIFrame062 R- |& e z; u 133.UIFrame07 134.UIFrame08 135.UIFrame096 k9 R4 `# o# S. w8 S 136.UIFrame10& |* r5 P, [5 C) q 137.Audio1 138.Audio2/ D- x4 J6 e0 B8 J0 P 139.Audio3' b5 c. |. B" _5 ? X/ H 140.www1 141.www2 142.www3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值