一、简介和入门
欢迎来到高级 Unity 游戏开发正文第一章。Unity 是一个强大的游戏创作工具。在许多方面,它几乎太强大和复杂了。这使得一些游戏开发者很难充分发挥该软件的潜力。文档、书籍和教程是降低学习曲线的好方法。编程经验,尤其是 C# 编程经验,以及对 Visual Studio 的熟悉也将极大地提高您使用 Unity 的时间。
有了所有这些不同的培训选择,其中许多是免费的,这本书提供了什么,其他人没有?是什么让它脱颖而出?嗯,在大多数情况下,前面提到的学习材料会告诉你如何在 Unity 中完成一个简单的任务。他们会给你看一个未完成的小游戏或演示来说明手头的材料。这本书与其他书的不同之处在于,它包含了一个完整的代码审查,详细介绍了一个全功能和完整的 Unity 游戏。这包括以下一般主题:
-
运行完整游戏的代码和类
-
代码结构要点
-
项目结构要点
-
水平/轨道建筑
-
人工智能对手/玩家
-
完整的 HUD 和菜单系统
-
音乐和音效
-
玩家偏好
-
触摸、鼠标、键盘、游戏手柄输入
-
模型、预设和脚本
这本书将引导你通过游戏的代码,脚本,模型,预置,和整体结构,同时向你展示代码如何与 Unity 引擎一起定义一个完整的,精致的游戏。在本文结束时,您将在以下专业领域获得经验,因为您将在指导下实现一个悬停赛车游戏:
-
C#:具有类、类管理和类代码集中化的项目级方面的经验
-
Unity 编码:使用扩展 Unity Monobehaviour 类的类的经验。在基于组件的游戏引擎中使用组件
-
Visual Studio:体验导航项目、查看和编辑类文件
-
Unity C# 项目管理:拥有完整的 Unity 游戏和相关 C# 代码的工作经验
-
Unity 环境:体验导航复杂 Unity 项目的模型、预设、资源、脚本文件和场景
-
Unity 项目管理:处理较小场景和相关类、预设和模型的经验
我们将回顾游戏功能的细节以及我们在整个旅程中遇到的 Unity 中的关键概念。这将给你一个坚实的基础,在此基础上建立你的 Unity 游戏开发的未来。我们将采取的一般路径如下:
-
回顾汽车模型、赛道、赛道特征以及它们之间的交互。
-
回顾驱动游戏的代码。代码分为以下几组:
-
基类
-
交互类
-
高级交互类
-
助手类
-
输入类
-
菜单系统类别
-
玩家和游戏状态类
-
-
回顾以下与 Unity 相关的主题:
-
输入映射
-
场景结构和不活跃的游戏对象
-
GameObject 标签
-
多摄像机设置
-
场景照明
-
音乐和音效
-
人工智能对手
-
将其应用到一个新的水平/轨道
-
在旅程的最后,你将拥有制作下一个伟大游戏所需的所有知识和经验。既然我们已经知道了摆在我们面前的是什么,让我们开始我们旅程的第一步,让我们的游戏开发环境启动并运行起来。
设置您的环境
在开始之前,我们需要正确设置和配置我们的环境。我们需要做的第一件事是打开浏览器并导航到 Unity 网站。导航至 www.unity.com
并创建一个新账户(如果您尚未创建)。完成这个过程,并确保您完成帐户验证步骤,因为您需要一个活跃的帐户,然后才能开始与包括赛车游戏项目。
在撰写本文时,使用 Unity 的正确方式是通过 Unity Hub 应用。该应用充当 Unity 项目的抽象层和集中点。该软件允许你管理多个项目,每个项目使用不同版本的 Unity。在 Unity 网站上找到下载页面并下载最新版本的 Unity Hub。安装软件。完成后,打开 Unity Hub 并使用您刚刚创建的帐户登录。
目前,Unity Hub 可以在 Windows、macOS 和某些 Linux 发行版上运行。有关如何安装 Unity Hub 以及支持哪些操作系统的快速参考,请在浏览器中导航至以下 URL:
docs。unity3d。com/Manual/getting start in stalling hub。html
接下来,我们将安装 Unity 的最新版本。打开 Unity Hub(如果您尚未打开),然后选择屏幕左侧的“安装”选项卡。选择最新版本的 Unity,并选择您想要与 Unity 编辑器一起安装的模块。
出于本文的目的,我们建议您仅选择下列模块。当然,如果你对你想要安装的模块有自己的想法,请随意。我们唯一的要求是将“Visual Studio”配置为 Unity 编辑器的默认脚本 IDE。您可以单独安装 Visual Studio,但我将只提供如何将其作为 Unity 模块安装的说明。
-
在“开发工具”下,选择“Microsoft Visual Studio 社区”;这是必需的。
-
为您的操作系统选择本机构建模块。根据您的操作系统选择以下选项之一:
-
Linux 构建支持(IL2CPP 或 Mono)
-
Mac 构建支持(单声道)
-
Windows 生成支持(IL2CPP)
-
您可以通过 Unity Hub 软件安装新模块或卸载现有模块。选择“安装”选项卡,然后单击目标 Unity editor 版本的图标上的三个点。从上下文菜单中选择“添加模块”,您可以自定义为该版本的 Unity editor 安装的模块。尝试将 Android 和 WebGL 构建模块的安装添加到您的设置中。安装了它们之后,您将有一些有趣的构建目标可以使用。既然我们已经解决了这个问题,让我们获取一份与本文相关的赛车游戏项目的副本。将您的浏览器导航到以下 URL:
www.middlemind.net/urgbook/[BOOK PUBLICATION
https://github.com/Apress/Advanced-Unity-Game-Development
查找游戏项目的最新版本。将列出的 Unity 版本与您刚刚安装的 Unity editor 的当前版本进行比较。如果游戏项目版本较旧,尝试使用前面列出的过程安装该版本的 Unity 编辑器。如果旧版本在 Unity editor 版本列表中不可用,则安装最早的可用版本。
如果你发现自己在第二种情况下,一个旧版本不可用,那么我建议按照这个过程来升级游戏项目。首先,用刚刚安装的旧版本 Unity 打开项目。应该会提示您升级项目。这样做,当升级过程完成时,保存项目。完成这些步骤后,执行相同步骤,只是这次使用您安装的最新版本的 Unity editor。第二次升级完成后,保存项目。
这种方法可以安全地将项目升级到 Unity 的最新版本。让我们打开项目,检查一些东西。一旦赛车游戏项目完成加载,打开“首选项”窗口,“编辑”➤“首选项”,并选择“外部工具”标签。确保“外部脚本编辑器”首选项设置为“Visual Studio”
如果您在可用编辑器列表中没有看到“Visual Studio ”,请返回并检查您的 Unity 编辑器版本的已安装模块,并确保安装了“Visual Studio”。如果您仍然遇到问题,请重新安装 Unity 编辑器,并确保选择“Visual Studio”模块。
玩悬浮赛车/做好准备
现在我们已经解决了这个问题,让我们在打开 Unity 编辑器的同时测试一下这个游戏。让我们检查一下默认情况下哪个场景被打开了,如果有的话。查看 Unity 编辑器窗口的标题栏。当前打开场景的名称应该列在窗口标题中。如果你在标题中看到单词“Main13”或“Main14 ”,那么我们就可以开始了。如果没有,那么我们就必须打开正确的场景。去“项目”面板,或者如果你没有看到它,去主菜单,选择“窗口”➤“面板”➤“项目”,并打开它。
找到名为“场景”的文件夹并打开它。双击名为“Main13”的场景。一旦场景加载完毕,我们需要定位“游戏”面板。如果找不到,就按照前述步骤打开。一旦面板打开,选择它,你应该在屏幕上看到一堆 UI 和菜单。这很好。找到编辑器窗口顶部中间的播放按钮。在你按下播放键之前,让我来回顾一下游戏的控制。
要控制汽车的方向,请左右移动鼠标。要加速汽车,按键盘上的向上箭头。你可以用左右箭头键来左右“扫射”汽车。要减慢车速,请使用返回箭头键。好了,现在你已经掌握了基本知识,按 play,然后在游戏中点击以确保输入是活动的。单击主菜单的“赛道 1”按钮,玩几场比赛。
既然你已经有机会玩这个游戏,让我们停止称它为“赛车游戏”我们现在称之为“悬浮赛车”所以从现在开始,当你看到“悬浮赛车”这几个字时,我最有可能指的是这个项目或这个游戏,这取决于上下文。我将使用汽车、玩家、当前玩家、赛车、悬停赛车或赛车来描述游戏的玩家,包括人类和人工智能。根据上下文,这可能意味着当前玩家、人类玩家或游戏中的任何对手玩家。注意上下文。
关于这个游戏的一点点。悬停赛车是一个完整的赛车游戏,支持三种比赛类型:简单,经典,和战斗。三个难度:容易,中等,困难。它有两条内置轨道。正如我们所看到的,游戏有一个完整的 UI 实现,包括游戏中的 HUD 和菜单系统。此外,游戏有背景音乐、音效和获胜条件。这里主要的要点是游戏是精致的、专业的和完整的。这不是一个演示或辅导项目。这是一个完整的游戏,有一套完整实现的功能。
通过这本书的结论,你会对 Hover Racers 游戏的工作原理有一个完整的理解。在这一点上,你将能够看到这个项目更像是一块被塑造的粘土,而不是一个最终的雕塑。您将能够看到项目的本来面目,并能够使用您在此获得的知识添加您自己的功能来塑造它。此外,您可以将这些知识应用到任何 Unity 游戏开发项目中。包括你自己的,下一个伟大的游戏。
我们会一起回顾绝大多数代码,所以你不一定需要精通 C#,但建议有一些编程经验。这里有一些教程,你可以阅读,以获得对 Unity、Visual Studio 和 C# 的基本理解:
(C# 入门教程)
(Visual Studio 入门教程)
(Unity 编辑器简介)
(Unity 碰撞简介)
这就引出了本节的结论。在下一节中,我们将以对我们所学内容的总结来结束这一章。
第二章结论
这就引出了本章的结论。在这小小的介绍章节中,我们实际上涵盖了相当多的内容。让我们来看看到目前为止我们都做了些什么:
-
列出了 Unity 游戏开发主题,您可以通过阅读和阅读本文获得经验
-
列出了通过使用本文你将获得经验的一般的、支配性的技能
-
制定了一个处理我们需要覆盖的材料的计划
-
设置我们的环境,包括安装 Unity Hub、Unity 编辑器和 Visual Studio
-
玩过悬浮赛车
现在我们已经处理好了所有的问题,我们准备开始审查游戏的代码。但是等等!我们必须概述我们正在进行的游戏。当然,我们有一个游戏的成品副本,所以这似乎有点多余,不是吗?好吧,我们真的需要把这当成一次有指导的游戏回顾之旅。因此,我们将在下一章研究盘旋赛车游戏规范,并在后续章节详细回顾游戏代码。
二、游戏规约
在这一章,我们将概述悬停赛车的游戏规约。这是游戏开发的重要一步。你应该总是花时间列出一些通用的游戏规范。在某些情况下,这似乎是不必要的,但我还是建议你花时间去做。当你列出一些规约时,往往会有一个非常宝贵的想法出现在你的脑海中。这就是我们想要的,围绕手头的主题形成的新思想和新概念。让我们看看我们的规约清单。
-
一种赛车游戏,赛车在跑道上跑一定的圈数,争夺最快的速度和第一名
-
游戏中的 HUD 显示当前比赛和汽车状态的信息,即修改器
-
支持六个玩家,通过视频游戏人工智能来管理不受玩家控制的汽车
-
赛道特性,包括保险杠屏障、助推面板(涡轮)、跳跃面板和战斗模式修改器
-
支持三种困难:低、中、高
-
支持三种游戏模式:简单、经典和战斗模式
-
检测偏离轨道、卡住和方向错误的汽车状态的能力
-
完整的菜单系统,包括主菜单、游戏中的 HUD、结束游戏和暂停游戏屏幕
-
像用户偏好、输入处理程序、音乐、音效和多条赛道这样的收尾工作
以下部分定义了在与游戏状态管理和输入类相结合时驱动游戏所需的基本游戏机制。
-
型号:汽车、轨道、传感器
-
模型:汽车,助推,跳跃,反弹修改器
乍一看,这个列表似乎不完整,但实际上在这一点上它是非常正确的。我们并不试图创建一个包含 100 个要点的列表来详细描述游戏的每个方面。即使这样的事情是可能的,它几乎肯定会导致某种僵硬的、机械的游戏。我们的目标是给游戏下一个宽泛的定义,这样我们就可以清晰地想象游戏中的大部分画面,而不会被太多的细节所困扰。
例如,考虑到前面的描述,您可能会把游戏的开始想象成一个静态的菜单屏幕,玩家在比赛前与之交互。或者你可以把它想象成一个类似于街机游戏柜的动画场景,在那里游戏自己玩,直到用户与游戏交互。这里的要点是,我们对自己想要的东西有一个大致的轮廓。细节将在以后补充。
花点时间回顾一下前面列出的规约。当你阅读它们的时候,让你的想象驰骋。当我们处理前面列出的某个具体条目时,我会试着记下来,但我会不时地回头看看这个列表,看看我们已经从列表中去掉了多少点,以防我漏掉一些。接下来,我们将看看这个游戏的一些机制,并详细描述描述这个问题的模型。
型号:汽车、轨道、传感器
我们要看的第一个模型涉及赛车、赛道和赛车的传感器。这个模型构建练习将帮助我们详细描述游戏的一些机制,而不必求助于冗长的描述或要点列表。让我们来看看。
图 2-1
汽车、轨道、传感器模型图描绘悬停赛车、轨道碰撞盒和汽车碰撞盒的图
之前展示的模型有一个侧视图,显示了 hover racer 在赛道上的汽车传感器碰撞盒。赛道模型 A 也有一个碰撞盒 Ac ,用来检测赛车是否在赛道上。如果汽车偏离了轨道,轨道碰撞盒会检测到这种变化,并将汽车标记为偏离轨道。另一个碰撞盒是汽车传感器, C ,如前所示。这个碰撞盒用于检测当前汽车前面的汽车。
这个模型非常简单,但是它回答了我们的一些问题。我们现在有了赛车和赛道如何相互作用的想法。这给了我们一种方法来确定当前玩家的车是否偏离了轨道。这是赛车游戏的一个共同特点,也是我们无论如何都要解决的问题,所以我们有一个工作计划是件好事。接下来,我们将看看一个模型,它描述了汽车应该如何与某些赛道特性进行交互。
模型:汽车,助推,跳跃,反弹修改器
在这个模型中,第二个这样的模型,我们试图描述,概述,玩家的汽车如何与某些轨道特征相互作用。在这种情况下,我们将查看跳跃、反弹和助推修改器。这些功能都改变了悬浮车的物理特性。跳跃修改器将赛车弹出到空中,而反弹修改器将汽车反弹回它来的方向。
我们将在这个模型中包括的最后一个修改器是助推修改器。与我们在这里使用的其他修改器类似,助推修改器也改变了悬浮车的物理特性,并向前推动悬浮车。让我们来看看我们对这些游戏机制的计划,以及它们将如何与游戏的赛车交互。
图 2-2
汽车助推和跳跃修改器模型图描绘悬停赛车、启动标记和跳跃标记的图
本节的第一个模型,如前所示,演示了助推和跳跃修改器如何与悬停赛车交互。显示的修改器将使用标准的 Unity 碰撞交互来触发。 Bc 和 Dc 功能指示碰撞盒,当悬停赛车越过助推或跳跃修改器时,碰撞盒会进行检测。作为这次碰撞的结果,一个力将被施加到悬停赛车上。 Bf 车型特征表示施加到汽车上的助力方向。类似地, Df 模型特征指示施加到赛车上的跳跃力的方向。我们要看的下一个模型是反弹修改器模型。
图 2-3
汽车弹跳修改器模型图描述一个悬停赛车与弹跳屏障交互的图
这个部分的第二个模型在前面已经展示过了。在这个模型中,我们将看看反弹修改器以及它将如何与玩家的悬停赛车交互。“反弹”修改器的作用类似于“助推”和“跳跃”修改器,因为它使用碰撞盒来检测经过它的悬停赛车。当检测到这种碰撞时,会向汽车施加一个反映碰撞角度的力。这导致悬停赛车从反弹对象反弹并远离反弹对象。
我们要看的最后一个模型是 hover racer 的输入处理模型。我们将从鼠标和键盘的角度来解释控制汽车的输入。我们这样做是因为我们将鼠标和键盘视为 Windows、macOS 和 Linux 的默认本机支持。游戏支持多种输入源,但是我们会在后面的文章中提到。让我们看看!
图 2-4
汽车控制/输入图表描述基本悬停赛车控制的图表
在前面显示的图表中,我们从自上而下的角度绘制了基本输入。基本的键盘和鼠标控制显示在悬停赛车的有效运动旁边。这些模型和图表就像是游戏规范列表的可视化扩展。它们用于帮助可视化交互,这种交互在单独用文本描述时会更加复杂或混乱。
我们将在本文中回顾的游戏使用输入映射将多个输入转换成一组游戏输入。一个例子,汽车左转或右转,可以通过使用键盘、鼠标或游戏手柄来完成。输入被映射到由一系列游戏类处理的游戏控件,以创建第三人称悬停汽车控件。我们将在接下来的章节中详细回顾每个类和所有的控件。
第二章结论
在这一章中,我们已经制定了游戏的一般规范。第一套规范负责创建游戏的整体概观。我们的规约列表描述了实际的游戏,人工智能对手,菜单系统,音效,音乐,等等。这个列表粗略地描述了一些游戏机制和交互。
根据规约清单,我们回顾了一系列描述悬停赛车和不同赛道特性之间相互作用的图表。让我们在下面的列表中总结一下我们在本章中复习过的图表:
-
汽车、赛道、传感器图:描绘了用于检测悬停赛车的赛道上/赛道外事件的赛道模型和赛道传感器。还描述了用于检测当前汽车前方的其他悬停赛车的汽车传感器。
-
助推、跳跃图:描述了助推和跳跃修改器,以及它们的碰撞盒如何用于检测与当前悬停赛车的交互,并随后对汽车施加力以改变其在赛道上的路径。
-
弹跳图:描述了“弹跳”修改器,以及与悬停赛车的碰撞如何产生一个施加到汽车上的力,使其从轨迹的弹跳障碍处弹开。
-
悬停赛车输入/控制图:该图描述了基本的鼠标和键盘输入,以及它如何改变悬停赛车的运动。
本章中讨论的模型描述了一个实施功能的总体计划,例如通过轨道传感器实现轨道上/下功能,以及汽车检测其前方悬停赛车的能力。我们还为如何处理由轨迹物体和障碍物触发的助推、跳跃和反弹修改器制定了一个总体计划。最后,我们设计了一个简单的输入方案来控制悬停赛车。在接下来的章节中,我们将回顾驱动 Hover Racers 游戏的代码。通过查看代码和运行特殊的演示场景,您将详细了解每个游戏特性是如何实现的。
三、基础类
基类是 Hover Racers 游戏中大部分职业的基础。例如,如果你打开一个类,在类声明中看到下面的内容,“: BaseScript
”,那么这个类是通过BaseScript
类的扩展得到的MonoBehaviour
。让我们使用下一节中讨论的评审模板快速评审基本脚本类。
课堂复习模板
描述代码类的功能和使用可能很困难。某些类可能涉及许多上下文和配置。Unity 项目也不例外。我们将尝试通过使用以下模板详细复习课程来克服这个困难。为了阐明被上下文和配置模糊的类的各个方面,我们将尽可能演示类是如何工作的。
-
静态/常量/只读类成员
-
枚举
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
并非每堂课都有课堂复习模板每一部分的讨论主题。在这种情况下,我们将简单地省略那些没有提及的部分。如果你注意到有一节不见了,可以肯定地认为这一节不适用于当前的课程复习。
课堂复习:基础脚本
在这一节中,我们将回顾一下BaseScript
类,这样你就可以了解在本文中类回顾是如何处理的。BaseScript
类被用来方便地集中公共功能。使用这个类作为基类简化了游戏类,因为它负责每个类都需要的默认准备、字段和方法。我们不需要关心枚举,所以我们将在检查过程中跳过它,并遵循下面列出的步骤:
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
可以在“项目”面板的“标准资源”文件夹的三个目录之一中找到类文件:
-
\角色控制器\源\
-
\ mmg _ 脚本\
-
\ mmg _ scripts \演示脚本\
演示脚本不是本文的一部分,但是当您完成本文时,您将是代码库方面的专家,因此您可能打算自己查看它们。该过程将为相关演示场景带来更多价值。
静态/常量/只读类成员:BaseScript
BaseScript
类有几个非常重要的静态类成员让我们看看。首先,我们来看看这个类的静态类字段,SCRIPT_ACTIVE_LIST
。
public static Dictionary<string, bool> SCRIPT_ACTIVE_LIST = new Dictionary<string, bool>();
Listing 3-1BaseScript Static/Constants/Read-Only Class Members 1
该字段用于存储扩展BaseScript
并使用其准备方法的每个类的类初始化结果。这样,类初始化代码是集中的,而类初始化成功数据仅特定于扩展类。
1 public static bool IsActive(string sName) {
2 if (SCRIPT_ACTIVE_LIST.ContainsKey(sName)) {
3 return SCRIPT_ACTIVE_LIST[sName];
4 } else {
5 return false;
6 }
7 }
1 public static void MarkScriptActive(string sName, bool val) {
2 if (SCRIPT_ACTIVE_LIST.ContainsKey(sName)) {
3 SCRIPT_ACTIVE_LIST.Remove(sName);
4 }
5 SCRIPT_ACTIVE_LIST.Add(sName, val);
6 }
Listing 3-2BaseScript Static/Constants/Read-Only Class Members 2
前面列出了两个静态类方法供我们回顾。第一个条目是一个非常重要的方法,在 Hover Racer 的所有代码库中都被使用,即IsActive
方法。该方法被设计成将当前类的名称作为参数,并检查该类是否已经在活动脚本列表中注册。这种注册是BaseScript
类准备方法的一部分。列出的第二种方法是MarkScriptActive
方法。此方法用于设置脚本的关联值是真还是假,是活动的还是非活动的。这就是本复习部分的结论。接下来,我们将查看该类的字段。
类字段:BaseScript
BaseScript
类有许多类字段供我们查看。这些字段对于每个扩展了BaseScript
类的类都是通用的。我应该注意到,尽管它们可以通过扩展来使用,但并不是每个类都使用它们。
public PlayerState p = null;
public GameState gameState = null;
public bool scriptActive = true;
public string scriptName = "";
public AudioSource audioS = null;
Listing 3-3BaseScript Class Fields 1
p
字段是一个PlayerState
实例,用于引用相关玩家的状态信息。下一个条目是gameState
字段,它用于保存对游戏中央 state 类实例的引用。这恰好是GameState MonoBehaviour
的一个实例,也是一个 Unity 脚本组件,附加到一个标记为“GameState”的 Unity 游戏对象上下面的条目scriptActive
是一个布尔标志,其值表示当前脚本是否已经正确初始化。
scriptName
字段是一个字符串实例,它保存扩展了BaseScript
类的类的名称。最后,还有一个audioS
字段,它是AudioSource
组件的一个实例。因为音效很常见,所以这个字段被添加到了BaseScript
类中,以简化游戏代码库中的其他类。这就是本复习部分的结论。接下来,我们将看看这个类的相关方法和类定义。
相关的方法大纲/类头:BaseScript
这个BaseScript
类有几个方法需要我们介绍。让我们来看看。
//Main Methods
public bool Prep(string sName);
public bool PrepPlayerInfo(string sName);
//Support Methods
public void MarkScriptActive(bool val);
Listing 3-4BaseScript Pertinent Method Outline/Class Headers 1
随后列出的这个类头显示了类声明和任何基类或由BaseScript
类使用的接口。
using System.Collections.Generic;
using UnityEngine;
public class BaseScript : MonoBehaviour { }
Listing 3-5BaseScript Pertinent Method Outline/Class Headers 2
注意,这个类使用一些 C# 库来支持它的数据结构。还要注意,这个类扩展了MonoBehaviour
类。这意味着每一个扩展了BaseScript
类的类也是一个MonoBehaviour
,脚本组件,并且可以附加到 Unity GameObject
上。
支持方法详细信息:BaseScript
BaseScript 类只有一个我们需要关注的支持方法。让我们来看看!
1 public void MarkScriptActive(bool val) {
2 scriptActive = val;
3 MarkScriptActive(scriptName, scriptActive);
4 }
Listing 3-6BaseScript Support Method Details 1
MarkScriptActive
法短而甜。该方法更新第 2 行的scriptActive
类字段,然后通过调用第 3 行的静态版本的MarkScriptActive
方法来更新活动脚本注册表中的当前脚本。注意,方法调用使用类字段scriptName
和scriptActive
在脚本注册表中将脚本注册为活动或不活动。这就是支持方法回顾部分的结论。接下来,让我们把注意力转向类的主要方法。
主要方法详细信息:BaseScript
在课程的主要方法回顾部分,我们有两个方法要看。两者都用于通过查找和加载必要的 Unity GameObject
及其相关组件来准备BaseScript
类。
1 public bool Prep(string sName) {
2 scriptName = sName;
3 scriptActive = (bool)Utilities.LoadStartingSet(scriptName, out gameState)[2];
4 MarkScriptActive(scriptName, scriptActive);
5 return scriptActive;
6 }
1 public bool PrepPlayerInfo(string sName) {
2 scriptName = sName;
3 scriptActive = (bool)Utilities.LoadStartingSetAndLocalPlayerInfo(scriptName, out gameState, out PlayerInfo pi, out int playerIndex, out p, gameObject, true)[2];
4 MarkScriptActive(scriptName, scriptActive);
5 return scriptActive;
6 }
Listing 3-7BaseScript Main Method Details 1
前面列出的第一种方法是Prep
方法。这个方法是大多数扩展BaseScript
类的类使用的基本初始化调用。第 2 行,设置类’scriptName
’字段,然后调用实用程序类’LoadStartingSet
方法。这个方法执行寻找游戏对象、脚本组件等的实际工作。请注意,此方法在结果数组中返回的第二个值是一个布尔值,它指示初始化代码是否成功。该值存储在第 3 行的scriptActive
类字段中。在第 4 行,BaseScript
类的注册表被更新为调用MarkScriptActive
方法,该方法接收扩展类名和活动标志,第 4 行。活动标志在方法结束时返回。
列出的第二种方法类似于我们刚刚回顾的方法,但是它支持为我们获取更多的信息。有几个类使用了PrepPlayerInfo
方法,它们需要在初始化时加载起始集和当前玩家的状态。我们在这里进行区分的唯一原因是因为效率。为什么要做比我们需要的更多的工作?这就是Prep
方法的用途。请注意第 3 行的调用,它也返回一个布尔标志,指示存储在数组索引 2 中的操作成功。
还要注意,“out p
”方法参数用于从使用out
关键字的方法调用中更新类的“PlayerState
字段、p
。其余的方法与Prep
方法相同,所以我让你自己检查一下。这就是本复习部分的结论。在下一节中,我们将看一看运行中的类。
演示:BaseScript
虽然没有这个类的直接演示,但它几乎在任何地方都被使用,所以我们可以加载任何演示场景并获得相同的效果。打开 Hover Racer 的 Unity 项目并导航到“项目”面板。找到“场景”文件夹,找到名为“DemoCollideTrack”的场景。双击场景并加载它。一旦加载完成,导航到 Unity 编辑器的“层次”面板。请注意,场景元素是使用父对象和子对象来组织的,以创建类似于保存游戏对象的文件夹结构。
这不是一件坏事。不要认为添加这些空白的GameObject
和创建父母/孩子的关系会减慢你的游戏或任何事情。我们最多在场景中添加几个空的物体,这不会导致任何方式的减速。相反,你应该试着组织你的场景元素,这样你就可以快速找到你要找的东西。在这种情况下,我们正在寻找“环境”游戏对象。将其展开,并找到名为“Street_30m”的孩子。选择它,然后将注意力转向“检查器”面板。
在“检查器”面板中找到“道路脚本”条目并展开它。请注意,此处列出的字段没有设置。p
、gameState
、scriptActive
和scriptName
字段都没有初始化。我想说的是,我们可以使用 Unity 编辑器而不是代码来连接它。例如,如果我将GameState
对象从层次结构中拖放到检查器的gameState
字段中,并填充到scriptName
字段中,该数据将被设置并可供游戏使用。所以为什么不这样做,看起来更容易,对不对?
这样做的原因与你正在开发的游戏的范围和需求有很大关系。如果你的游戏很简单或者只是一个概念验证,那么使用这种方法是可以的。问题是在 Unity 编辑器中手工连接东西并不灵活,也不能适应复杂的游戏。例如,如果我改变场景并破坏GameState
对象,这将会破坏我建立的所有有线连接。一旦我完成了我正在处理的场景,我就必须回去调整它们。
通过使用编程方法,没有什么需要调整的。代码为我们找到GameObject
及其相关组件,并在出错时报告问题。这给了我们更多的力量和对比赛的控制。这就是我们想要的这种规模的游戏。游戏实际上是数据驱动的,这里的数据是一些预先配置的 Unity 游戏对象和脚本组件。赛道和球员是作为游戏的集中初始化过程的一部分加载的数据。这种方法确保每个类在运行时都有必需的组件引用。
回到演示。打开“检查器”面板,显示“道路脚本”组件的详细信息,运行演示场景。请注意,一旦场景启动,gameState
和scriptName
字段现在已正确设置。
考虑一下这个问题。这是一个重要的区别,您可能会遇到许多情况,您必须决定我是应该在编辑器中进行连接还是使用代码来连接。这就把我们带到了复习部分的结尾。
第二章结论
在这一章中,我们完成了一些重要的基础工作。我们回顾了被许多游戏类扩展的BaseScript
。
-
课程复习模板:我们看了我们将用来复习游戏课程的框架,并试图解释和理解它们是如何运作和使用的。
-
BaseScript 类回顾:我们详细回顾了
BaseScript
类,它是游戏中许多类扩展的重要基类。 -
使用模板:我们演示了如何将课堂复习模板应用到实际的课堂中。
-
硬连线和代码连线之间的区别:我们很快讨论了硬连线连接和代码连线连接之间的区别,硬连线连接使用 Unity 编辑器来设置,代码连线连接使用脚本组件来建立
GameObject
和组件之间的连接。
这就引出了本章的结论。尽管这是一个很短的章节,但它包含了很多关于在 Hover Racers 游戏中使用和连接GameObject
的重要信息,同时也包含了在 Unity 中构建游戏的一般信息。
四、交互类
欢迎来到交互类这一章。在这一章中,我们将深入这个项目,并开始回顾支持悬停赛车修改器和交互的类。我有时将这些交互称为游戏机制,反之亦然,但这有点不准确。让我澄清一下。我们在这里讨论的交互是由 Unity 引擎驱动的。他们使用基于MonoBehaviour
的类和碰撞盒来确定悬停赛车何时触发碰撞事件。
此时,玩家悬停赛车的状态被更改,以应用由碰撞触发的修改器。从 Unity 引擎交互到游戏状态调整的转换是游戏机制的实际应用。因为这一切都是按顺序发生的,而且非常快,所以很容易忽略这种区别。打开 Hover Racers Unity 项目,找到“项目”面板。如果面板不可见,转到主菜单,选择“窗口”➤“面板”➤项目一旦面板可见,展开文件夹,直到您可以在根目录中看到“标准资产”文件夹。在我们看任何代码之前,让我们先看一下我们要复习的类的列表。我们将在本章中回顾的类如下:
-
反弹脚本
-
loadscript
-
航点检查
-
TrackHelpScript
现在我们已经解决了所有这些问题,让我们直接进入一些代码。双击位于“资产”➤“标准资产”➤“mmg _ scripts”中名为“BounceScript”的脚本,在 Visual Studio 中打开它。
课堂回顾:反弹脚本
BounceScript
类负责触发悬停赛车的反弹修改器。这种游戏机制出现在使用反弹屏障的赛道上。你可以知道你何时撞上了一个活动的反弹屏障,因为你的悬停赛车将向屏障的相反方向飞去。我们将使用前面提到的课程复习模板来复习课程。相关部分如下所示:
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
没有相关的枚举可言,所以我们将跳过回顾部分。当我们回顾代码时,我希望你试着想象这个类和它的方法是如何被使用的。不要带着任何具体的观察走开,直到你把你的想法和这个班级的演示场景放在一起。
类字段:BounceScript
我将根据上下文列出大约 5 到 15 个类别的字段。有些字段是私有的,仅供类方法内部使用。我不会总是列出这些字段,也不会总是详细介绍它们。把它们看作是局部变量,但是在类级别注册。因为这个设计决策在代码中出现了很多次,所以我想稍微讨论一下。Unity 使用 C# 作为主要的脚本语言。原来 C# 是一种托管内存语言。
这意味着 C# 程序运行垃圾收集器,监视对象引用计数,并从未引用的对象中清除任何未使用的内存。这是对一个相当复杂的过程的一句话描述,所以让我们忽略任何不准确的地方。这里的要点是,仅仅因为有一个垃圾收集器在运行,并不意味着我们应该给它任何工作。
一个被滥用的垃圾收集器最终会有很多工作要做。这反过来会对您的游戏性能产生负面影响,并可能影响帧速率和用户体验。为了尽可能地防止垃圾收集器运行,局部方法变量被移到私有类字段中。这避免了在方法调用时分配和释放内存的需要,因为使用中的对象仍然被该类引用。
我在更新调用和事件处理程序中使用这种有意义的方法。在我看来,任何有可能每秒运行几次的代码都应该调整为使用私有类字段,并减少垃圾收集。尽管如此,也不要滥用这种方法。如果一个方法可以很好地处理局部变量,就让它去吧。你可以随时回来重构代码。让我们看看如下列出的第一组类字段。
public float bounceDampener = 0.75f;
public float minBounceVelocityHor = 20.0f;
public float minBounceVelocityVer = 20.0f;
private bool useReflect = false;
Listing 4-1BounceScript Class Fields 1
bounceDampener
字段用于减小 X 和 Z 轴上的初始速度。应用此修改器时,Y 轴(垂直轴)保持为零。接下来的两个类字段,minBounceVelocityHor
和minBounceVelocityVer
,用于保持反弹修改器速度的最小强度。这确保了在较低的碰撞速度下反弹效果显著。如果有疑问,就用漫画式的、稍微夸张的物理学。它们在游戏中更有趣,如果需要的话,你可以随时把它调回来。
字段列表中的下一个是useReflect
布尔标志。该字段控制用于确定汽车弹跳速度向量的计算。通常在游戏开发中,你会遇到这样的情况,你可以用更复杂但更精确的数学来描述游戏的物理特性。我的建议是首先使用一个简单的“模拟”方法,看看你是否能让事情正常运行。
我推荐这种方法的唯一原因是,通常情况下,简化的方法无法察觉高级数学。在这种情况下,您将受益于更高效的实现。我提出这个建议的第二个原因是,现实世界的物理学并不有趣。再次,想想卡通物理学。无论如何,让我们回到课堂实地审查。其余的类字段是私有的,由类方法内部使用。让我们来看看。
//***** Internal Variables: BounceObjOff *****
private Vector3 v3;
private float x;
private float y;
private float z;
//***** Internal Variables: OnTriggerEnter *****
private CharacterMotor cm = null;
Listing 4-2BounceScript Class Fields 2
如前所述,前面列出的字段由类’BounceOffObj
和OnTriggerEnter
方法内部使用。v3
字段用于表示最终的反弹速度矢量。后续字段x
、y
和z
用于计算v3
、Vector3
字段分量的速度。最终结果是一个三维向量,其 x 和 z 分量的值和 y 分量的值为零。这构成了弹跳效果,并应用于悬停赛车的运动。
我们要看的最后一个类字段由OnTriggerEnter
方法使用。通常,在整个游戏代码中,我们需要从一条信息、一个游戏对象或一个碰撞事件连接回保存玩家和游戏状态信息的类。CharacterMotor
区域cm
是一个允许玩家的悬浮赛车移动的组件。这个类是 Unity 2.x 的FPSWalker.js
脚本的继承者,也是 Unity 5.x 的FirstPersonController.cs
脚本的前身。是的,曾经有一段时间,你可以同时使用 JavaScript 和 C# 在 Unity 中编码。实际上,这个游戏最初是作为 JavaScript 和 C# 混合项目实现的。接下来,我们将看看类的相关方法列表和类头。
相关的方法大纲/类头:BounceScript
BounceScript
类的相关方法列表如下。
//Support Methods
public void OnTriggerEnter(Collider otherObj);
//Main Methods
public void BounceObjOff(GameObject go, Collider otherObj, PlayerState p, CharacterMotor cm);
void Start();
Listing 4-3BounceScript Pertinent Method Outline/Class Headers 1
class '头显示了 import 语句和 class '声明,包括它扩展的任何基类或它实现的接口。
using UnityEngine;
public class BounceScript : BaseScript {}
Listing 4-4BounceScript Pertinent Method Outline/Class Headers 2
注意,BounceScript
类是一个MonoBehaviour
类,因为它扩展了基类BaseScript
,后者又扩展了 Unity 引擎的MonoBehaviour
类。我们将首先看一下类的支持方法。我将从第一个开始标记每个方法的行号。当我们回顾不同的类时,请随意跟随 Unity 编辑器和 Visual Studio。
支持方法详细信息:BounceScript
BounceScript
类有一个支持方法,我们接下来会看到。有时,我们会遇到一个拥有大量简单支持方法的类,比如 get、set 和 show、hide 方法。方法,因为这些方法简单而直接,所以只需查看很少的代码。在这种情况下,我们需要检查一些代码。我们来看看下面这个方法。
01 public void OnTriggerEnter(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 cm = null;
07 if (otherObj != null && otherObj.gameObject != null && otherObj.gameObject.CompareTag(Utilities.TAG_PLAYERS)) {
08 Utilities.LoadPlayerInfo(GetType().Name, out PlayerInfo pi, out int playerIndex, out p, otherObj.gameObject, gameState, false);
09 if (p != null) {
10 cm = p.cm;
11 }
12
13 if (p != null && cm != null && p.isBouncing == false) {
14 BounceObjOff(otherObj.gameObject, otherObj, p, cm);
15 }
16 }
17 }
Listing 4-5BounceScript Support Method Details 1
这个方法是一个碰撞检测回调方法,作为 Unity 游戏引擎的游戏对象交互的一部分被触发。我在简化事情。碰撞检测涉及到更多的组件,但是我们将在课程回顾的演示部分更详细地介绍它们。这个方法获得一个碰撞器对象作为碰撞事件的一部分传递给它。在第 2–4 行,如果该类遇到了配置问题,并在BaseScript
类的初始化结果Dictionary
、SCRIPT_ACTIVE_LIST
中注册了一个假值,则该类不做任何工作就被转义。
如果定义了otherObj
参数,具有有效的gameObject
字段,并标记为“玩家”GameObject
,那么我们处理碰撞,第 7 行。在第 8 行,对实用方法LoadPlayerInfo
的调用使用GameObject
的PlayerInfo
脚本组件来找出玩家的索引,然后使用该信息在游戏的中央状态类GameState
中查找玩家的状态类PlayerState
。结果是类字段p
被设置,并可用于检查该玩家的角色运动cm
是否被定义,第 9 行。
如果定义了必要的字段,并且玩家的悬停赛车还没有弹跳,那么我们通过调用BounceOffObj
方法来应用弹跳修改器,第 13–14 行。这就结束了我们对类的支持方法的回顾。接下来我们将看看这个类的主要方法。
主要方法详细信息:BounceScript
我们复习的方法主要有两种。首先是Start
法。该方法是 Unity 引擎组件架构的一部分。简而言之,游戏中的每个组件都有一个Start
和一个Update
回调方法。在组件生命周期的开始,调用一次Start
方法。特别是,这种方法被认为是准备和配置的要点。让我们开始写代码吧!
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
08 audioS = GetComponent<AudioSource>();
09 if (audioS == null) {
10 Utilities.wrForce(scriptName + ": audioS is null!");
11 }
12 }
Listing 4-6BounceScript Main Method Details 1
Start
方法负责初始化类,同样,如果不满足初始化要求并且类被标记为不活动,它也能够停用类。通过调用第 2 行的Prep
方法进行初始化。注意,该方法的第一个参数是这个类的名称。该字符串用于注册初始化的结果,并将类标记为活动或非活动。第 3 行,我们测试初始化结果是否成功。
我选择添加 disable-class 特性来防止类抛出大量异常。如果这个类碰巧用在了一个Update
方法中,那么可能会记录大量的异常。这种情况会堵塞“控制台”面板,隐藏异常的最初原因。最后,在第 8–11 行,我们加载了一个AudioSource
组件,如果可用的话,在应用反弹修改器时用作声音效果。我们要看的下一个主要方法是BounceOffObj
方法。让我们开始写代码吧!
01 public void BounceObjOff(GameObject go, Collider otherObj, PlayerState p, CharacterMotor cm) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 v3 = Vector3.zero;
07 x = 0;
08 y = 0;
09 z = 0;
10 p.isBouncing = true;
11
12 x = cm.movement.velocity.x;
13 if (useReflect == true) {
14 x = x * bounceDampener;
15 } else {
16 x = x * -1 * bounceDampener;
17 }
18
19 if (x < 0) {
20 if (x > -minBounceVelocityHor) {
21 x = -minBounceVelocityHor;
22 }
23 } else if (x >= 0) {
24 if (x < minBounceVelocityHor) {
25 x = minBounceVelocityHor;
26 }
27 }
28
29 z = cm.movement.velocity.z;
30 if (useReflect == true) {
31 z = z * bounceDampener;
32 } else {
33 z = z * -1 * bounceDampener;
34 }
35
36 if (z < 0) {
37 if (z > -minBounceVelocityHor) {
38 z = -minBounceVelocityHor;
39 }
40 } else if (z >= 0) {
41 if (z < minBounceVelocityHor) {
42 z = minBounceVelocityHor;
43 }
44 }
45
46 if (useReflect == true) {
47 v3 = Vector3.Reflect(v3, otherObj.ClosestPointOnBounds(go.transform.position).normalized);
48 } else {
49 v3 = new Vector3(x, y, z);
50 }
51
52 cm.movement.velocity = v3;
53 if (audioS != null) {
54 if (audioS.isPlaying == false) {
55 audioS.Play();
56 }
57 }
58 p.isBouncing = false;
59 }
Listing 4-7BounceScript Main Method Details 2
类似于OnTriggerEnter
方法,BounceObjOff
方法受到在第 2-4 行执行的IsActive
方法调用和检查的保护。第 6–10 行的小代码块用于初始化方法的局部变量,在本例中是私有类字段。该方法创建一个反弹向量,调整速度以使悬停赛车弹开。反弹向量的x
分量在第 12–27 行计算。本地x
组件用悬停赛车的x
组件的速度初始化。
如果useReflect
被启用,则执行不同的计算。首先,如果useReflect
为假,则x
分量被抑制和反射。这是违反直觉的;让我解释一下。如果useReflect
为真,我们使用统一反映计算。如果没有,我们使用一个简单的模拟反射。第 19–27 行的代码块用于在x
和z
轴上强制最小反弹速度。您也可以将它们视为一个Vector3
实例的x
和z
组件。
一个非常相似的过程用于设置第 29–44 行的速度矢量的z
分量。因为我们在游戏中排除了垂直调整,这就是我们准备反弹速度向量所要做的。如果你看一下第 46–50 行,你会看到反弹速度向量的创建。在这段代码中,如果useReflect
标志为真,则该类使用 Unity reflect 方法;否则,使用非常简单的模拟反射,第 47 行。
在第 52 行可以找到一行微妙但重要的代码。这是悬停赛车游戏对象的角色马达调整其运动向量的地方。最后,在第 52-56 行,如果设置了音效AudioSource
,则播放声音以指示弹跳,并且玩家状态标志isBouncing
被设置为假,第 58 行。在该方法的第 10 行,该字段被设置为 true。使用该标志可以防止反弹修改器重叠。我们对BounceScript
类的复习到此结束。接下来,我们将看到这个类是如何工作的。
演示:反弹脚本
有一个场景是专门设计来演示BounceScript
的动作的。找到“项目”面板,并找到“场景”文件夹。找到并打开名为“DemoCollideBounce”的场景。在开始演示之前,我们先来说说这里是怎么回事。几秒钟后,你将能够启动场景并控制悬停赛车,就像在游戏的正常使用中一样。你可以使用第二章中列出的基本键盘和鼠标控制来控制悬停赛车。
在演示场景中,有三根彩色柱子被四个反弹屏障包围。每个组都以不同的方式配置。绿色柱子周围的栅栏是用来弹跳汽车的。这些屏障有一个勾选了“触发”复选框的碰撞箱。这表明碰撞物理将由脚本而不是默认的物理引擎来处理。碰到这个障碍将会运行我们刚刚复习过的类中的可操作代码。汽车会被弹开。
下一个要讨论的支柱是红色支柱。这个柱子周围也有屏障,但这些屏障的配置有点不同。这些障碍也有BoxCollider
,但是它们没有选中“是触发器”框。为了表明脚本不再重要,障碍使脚本失效。碰撞这些障碍将导致默认的物理行为。汽车会直接撞上并停在栅栏前。试试看!
最后一组要讨论的障碍围绕着紫色柱子。这些障碍是错误配置的一个例子。在这种情况下,障碍有一个勾选了“是触发”框的BoxCollider
,就像绿色柱子一样。这里的区别在于,这些障碍没有处理碰撞事件的活动脚本。你会注意到在这种情况下,汽车可以直接穿过护栏。
图 4-1
弹跳脚本演示场景从自上而下的角度显示弹跳演示场景的图像
前面显示的图像描述了用于本课程的演示场景。这就是我们对BounceScript
课复习的总结。下一个要复习的课程是RoadScript
。这个脚本的行为有点类似于BounceScript
。
课堂回顾:道路脚本
RoadScript
类负责检测玩家的悬停车是否主动在赛道上。在游戏中使用时,赛道的每一块都有一个附带的RoadScript
组件。这确保玩家的悬停赛车在赛道上或赛道外的状态一直受到监控。本课程复习的相关部分如下所示:
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
没有相关的枚举或静态类成员,所以我们将跳过这一部分。同样,当你阅读课程回顾时,试着想象正在使用的课程。试着保留最后的判断,直到你看了班上的示范。
类字段:RoadScript
RoadScript
类有几个字段供我们回顾。其中一些字段是私有的,仅供某些类方法内部使用。对于私有的内部字段,我们就不赘述了。这种设计决策的原因是私有类字段由该类引用,只要该类在使用中,垃圾收集器就不会触及这些字段。
//***** Class Fields *****
private float delay = 5.0f;
private PlayerState sdp;
//***** Internal Variables: OnTrigger Methods *****
private PlayerState pEntr = null;
private PlayerState pStay = null;
private PlayerState pExit = null;
Listing 4-8RoadScript Class Fields 1
第一个类字段delay
用于为玩家的悬停赛车添加 5 秒的延迟,使其脱离赛道设置为真。接下来的字段sdp
是一个PlayerState
对象,它引用了玩家汽车的当前状态。接下来的三个类字段由类的碰撞事件处理程序用来保存对与碰撞对象相关的播放器状态的引用(如果有的话)。
相关的方法大纲/类头:RoadScript
相关方法的列表如下。
//Main Methods
void Start();
//Support Methods
public void OnTriggerEnter(Collider otherObj);
public void OnTriggerStay(Collider otherObj);
public void OnTriggerExit(Collider otherObj);
public void SpeedUp(PlayerState p);
public void SlowDown(PlayerState p);
public void RunSlowDown();
Listing 4-9RoadScript Pertinent Method Outline/Class Headers 1
随后列出的这个类头显示了类声明和任何基类或由RoadScript
类使用的接口。
using UnityEngine;
public class RoadScript : BaseScript {}
Listing 4-10RoadScript Pertinent Method Outline/Class Headers 2
让我们来看看这个类的支持方法。
支持方法详细信息:RoadScript
RoadScript
类有一些支持方法供我们回顾。我们将从查看碰撞事件处理程序开始。让我们跳到一些代码中。
01 public void OnTriggerEnter(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null) {
07 Utilities.LoadPlayerInfo(GetType().Name, out PlayerInfo pi, out int playerIndex, out pEntr, otherObj.gameObject, gameState, false);
08 if (pEntr != null) {
09 SpeedUp(pEntr);
10 }
11 }
12 }
01 public void OnTriggerStay(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null) {
07 Utilities.LoadPlayerInfo(scriptName, out PlayerInfo pi, out int playerIndex, out pStay, otherObj.gameObject, gameState, false);
08 if (pStay != null) {
09 SpeedUp(pStay);
10 }
11 }
12 }
01 public void OnTriggerExit(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null) {
07 Utilities.LoadPlayerInfo(GetType().Name, out PlayerInfo pi, out int playerIndex, out pExit, otherObj.gameObject, gameState, false);
08 if (pExit != null && pExit.isJumping == false && pExit.boostOn == false) {
09 sdp = pExit;
10 Invoke(nameof(RunSlowDown), delay);
11 }
12 }
13 }
Listing 4-11RoadScript Support Method Details 1
因为前面列出的所有三种方法都遵循几乎相同的模式,所以我在这里只详细回顾一下OnTriggerExit
方法。将这些知识应用到OnTriggerEnter
和OnTriggerStay
方法中。首先,在第 2–4 行,我们有活动脚本检查。如果返回 false,那么该方法不做任何工作就返回。这是该类“故障安全锁定”的一部分。请注意,锁定会影响脚本的所有实例。同样,当类配置失败时,我们锁定类的功能。这可以防止一连串错误堵塞控制台输出。请记住,这个特性总是可以在生产版本中被注释掉。
在第 6 行,如果定义了碰撞对象,我们用它来加载相关玩家的PlayerState
类,如果有的话,在第 7 行调用 Utilities 类的LoadPlayerInfo
方法。如果合适的标准匹配,在第 8 行,我们将本地字段sdp
设置为我们刚刚碰撞的游戏对象的PlayerState
,并调用RunSlowDown
方法。因为OnTriggerExit
方法在玩家的悬停赛车离开道路传感器时触发,所以我们调用减速方法来激活脱离赛道速度惩罚。
碰撞事件处理程序方法的 stay 和 enter 版本会将玩家的汽车加速到正常速度。exit collision 事件处理程序方法将使玩家的汽车减速到我们刚刚看到的偏离赛道的速度。注意,在第 10 行,通过用要运行的方法的名称和一个时间延迟调用类’Invoke
,间接地调用了RunSlowDown
方法。在这种情况下,玩家或 AI 对手在减速惩罚触发前有几秒钟的时间回到赛道。
1 public void SlowDown(PlayerState p) {
2 if (p != null && p.offTrack == false && p.controller.isGrounded == true) {
3 p.offTrack = true;
4 p.SetSlow();
5 }
6 }
1 public void RunSlowDown() {
2 SlowDown(sdp);
3 }
1 public void SpeedUp(PlayerState p) {
2 if (p != null && p.offTrack == true) {
3 p.offTrack = false;
4 p.SetNorm();
5 }
6 }
Listing 4-12RoadScript Support Method Details 2
前面详述的一组支持方法中列出的第一种方法是SlowDown
方法。该方法通过调用PlayerState
class’ SetSlow
方法将玩家状态设置为脱离赛道,并降低悬停赛车的最大速度。列出的下一个方法RunSlowDown
,用于从对Invoke
方法的调用中执行SlowDown
方法,正如我们前面看到的。
我们要看的最后一个方法是SpeedUp
方法。这种方法实质上逆转了SlowDown
方法的效果。在该方法的第 3-4 行,悬停赛车被标记为在赛道上,其速度被设置为悬停赛车在赛道上可以达到的正常速度。我们要看的下一部分是主要的方法回顾。让我们看一看,好吗?
主要方法详细信息:RoadScript
RoadScript
类只有一个主要方法让我们复习。无处不在的Start
方法。
1 void Start() {
2 base.Prep(this.GetType().Name);
3 if (BaseScript.IsActive(scriptName) == false) {
4 Utilities.wrForce(scriptName + ": Is Deactivating...");
5 return;
6 }
7 }
Listing 4-13RoadScript Main Method Details 1
这个方法应该看起来很熟悉。我们以前看到过类似的实现,我们肯定会再次看到它们。在该方法的第 2 行,我们看到对基类BaseScript
的集中式类准备方法Prep
的调用。这个方法调用加载所有需要的默认类字段,并引用游戏对象和脚本组件。如果准备调用失败,那么在BaseScript
类的注册表中,这个名为的类被注册为一个假值。请注意,在第 4 行,有一个日志条目表明该类有问题。这就结束了主要的方法评审。接下来,我们将演示该类的实际操作。
演示:RoadScript
有一个场景是专门设计来演示道路脚本的。找到“项目”面板,并找到“场景”文件夹。找到并打开名为“DemoCollideTrack”的场景。在我们开始演示之前,让我描述一下场景是如何工作的。在这个场景中是一段 30 米长的轨道,有一个RoadScript
脚本组件和一个碰撞器,具有正确的触发标志设置。
您会注意到在运行演示的左下角有一些额外的文本。该文本指示悬停赛车的脱离赛道状态以及赛车脱离赛道的时间(毫秒)。请注意,当您偏离轨道时,偏离轨道标志不会立即触发。在标志翻转之前有几秒钟的延迟,所以请耐心等待。运行演示并检查东西。用户控制需要几秒钟才能激活。这个游戏实际上在运行一个看不见的倒计时,就像在真实游戏中一样。接下来,我们将看看WaypointCheck
脚本。
图 4-2
RoadScript 演示场景以第三人称视角展示赛道上/赛道外演示场景的图像
课堂复习:中途检查
WaypointCheck
脚本负责检测赛道上赛车的方向和大致位置。我想花点时间对游戏的航路点系统做一个深入的了解。游戏中的 AI 对手使用航路点系统在赛道上导航。让我们来看看这个模型的示意图。
图 4-3
赛道航路点模型图描述使用航路点定义赛道的图表
这些路点为游戏的人工智能玩家提供了一个脚手架。它还提供了一种快速确定悬停赛车在赛道上的大致位置的方法。它可以用来指示汽车的方向,并随后检测汽车是否在错误的方向上行驶。让我们来看看人工智能的对手是如何确定前进方向的。下图显示了简化模型中当前实现的逻辑。
图 4-4
人工智能路点逻辑模型图描述使用路点定义人工智能玩家方向的图
在此图中,悬停赛车使用指向下两个航路点中心的已知矢量。取这些向量的平均值, G ,并用于引导汽车。这些路点也有可以让人工智能汽车减速的指示器。这有助于在转弯、转弯和跳跃时控制车辆。这涵盖了所有的先决条件的材料。让我们开始课堂复习。我们将涉及的相关部分如下所示:
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
现在,让我们从第一部分开始复习。
类字段:航路点检查
我们要查看的第一组类字段是公开的,可以通过 Unity 编辑器的“Inspector”面板查看。
public int waypointRoute = 0;
public int waypointIndex = 0;
public float waypointStartY = 4;
public float waypointSlowDown = 1.0f;
public bool isSlowDown = false;
public float slowDownDuration = 100.0f;
Listing 4-14WaypointCheck Class Field 1
前面列出的第一个类字段是waypointRoute
字段。这个字段实际上在游戏中并没有使用,所有的东西都默认为路线 0,但是如果你想进一步实现路点路线,我们还是会回顾一下。下面列出的类字段是waypointIndex
字段。该字段应在 Unity 编辑器的等级构建过程中设置。重要的是用递增的索引对航路点进行适当的编号。但是,您可以等到航路点放置过程结束时再这样做。
waypointStartY
域用于给航迹航路点分配一个 Y 值。当玩家的悬停赛车脱离赛道、倒退太久或落入水中后,将该赛车放回赛道时使用。它可以用来确保悬停赛车在更换时处于正确的高度。waypointsSlowDown
字段设置一个值,指示应用于 AI 玩家的减速量。布尔型isSlowDown
是一个标志字段,表明减速动作是活跃的,应该适用于人工智能控制的悬停赛车。该组中列出的最后一个字段是slowDownDuration
字段。该字段控制减速应用于汽车的时间长度。在下一个复习部分,我们将看看这个类的相关方法。
相关的方法大纲/类头:WaypointCheck
下面列出了WaypointCheck
类的相关方法。
//Main Methods
void Start();
//Support Methods
public void OnTriggerEnter(Collider otherObj);
public void ProcessWaypoint(Collider otherObj);
Listing 4-15WaypointCheck Pertinent Method Outline/Class Headers 1
随后列出的类头显示了类声明和任何基类或接口。
using UnityEngine;
public class WaypointCheck : BaseScript {}
Listing 4-16WaypointCheck Pertinent Method Outline/Class Headers 2
让我们先看看类的支持方法。
支持方法详细信息:航点检查
WaypointCheck
类很少有支持方法让我们回顾。我们将看看触发器事件处理程序及其相关的支持方法。
01 public void OnTriggerEnter(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null) {
07 ProcessWaypoint(otherObj);
08 }
09 }
01 public void ProcessWaypoint(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null && otherObj.gameObject.CompareTag(Utilities.TAG_PLAYERS)) {
07 Utilities.LoadPlayerInfo(GetType().Name, out PlayerInfo pi, out int playerIndex, out p, otherObj.gameObject, gameState, false);
08 if (p != null) {
09 if ((waypointIndex + 1) < p.aiWaypointIndex && ((waypointIndex + 1) - p.aiWaypointIndex) <= 3) {
10 p.wrongDirection = true;
11 } else {
12 p.wrongDirection = false;
13 }
14
15 if ((waypointIndex + 1) > p.aiWaypointIndex && ((waypointIndex + 1) - p.aiWaypointIndex) <= 5) {
16 if (p.aiWaypointLastIndex != p.aiWaypointIndex) {
17 p.aiWaypointPassCount++;
18 }
19
20 p.aiWaypointLastIndex = p.aiWaypointIndex;
21 p.StampWaypointTime();
22
23 if (p.IsValidWaypointIndex(waypointIndex + 1) == true) {
24 p.aiWaypointIndex = (waypointIndex + 1);
25 } else {
26 if (p == gameState.GetCurrentPlayer() && gameState.gameWon == false) {
27 gameState.LogLapTime(p);
28 p.lapComplete = true;
29 }
30
31 p.aiWaypointJumpCount = 0;
32 p.aiWaypointPassCount = 0;
33 p.aiWaypointIndex = 0;
34 if (p.currentLap + 1 <= p.totalLaps) {
35 p.currentLap++;
36 }
37 p.ResetTime();
38 }
39
40 if (p.aiWaypointIndex == 1 && p.currentLap == gameState.totalLaps && playerIndex == gameState.currentIndex) {
41 //game over
42 if (gameState.IsStartMenuShowing() == false) {
43 gameState.gameWon = true;
44 gameState.SetPositions();
45 gameState.ShowEndMenu();
46 }
47 }
48 } else {
49 p.skippedWaypoint = true;
50 }
51
52 if (p.aiOn == true) {
53 if (isSlowDown == true) {
54 p.aiSlowDownTime = 0f;
55 p.aiSlowDownDuration = slowDownDuration;
56 p.aiSlowDownOn = true;
57 p.aiSlowDown = waypointSlowDown;
58 }
59 }
60 }
61 }
62 }
Listing 4-17WaypointCheck Support Method Details 1
正如我们之前所见,调用OnTriggerEnter
回调方法是为了响应WaypointCheck
组件的盒子碰撞器和悬停赛车之间的碰撞。正如我们之前看到的,如果类没有正确配置,第 2–4 行的代码会阻止事件处理方法做任何工作。在第 6–8 行,如果给定的碰撞对象不为空,则调用ProcessWaypoint
方法,第 7 行。
ProcessWaypoint
方法在第 2–4 行具有相同的逸出故障保护。如果定义了碰撞对象,并且它有玩家标签,第 6 行,那么我们继续加载与该玩家相关的PlayerState
数据,第 7 行。如果在第 8 行找到了玩家的状态对象,那么在处理路点的过程中,我们需要检查一些东西。我们需要检查的第一件事是汽车是否在倒车。该检查在第 9–13 行处理。航路点索引增加 1,以使检测更灵活,与汽车的最后一个航路点进行比较。
如果汽车的航路点在当前航路点之前,并且仅领先三个或更少,那么我们在第 10 行将悬停赛车标记为反向行驶。如果没有,我们将该字段的值设置为 false,第 12 行。在第 15 行,我们检查该航路点是否比悬停参赛者的当前航路点领先 5 或更少的值;然后我们继续处理它。执行快速检查以查看悬停赛车是否通过了一个航路点,确保赛车的前一个航路点不等于其当前航路点。如果是,悬停赛车的aiWaypointPassCount
将递增。
接下来,在第 20 行,玩家的aiWaypointLastIndex
被更新为汽车的前一个路点的值,在第 21 行,调用StampWaypointTime
方法来更新路点时间戳。请注意,有时我可能会将悬停赛车称为玩家,因为赛车是玩家在游戏中的代表。代码片段,第 23–38 行,用于检测我们是否到达了赛道上的最后一个路点,并为下一圈重置一些值。在第 26-29 行,如果当前玩家正在与最后一个路点交互,而比赛还没有结束,那么我们记录一圈时间并标记该圈已经完成。
这个方法的下一个责任是检查游戏是否已经结束。这由第 40–47 行的代码片段处理。如果我们已经到达当前圈的最后一个路点,这是比赛的最后一圈,我们检查开始菜单是否显示在第 42 行。如果没有,意味着我们没有运行比赛的 AI 演示,那么我们标记游戏结束,刷新赛车的位置,然后显示结束菜单屏幕。
最后但同样重要的是,该方法负责通过将减速数据复制到第 53 到 58 行的 AI 玩家状态中来指导 AI 玩家。请注意,仅当该航路点的isSlowDown
字段设置为真时,才会设置数据。这就结束了该类的支持方法。接下来,我们将看看这个类的主要方法。
主要方法详细信息:航路点检查
别担心,WaypointCheck
类只有一个主要方法让我们看一看。花点时间看看这里列出的类的开始方法。
1 void Start() {
2 base.Prep(this.GetType().Name);
3 if (BaseScript.IsActive(scriptName) == false) {
4 Utilities.wrForce(scriptName + ": Is Deactivating...");
5 return;
6 }
7 }
Listing 4-18WaypointCheck Main Method Details 1
这个 start 方法提供了与我们之前看到的其他类的 start 方法相同的类初始化。因此,我不会在这里赘述。在继续下一步之前,请务必通读该方法并理解其工作原理。你会在每个扩展了BaseScript
类的游戏类中看到非常相似的代码。本章中我们将讨论的最后一个类是TrackHelpScript
。就像我们到目前为止已经讨论过的类一样,TrackHelpScript
类是一个独立的碰撞检测器。它提供了在音轨首次使用时触发帮助通知的交互。在我们开始课程复习之前,让我们先来看一下WaypointCheck
课程的演示场景。
演示:航路点检查
WaypointCheck
班有演示场景让我们去看看。在我们看它之前,让我们先讨论一下这个场景是做什么的。首先,像其他每个演示场景一样,在你实际控制悬停赛车之前会有一点延迟。别担心,这不是一个 bug,游戏代码已经调整为支持演示场景,但它仍然有一些游戏功能,如比赛开始时的倒计时。你不会看到倒计时数字,但在短短几秒钟内,你就能控制汽车。
如果你沿着与场景的路点相反的方向驾驶汽车,柱子会变红或保持红色。如果你沿同一方向比赛,增加路标指数,柱子会变成绿色或保持绿色。如果你点击“层级”面板中的每个航路点,并在“检查器”面板中查看其详细信息,你会注意到其中一个配置了减速信息。这不会影响你的车,因为减速信息只被人工智能玩家使用。
你可以在“项目”面板的“场景”文件夹中找到这个演示场景。寻找名为“DemoCollideWaypoint”的场景。打开它,玩一会儿。看看场景是如何设置的,并特别注意路点及其与玩家汽车的交互。这个类的演示场景截图如下。
图 4-6
航路点检查演示场景 2A 航路点演示场景截图
图 4-5
航路点检查演示场景 1A 航路点演示场景的俯视图
课堂回顾:TrackHelpScript
TrackHelpScript
类被设计用来在玩家第一次比赛时显示帮助信息。我想谈谈一个小小的实现警告。游戏的 HUD 支持三个这样的帮助信息。游戏的 HUD 屏幕上显示加速、减速和转弯帮助通知。然而,加速帮助通知不受TrackHelpScript
控制。
三个支持的帮助消息中的第一个实际上显示为比赛开始代码的一部分。正如我之前提到的,TrackHelpScript
类是一个独立的碰撞事件处理程序,与我们在本章中看到的其他类非常相似。我们将使用下面的课程回顾部分来介绍本课程。
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
没有相关的静态类成员、枚举或类字段需要查看,所以我们将跳过这些部分。同样,当我们逐步浏览TrackHelpScript
类的不同复习部分时,试着想象一下正在使用的类。我们要看的第一部分是课程的相关方法大纲。让我们来看看。
相关的方法大纲/类头:TrackHelpScript
TrackHelpScript
类的相关方法如下。
//Main Methods
void Start();
//Support Methods
public void OnTriggerEnter(Collider otherObj);
public void ProcessTrackHelp(Collider otherObj);
Listing 4-19TrackHelpScript Pertinent Method Outline/Class Headers 1
接下来,我将列出类头、导入语句和类声明。请特别注意所使用的任何基类。如果需要的话,花点时间复习一下这些课程。
using UnityEngine;
public class TrackHelpScript : BaseScript {}
Listing 4-20TrackHelpScript Pertinent Method Outline/Class Headers 2
在下一节,我们将看看类的支持方法。
支持方法详细信息:TrackHelpScript
TrackHelpScript
类的实现类似于其他独立碰撞交互脚本的实现。OnTriggerEnter
事件回调处理一个冲突事件,并调用一个 worker 方法来处理必要的冲突响应。
01 public void OnTriggerEnter(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null) {
07 if (gameState.trackHelpOn == true) {
08 ProcessTrackHelp(otherObj);
09 }
10 }
11 }
01 public void ProcessTrackHelp(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj != null && otherObj.gameObject.CompareTag(Utilities.TAG_PLAYERS)) {
07 Utilities.LoadPlayerInfo(GetType().Name, out PlayerInfo pi, out int playerIndex, out p, otherObj.gameObject, gameState, false);
08 if (p != null) {
09 if (p == gameState.GetCurrentPlayer() && p.aiOn == false) {
10 if (gameObject.CompareTag("TrackHelpSlow")) {
11 if (gameState.hudNewScript != null) {
12 gameState.hudNewScript.HideHelpAccel();
13 gameState.hudNewScript.HideHelpTurn();
14 gameState.hudNewScript.ShowHelpSlow();
15 }
16 gameState.trackHelpSlowOn = true;
17 gameState.trackHelpSlowTime = 0f;
18 } else if (gameObject.CompareTag("TrackHelpTurn")) {
19 if (gameState.hudNewScript != null) {
20 gameState.hudNewScript.HideHelpAccel();
21 gameState.hudNewScript.ShowHelpTurn();
22 gameState.hudNewScript.HideHelpSlow();
23 }
24 gameState.trackHelpTurnOn = true;
25 gameState.trackHelpTurnTime = 0f;
26 gameState.trackHelpOn = false;
27 }
28 }
29 }
30 }
31 }
Listing 4-21TrackHelpScript Support Method Details 1
我们先来看看OnTriggerEnter
方法。在该方法的第 2–4 行中可以找到我们之前见过的相同的故障安全检查。如果碰撞物体被定义,第 6 行,在继续之前,我们在第 7 行检查跟踪帮助标志是否被设置为真。如果启用了跟踪帮助,那么在第 8 行调用ProcessTrackHelp
方法。继续讨论ProcessTrackHelp
方法。第 2-4 行应该看起来很熟悉,所以我们将跳过它。该方法设置为忽略与未标记为“玩家”的对象的碰撞。这确保了事件只有在玩家的车与它相撞时才会触发,第 6 行。
在第 7 行,我们使用了一个非常重要的方法,LoadPlayerInfo
,它将为玩家触发碰撞的悬停赛车找到并加载状态。你会看到这个方法在整个游戏代码中被大量使用,所以确保你理解它是如何被使用的。这个方法调用的关键特性之一是“out p
”参数。这个特殊的方法参数用于更新p
字段,而不使用方法的返回值。如果调用成功并且定义了p
字段,则执行第 9 到 28 行的代码。
在第 9 行,我们检查碰撞的玩家是否是当前玩家,我们确保该玩家不是 AI 玩家。你能想出我们为什么这样做吗?一个原因是,如果碰撞的玩家不是当前活跃的玩家或者 AI 控制为真,我们不需要显示帮助通知。这是因为 HUD 只连接到当前的玩家,没有必要显示其他汽车的帮助通知,或者如果这些汽车是人工智能控制的。TrackHelpScript
能够切换轨道帮助“慢”和“转”HUD 通知。
随后,在第 10 行,我们检查当前的 Unity GameObject
,gameObject
字段是否有标签“TrackHelpSlow”;然后我们准备显示减速帮助通知。我们需要隐藏任何当前的帮助通知,所以第 11–15 行的代码检查我们是否可以通过GameState
类gameState
访问游戏的 HUD。如果是这样,我们就隐藏任何其他的帮助通知,显示有效的通知,即第 14 行。第 16–17 行重置控制帮助通知显示的字段。这些字段负责在游戏的 HUD 上显示通知的时间。
第 18–27 行的类似代码用于控制轨道转向帮助信息。在继续下一步之前,看一下代码,确保你理解了它。这就是支持方法回顾部分的结论。在下一节中,我们将看看这个类的主要方法。
主方法详细信息:TrackHelpScript
TrackHelpScript
类有一个主要的方法供我们回顾。我们以前见过这种方法。它用于初始化类和设置gameState
字段。让我们来看看代码!
1 void Start() {
2 base.Prep(this.GetType().Name);
3 if (BaseScript.IsActive(scriptName) == false) {
4 Utilities.wrForce(scriptName + ": Is Deactivating...");
5 return;
6 }
7 }
Listing 4-22TrackHelpScript Main Method Details 1
正如我们之前讨论的,start 方法负责初始化类。它的主要职责是定位作为 GameState Unity GameObject
组件的GameState
脚本组件。您将在场景的层次中看到该对象。如果一切都成功了,那么这个类就通过名字注册为活动的。换句话说,当类初始化时,对IsActive
方法的检查使用这里创建的结果。这就到了本复习部分的结尾。接下来,我们将看看TrackHelpScript
的运行情况。
演示:TrackHelpScript
组件的演示场景可以在“项目”面板的“场景”文件夹中找到。找到名为“DemoCollideTrackHelp”的场景并打开它。让我花点时间来描述一下演示场景是如何工作的。场景开始几秒钟后,玩家就可以控制汽车了。一旦玩家控制了赛车,场景应该以赛道帮助加速通知标志开启开始。通知只会持续几秒钟。
当 track help acceleration 信息打开时,第一组柱子将显示为绿色。当信息关闭时,柱子会变成红色。如果你缓慢地向前行驶通过柱子,你将触发下一个轨道帮助通知,轨道帮助减速信息。为了表示第二个通知处于活动状态,中间的一组柱子会变绿几秒钟。检查后视镜,看看你后面的柱子变回来了。
将汽车移动到轨道末端会打开最后一个轨道帮助通知,即轨道帮助转向。请注意,因为演示场景没有连接 HUD,所以我们通过改变不同柱子组的颜色来表示何时显示通知。演示场景在游戏面板的左下角显示与赛道帮助消息相关的游戏状态。场景将只在帮助通知中循环一次。
如果你想重置场景,要么点击左下角的“重启”按钮,要么使用 Unity editor“游戏”面板控件启动和停止场景。下面的屏幕截图描述了这个类的演示场景。
图 4-8
跟踪帮助脚本演示场景 2 跟踪帮助脚本场景的俯视图
图 4-7
TrackHelpScript 演示场景 1 描述使用中的跟踪帮助演示场景的图像
这就引出了本章的结论。在进入下一个话题之前,让我们先来看一下本章已经介绍过的信息。
第二章结论
在这一章中,我们已经讨论了很多内容。让我们回顾一下我们在这里讨论过的不同课程。
-
BounceScript:一个独立的碰撞检测器,为与特定游戏对象碰撞的玩家汽车添加反弹修改器。最常见的支持反弹修改器的游戏对象是反弹屏障。该类的演示场景展示了激活和停用反弹碰撞脚本的不同方式。
-
RoadScript:这个类是一个独立的碰撞检测器,它跟踪玩家的汽车是在赛道上还是在赛道外。这个脚本附在游戏中的每一条赛道上。这个类的演示场景展示了一段切换当前玩家赛道标志的赛道。
-
WaypointCheck:这个类用于处理游戏机制的一些不同方面。航路点系统提供了人工智能控制的汽车可以用来导航赛道的脚手架。航路点系统还提供减速队列,可以在弯道和赛道上其他棘手的部分减慢人工智能控制的汽车。航路点系统还用于确定汽车是否行驶在错误的方向上,以及在哪里恢复脱离轨道的汽车。本课程的演示场景展示了一小段赛道,有三个测试点。
-
TrackHelpScript:这个类负责打开和关闭不同的赛道信息消息,以帮助用户学习如何快速控制 hover racer,以便他们能够胜任地玩游戏。这个类的演示场景显示了一系列的帮助信息,用彩色的柱子表示,当玩家在一小段赛道上行驶时,这些信息被打开和关闭。
至此,我们结束了对简单、独立交互的回顾。我们已经解决了游戏规约列表和模型图中的一些问题。在下一章中,我们将看一看集中的、高级的交互处理程序。这将为我们总结所有交互驱动的游戏机制。
五、高级交互类
在这一章中,我们将看看两个重要的集中式交互处理程序:CollideScript
和CarSensorScript
类。这两个脚本完善了游戏的交互处理程序及其相关的游戏机制。CollideScript
处理许多不同的交互,这些交互发生在不同的游戏对象与悬停赛车发生碰撞时。
CarSensorScript
与此类似,但只适用于悬浮赛车。它的主要职责是处理不同类型的车对车交互。我们先来回顾一下CollideScript
。
课堂回顾:碰撞脚本
CollideScript
类负责处理许多不同的碰撞交互和它们相关的游戏机制。我将在此列出支持的交互类型:
-
助推器标记
-
SmallBoostMarker
-
TinyBoostMarker
-
MediumBoostMarker
-
TinyBoostMarker2
-
跳跃标记
-
健康标记
-
枪标记
-
无敌标记
-
装甲标记
-
未加标签的
-
可击中的
-
HittableNoY
-
演员
那是很多的交互。花点时间看一下。有什么让你印象深刻的吗?在大多数情况下,它们看起来很简单。不过,名为“Players”的列表看起来确实有点有趣。当我们复习CollideScript
课程时,我们必须留意这一点。
正如我之前提到的,CollideScript
是一个与玩家的悬浮赛车进行集中交互的游戏。在比赛过程中,当玩家的汽车与赛道上的不同物体发生碰撞时,游戏机制会激活以调整汽车的物理特性。我们将使用以下课堂回顾模板来介绍本课程:
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
关于CollideScript
类没有相关的枚举,所以我们将省略这一部分。除此之外,这是一个比我们之前回顾的更复杂的类。别担心,一点点努力就能走很长的路。让我们开始写代码吧!
静态/常量/只读类成员:碰撞脚本
CollideScript
类有几个纯实数字段供我们查看。我把它们列在这里。
public readonly float BOUNCE_DURATION = 80.0f;
public readonly float BOOST_DURATION = 200.0f;
public readonly float MIN_JUMP_FORCE = 18.0f;
public readonly float MAX_JUMP_FORCE = 22.0f;
Listing 5-1CollideScript Static/Constants/Read-Only Class Members 1
之前列出的只读类字段集用于控制类碰撞交互的某些方面。第一个条目BOUNCE_DURATION
用于控制反弹修改器应用于玩家悬停赛车的时间长度。类似地,BOOST_DURATION
字段用于控制一个增强修改器应用于一辆汽车的时间长度。以下两个条目用于设置与跳跃修改器相关的力的限制。
类字段:碰撞脚本
CollideScript
有许多用来管理它负责处理的不同游戏机制的类字段。其中一些字段是私有的,由某些类方法在内部使用。
private float maxSpeed = 200.0f;
private GameObject player = null;
private CharacterController controller = null;
private CharacterMotor cm = null;
Listing 5-2CollideScript Class Fields 1
列出的第一个类字段maxSpeed
用于跟踪当前玩家的悬停赛车的非加速最大速度。列出的下一个字段是玩家字段。它引用了当前玩家的悬停赛车的GameObject
。该参考用于根据应用的游戏机制调整汽车的运动。接下来的两个字段也用于移动悬停赛车。CharacterController
实例controller
用于沿着CharacterMotor
字段 cm 移动汽车。这给了我们三种不同的方法来控制游戏的悬停赛车模型,以应对不同的碰撞驱动的游戏机制。
我们要查看的下一组类字段与在某些碰撞交互中应用于悬停赛车的力有关。我不严格地使用“力”这个术语。我们采用了一些不同的技术来使汽车弹跳、颠簸、跳跃和加速。在这样做的时候,我们将应用不同的力、速度和位置调整来满足游戏力学的要求,我们将它们统称为“力”或“速度”
public float forceMultiplier = 2.5f;
public float minForce = 20.0f;
public float maxForce = 80.0f;
public bool lockAxisY = false;
public float bounceDampener = 1.0f;
public float minBounceVelocityHor = 25.0f;
public float minBounceVelocityVer = 25.0f;
public float jump = 9.0f;
Listing 5-3CollideScript Class Fields 2
前面列出的第一个字段forceMultiplier
,是一个浮点值,用于增加跳跃的垂直力。接下来的两个类字段,minForce
和maxForce
,用于设置当玩家的赛车与可击中的物体发生碰撞时所施加的力的范围。随后的字段lockAxisY
是一个布尔标志,控制在确定碰撞效果时是否使用 Y 轴(垂直轴)。bounceDampener
场用于减少反弹事件中的力。
以下两个字段minBounceVelocityHor
和minBounceVelocityVer
,用于确保反弹修改器有足够的力来真正反弹汽车。请注意,该修改器作用于水平轴和垂直轴。我应该注意到这个反弹修改器不同于我们之前讨论过的BounceScript
类。该类负责由悬停赛车与物体碰撞激活的反弹修改器。
该反弹修改器由两个相互碰撞的悬停赛车激活。集合中列出的最后一个类字段是jump
字段。该字段用于设置基线垂直力,该力在应用跳跃修改器时使用。下一组类字段是私有的,在一些类的方法中作为局部变量在内部使用。让我们快速地看一下它们,并描述它们是如何使用的。
//***** Internal Variables: Mod Markers *****
private GameObject lastHealthMarker = null;
private GameObject lastGunMarker = null;
private GameObject lastInvcMarker = null;
private GameObject lastArmorMarker = null;
Listing 5-4CollideScript Class Fields 3
下一组职业字段全部用于替换玩家可以在赛道上获得的战斗模式汽车修改器。如果你跑一场战斗模式的比赛,这将打开赛道上的许多赛车修改器。为了在几秒钟后替换修改器,我们保留了最后激活的修改器标记的副本。如你所见,它支持四个战斗模式修改器。我们要查看的下一组类字段由类的 start 方法使用。
//***** Internal Variables: Start *****
private AudioSource audioJump = null;
private AudioSource audioBounce = null;
private AudioSource audioBoost = null;
private AudioSource audioPowerUp = null;
Listing 5-5CollideScript Class Fields 4
前面列出的四个条目是附加到CollideScript
脚本组件的父GameObject
的AudioSource
组件。这些声音效果被加载到类字段中,以便在该类处理碰撞交互时使用。我们要看的下一个字段块是在可碰撞物体碰撞时使用的。
//***** Internal Variables: PerformHit *****
private float collideStrength;
private float jumpHit = 15.0f;
private Rigidbody body = null;
private Vector3 moveDirection = Vector3.zero;
private Vector3 rotateDirection = Vector3.zero;
private AudioSource phAudioS = null;
Listing 5-6CollideScript Class Fields 5
首先,我们有collideStrength
字段,用于计算赛车在赛道上撞上物体时的强度。在游戏中,这种机制体现在经典模式下散落在赛道上的可击中的油桶中。浮动实例jumpHit
是一个类字段,当汽车与可击中的轨迹对象碰撞时,它用于计算 Y 轴、垂直轴的力。body
字段是RigidBody
类的一个实例,用于检测可击对象是否应该被施加力。
接下来的两个条目moveDirection
和rotateDirection
用于确定碰撞后可击中的物体以何种方向和何种旋转飞离悬停赛车。最后,phAudioS
字段用于在碰撞事件中播放声音效果。下一组要查看的类字段是一个大字段。它们由用于为反弹机制提供动力的场组成。现在,我们已经看到了反弹机制应用于反弹障碍。
在这种情况下,汽车在碰撞事件中从护栏上弹开。正如我前面提到的,在这种情况下,我们实现了一个反弹机制,但这次是在两辆车之间,而不是一辆车和一个障碍之间。为了支持这种类型的反弹,我们需要几个字段来保存与我们相撞的汽车的不同信息,并在其上激活一个反弹修改器。
//***** Internal Variables: PerformBounce *****
private PlayerInfo lpi = null;
private int lpIdx = 0;
private PlayerState lp = null;
private CollideScript lc = null;
private Vector3 v3;
private float x;
private float y;
private float z;
private bool isBouncing = false;
private bool useReflect = false;
private bool useInverse = false;
private bool bounceHandOff = false;
private Vector3 bounceV3 = Vector3.zero;
private float bounceTime = 0.0f;
Listing 5-7CollideScript Class Fields 6
前面清单中的前三个字段用于在游戏状态中查找玩家信息,使用的是被撞汽车的脚本组件。lc
类字段用于保存与 hover racer 的CollideScript
冲突的引用。以下四个字段v3
、x
、y
、z
,都是用来计算碰撞结果所涉及的力。计算结束时,矢量分量值x
、y
、z
存储在矢量场v3,
中。isBouncing
字段是一个布尔标志,指示悬停赛车是否被反弹。
随后列出的字段useReflect
和useInverse
是用于改变反弹力计算的布尔标志。下一个字段bounceHandOff
是一个布尔值,用于触发来自外部源的反弹。你能猜到我们什么时候会用这个吗?如果你认为你的车在碰撞事件中触发了另一辆车的反弹,你认为是对的。bounceV3
是一个Vector3
实例,指示最终反弹的方向和力度。bounceTime
向量记录反弹修改器应用于汽车的时间。我们要复习的最后一组职业字段对应于助推机制。
//***** Internal Variables: PerformBoost *****
private float pbAbsX = 0.0f;
private float pbAbsZ = 0.0f;
private bool boostOn = false;
private bool boostHandOff = false;
private Vector3 boostV3 = Vector3.zero;
private float boostTime = 0.0f;
Listing 5-8CollideScript Class Fields 7
前面清单中的一组职业字段用于增强游戏机制。助推机制听起来就像它一样。它为悬停赛车提供了动力,以新的最大速度向前射击。前两个条目用于确定悬停赛车的当前 X 轴速度的绝对值。类似地,pbAbsZ
字段跟踪悬停赛车的当前 Z 速度的绝对值。boostOn
字段是一个布尔标志,表示汽车的助推修改器已激活。boostHandOff
字段用于从另一个源触发一个增强修改器。最后,boostV3
向量保存应用于汽车的计算出的推进,而boostTime
浮动实例跟踪推进的持续时间。接下来,我们将看看控制跳跃修改器的字段。
//***** Internal Variables: PerformJump *****
private bool isJumping = false;
private bool jumpHandOff = false;
private Vector3 jumpV3 = Vector3.zero;
private float jumpStrength;
private float gravity = 10.0f;
Listing 5-9CollideScript Class Fields 8
这个集合中的前两个字段类似于它们的 boost 等价物。isJumping
字段是一个布尔标志,指示当前悬停赛车是否正在跳跃。jumpHandOff
字段是另一个布尔标志,它将触发当前汽车上的跳跃修改器。jumpV3
字段是应用于悬停赛车的计算跳跃向量。浮动实例jumpStrength
看起来是一个跟踪计算的跳转强度的字段。最后,我们有gravity
场,负责估计重力,并慢慢地把车拉回赛道。接下来,我们将看看这个类的相关方法列表。
相关的方法大纲/类头:冲突脚本
CollideScript
的相关方法概述如下。
//Main Methods
void Start();
void Update();
public void OnControllerColliderHit(ControllerColliderHit hit);
//Support Methods
public void RecreateHealthMarker();
public void RecreateGunMarker();
public void RecreateInvcMarker();
public void RecreateArmorMarker();
private void CalcCollideStrength();
private float GetMinForce(float v);
private float GetMaxForce(float v);
private float GetBounceVelHor(float v);
private float GetBoostVelHor(int mode, float movVel);
public void PerformHit(GameObject go, ControllerColliderHit hit);
public void PerformBounce(GameObject go, ControllerColliderHit hit)
public void PerformBoost(GameObject go, ControllerColliderHit hit, int mode);
public void PerformJump(GameObject go, ControllerColliderHit hit);
Listing 5-10CollideScript Pertinent Method Outline/Class Headers 1
接下来,我将列出CollideScript
类的导入语句和类声明。密切注意使用的任何基类。
using UnityEngine;
public class CollideScript : BaseScript {}
Listing 5-11CollideScript Pertinent Method Outline/Class Headers 2
如您所见,CollideScript
类扩展了BaseScript
类,因此是一个MonoBehaviour
实例,换句话说,是一个脚本组件。这意味着您可以将它附加到场景中的不同游戏对象。我们要复习的一些课程不是MonoBehaviour
s。请留意它们。
支持方法详细信息:碰撞脚本
这个课程是我们要复习的较大的课程之一。因为支持方法很少,我们将分组列出并回顾它们。让我们来看看前九种更简单的支持方法。
01 public void RecreateHealthMarker() {
02 lastHealthMarker.SetActive(true);
03 }
01 public void RecreateGunMarker() {
02 lastGunMarker.SetActive(true);
03 }
01 public void RecreateInvcMarker() {
02 lastInvcMarker.SetActive(true);
03 }
01 public void RecreateArmorMarker() {
02 lastArmorMarker.SetActive(true);
03 }
01 private void CalcCollideStrength() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (p == null) {
07 collideStrength = 0;
08 } else {
09 collideStrength = (p.speed * forceMultiplier) / maxSpeed;
10 }
11 }
01 private float GetMinForce(float v) {
02 if (Mathf.Abs(v) < minForce) {
03 if (v < 0) {
04 return -minForce;
05 } else {
06 return minForce;
07 }
08 }
09 return v;
10 }
01 private float GetMaxForce(float v) {
02 if (Mathf.Abs(v) > maxForce) {
03 if (v < 0) {
04 return -maxForce;
05 } else {
06 return maxForce;
07 }
08 }
09 return v;
10 }
01 private float GetBounceVelHor(float v) {
02 if (useReflect == true) {
03 v = v * bounceDampener;
04 } else {
05 if (useInverse == true) {
06 v = v * -1 * bounceDampener;
07 } else {
08 v = v * bounceDampener;
09 }
10 }
11
12 if (v < 0) {
13 if (v > -minBounceVelocityHor) {
14 v = -minBounceVelocityHor;
15 }
16 } else if (v >= 0) {
17 if (v < minBounceVelocityHor) {
18 v = minBounceVelocityHor;
19 }
20 }
21 return v;
22 }
01 private float GetBoostVelHor(int mode, float movVel) {
02 float v3 = 0.0f;
03 if (mode == 0) {
04 v3 = 200;
05 } else if (mode == 1) {
06 v3 = 50;
07 } else if (mode == 2) {
08 v3 = 25;
09 } else if (mode == 3) {
10 v3 = 100;
11 } else if (mode == 4) {
12 v3 = 15;
13 }
14
15 if (movVel < 0) {
16 v3 *= -1;
17 }
18 return v3;
19 }
Listing 5-12CollideScript Support Method Details 1
这组中列出的前四种方法几乎完全相同。这些方法被设计成在玩家与一个战斗模式修改器碰撞几秒钟后触发,导致修改器的游戏对象标记被禁用,变得不可见。这四种方法之间的唯一区别是要重新激活哪个标记。下一个方法是CalcCollideStrength
方法,这个方法依赖于可能没有正确初始化的p
类字段。
因此,第 2–4 行具有预期的逸出检查。该方法简单、直接。如果p
为空,则collideStrength
字段被设置为零;否则,使用公式来确定正确的值,第 9 行。下面列出的两种支持方法GetMinForce
和GetMaxForce
非常相似。这两种方法都限制传入的力,并尊重力的符号。这些方法相当直接。通读它们,确保你理解这些方法是如何工作的。
下一个要回顾的方法是GetBounceVelHor
方法。该方法用于计算水平轴上的反弹速度。第 2-10 行的代码减小了初始力,同时考虑到了反向调整和反射调整的使用。第 12–20 行的代码是为了确保计算的速度有一个标准的最小值,同时考虑它的符号。第 21 行返回最终值。
这组中我们要回顾的最后一个方法是GetBoostVelHor
方法。该方法用于计算增强修改器的水平速度分量。这种方法支持五种不同的升压类型。基于mode
方法参数,确定速度,第 2–13 行。如果速度是负的,新的加速速度被调整并返回,第 5–18 行。
接下来我们要回顾的一组方法是负责实际制定修饰符的。这些方法在长度上有点长,所以我们将一个一个地回顾它们。第一种方法是PerformHit
方法。此方法用作可碰撞对象碰撞处理的一部分。
01 public void PerformHit(GameObject go, ControllerColliderHit hit) {
02 if (BaseScript.IsActive(scriptName) == false || go == null || hit == null) {
03 return;
04 }
05
06 body = hit.collider.attachedRigidbody;
07 if (body == null || body.isKinematic) {
08 return;
09 }
10
11 moveDirection = Vector3.zero;
12 CalcCollideStrength();
13 if (lockAxisY == false) {
14 moveDirection.y = (jumpHit * collideStrength);
15 } else {
16 moveDirection.y = 0;
17 }
18 moveDirection.x = (cm.movement.velocity.x * collideStrength);
19 moveDirection.z = (cm.movement.velocity.z * collideStrength);
20
21 if (minForce > 0) {
22 moveDirection.x = GetMinForce(moveDirection.x);
23 moveDirection.z = GetMinForce(moveDirection.z);
24
25 if (lockAxisY == false) {
26 moveDirection.y = GetMinForce(moveDirection.y);
27 }
28 }
29
30 if (maxForce > 0) {
31 moveDirection.x = GetMaxForce(moveDirection.x);
32 moveDirection.z = GetMaxForce(moveDirection.z);
33
34 if (lockAxisY == false) {
35 moveDirection.y = GetMaxForce(moveDirection.y);
36 }
37 }
38
39 rotateDirection = (moveDirection * 1);
40 body.rotation = Quaternion.Euler(rotateDirection);
41 body.velocity = moveDirection;
42
43 phAudioS = go.GetComponent<AudioSource>();
44 if (phAudioS != null) {
45 if (phAudioS.isPlaying == false) {
46 phAudioS.Play();
47 }
48 }
49 }
Listing 5-13CollideScript Support Method Details 2
如您所料,第 2–4 行检查该类是否已经正确配置。否则,该方法不做任何工作就返回。在第 6–9 行,我们检查了 hit 参数的主体,以查看值是否为空,或者body
的isKinematic
标志是否设置为真。如果是的话,那么力和碰撞将不再影响刚体。我们尊重这一点,并在我们的代码中进行检查。
通过调用CalcCollideStrength
方法在第 12 行设置collideStrength
的值。第 13–17 行的小代码块控制运动向量的 Y 分量是否是碰撞计算的一部分。初始水平力设置在第 18 行和第 19 行。第 21–28 行的代码块过滤分力,以确保它们具有最小值。
做同样的事情来确保水平值不大于最大允许值,第 30–37 行。rotateDirection
向量基于moveDirection
向量。被击中对象的实际旋转设置在第 40 行,而移动速度设置在第 41 行。第 43–48 行的代码用于在碰撞发生时播放声音效果。接下来,我们来看看PerformBounce
方法。
01 public void PerformBounce(GameObject go, ControllerColliderHit hit) {
02 if (BaseScript.IsActive(scriptName) == false || go == null || hit == null) {
03 return;
04 }
05
06 x = GetBounceVelHor(cm.movement.velocity.x);
07 y = cm.movement.velocity.y;
08 z = GetBounceVelHor(cm.movement.velocity.z);
09
10 if (useReflect == true) {
11 v3 = Vector3.Reflect(v3, hit.collider.ClosestPointOnBounds(player.transform.position).normalized);
12 } else {
13 v3 = new Vector3(x, y, z);
14 }
15
16 Utilities.LoadPlayerInfo(GetType().Name, out lpi, out lpIdx, out lp, hit.gameObject, gameState, false);
17 if (lp != null) {
18 lc = lp.player.GetComponent<CollideScript>();
19 lc.bounceHandOff = true;
20 lc.bounceV3 = v3;
21 }
22 }
Listing 5-14CollideScript Support Method Details 3
顾名思义,PerformBounce
方法负责将反弹修改器应用于碰撞的悬停赛车。如果配置步骤失败,方法开头的代码行 2–4 会阻止方法执行任何工作。反弹向量的x
、y
和z
分量在第 6–8 行设置。第 10–14 行使用第 11 和 13 行显示的两种技术中的一种来完成反弹向量。
看看第 16–21 行的代码块。这是一个标准的玩家查找,我们已经见过一次又一次,除了在这种情况下,我们正在查找与我们碰撞的玩家的玩家状态数据。注意第 17–20 行的代码。我们通过设置bounceHandOff
标志和bounceV3
字段值,得到一个与玩家碰撞的CollideScript
的引用,并触发玩家汽车的反弹。我们要看的下一个方法是PerfectBoost
方法。
01 public void PerformBoost(GameObject go, ControllerColliderHit hit, int mode) {
02 if (BaseScript.IsActive(scriptName) == false || go == null || hit == null) {
03 return;
04 }
05
06 pbAbsX = Mathf.Abs(p.cm.movement.velocity.x);
07 pbAbsZ = Mathf.Abs(p.cm.movement.velocity.z);
08 boostV3 = Vector3.zero;
09
10 if (pbAbsX > pbAbsZ) {
11 boostV3.x = GetBoostVelHor(mode, p.cm.movement.velocity.x);
12 } else {
13 boostV3.z = GetBoostVelHor(mode, p.cm.movement.velocity.z);
14 }
15
16 boostHandOff = true;
17 if (audioBoost != null) {
18 if (audioBoost.isPlaying == false) {
19 audioBoost.Play();
20 }
21 }
22
23 if (p != null) {
24 p.flame.SetActive(true);
25 }
26 }
Listing 5-15CollideScript Support Method Details 4
PerformBoost
的方法签名类似于我们之前看到的“执行”方法,但是它需要一个额外的参数,一个模式值。现在,第 2-4 行对您来说应该很熟悉了,所以我们将继续。方法变量在第 6–8 行初始化。在第 10 行,确定哪个水平方向是主导方向。矢量分量的速度在第 11 行和第 13 行使用我们前面提到的GetBoostVelHor
方法设置。
在第 16 行上,boostHandOff
标志被设置为真。这用于打开在类的 update 方法中应用的 boost 修饰符。第 17–21 行播放声音效果。最后,在第 23–25 行,粒子效果被打开,如果可用的话,以指示增强修改器。接下来我们要复习的方法是PerformJump
方法。让我们来看看一些代码。
01 public void PerformJump(GameObject go, ControllerColliderHit hit) {
02 if (BaseScript.IsActive(scriptName) == false || go == null || hit == null) {
03 return;
04 }
05
06 jumpStrength = ((p.speed) * forceMultiplier) / maxSpeed;
07 jumpV3 = Vector3.zero;
08 jumpV3.y = (jump * jumpStrength);
09
10 if (jumpV3.y < MIN_JUMP_FORCE) {
11 jumpV3.y = MIN_JUMP_FORCE;
12 }
13
14 if (jumpV3.y >= MAX_JUMP_FORCE) {
15 jumpV3.y = MAX_JUMP_FORCE;
16 }
17
18 jumpHandOff = true;
19 if (audioJump != null) {
20 if (audioJump.isPlaying == false) {
21 audioJump.Play();
22 }
23 }
24 }
Listing 5-16CollideScript Support Method Details 5
PerformJump
方法遵循的模式与我们为这个类回顾的前面的方法相似。该方法采用我们在其他“执行”方法中见过的相同参数。同样,我们在第 2–4 行有方法保护代码。jumpStrength
字段的值基于悬停赛车的速度,并在第 6 行计算。跳跃速度变量在第 7 行初始化,而垂直力在第 8 行设置。
跳跃力的强度由第 10-16 行的代码调节在最小和最大范围内。第 18 行上的jumpHandOff
标志被设置为真。这将启用该类的 update 方法中的跳转修饰符。跳跃音效在第 19–23 行处理。这就引出了支持方法细节部分的结论。
主要方法细节:碰撞脚本
CollideScript
类有几个主要的方法让我们复习。我们要看的第一个方法是Start
方法。这个方法被 Unity 游戏引擎称为MonoBehaviour
生命周期的一部分。
01 void Start() {
02 base.PrepPlayerInfo(this.GetType().Name);
03 if (BaseScript.IsActive(scriptName) == false) {
04 Utilities.wrForce(scriptName + ": Is Deactivating...");
05 return;
06 } else {
07 player = p.player;
08 maxSpeed = p.maxSpeed;
09 controller = p.controller;
10 cm = p.cm;
11
12 if (controller == null) {
13 Utilities.wrForce("CollideScript: controller is null! Deactivating...");
14 MarkScriptActive(false);
15 return;
16 }
17
18 if (player == null) {
19 Utilities.wrForce("CollideScript: player is null! Deactivating...");
20 MarkScriptActive(false);
21 return;
22 }
23
24 if (cm == null) {
25 Utilities.wrForce("CollideScript: cm is null! Deactivating...");
26 MarkScriptActive(false);
27 return;
28 }
29
30 AudioSource[] audioSetDst = Utilities.LoadAudioResources(GetComponentsInParent<AudioSource>(), new string[] { Utilities.SOUND_FX_JUMP, Utilities.SOUND_FX_BOUNCE, Utilities.SOUND_FX_BOOST, Utilities.SOUND_FX_POWER_UP });
31 if (audioSetDst != null) {
32 audioJump = audioSetDst[0];
33 audioBounce = audioSetDst[1];
34 audioBoost = audioSetDst[2];
35 audioPowerUp = audioSetDst[3];
36 }
37 }
38 }
Listing 5-17CollideScript Main Method Details 1
Start
方法比我们到目前为止讨论过的大多数方法都要长一点。别担心,没有看起来那么复杂。第 2 行的代码用于加载与当前玩家相关的标准类和PlayerState
。如果配置成功,第 3 行的活动标志检查将返回 true,然后我们继续在类字段中存储对玩家的悬停赛车模型、最大速度、控制器和角色运动的引用。第 12–28 行的代码片段用于检查是否定义了该类的必填字段,如果没有,它将该类标记为非活动并返回。
代码的音频资源加载部分从第 30 行到第 36 行。在这种情况下,我们使用实用程序方法LoadAudioResources
,并向它传递一个对连接的AudioSource
数组的引用和一个要搜索的名称数组。该方法搜索音频源并寻找每个搜索目标。结果是一个定制的AudioSource
实例数组。在第 32 到 35 行,从音频资源的结果阵列中设置单独的交互声音效果。我们要看的下一个方法是Update
方法。
01 void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (gameState != null) {
07 if (gameState.gamePaused == true) {
08 return;
09 } else if (gameState.gameRunning == false) {
10 return;
11 }
12 }
13
14 //bounce code
15 if (bounceHandOff == true) {
16 bounceTime = 0f;
17 isBouncing = true;
18 p.isBouncing = true;
19
20 if (audioBounce != null) {
21 if (audioBounce.isPlaying == false) {
22 audioBounce.Play();
23 }
24 }
25 }
26
27 if (isBouncing == true) {
28 bounceTime += (Time.deltaTime * 100);
29 bounceHandOff = false;
30 controller.Move(bounceV3 * Time.deltaTime);
31 }
32
33 if (isBouncing == true && bounceTime >= BOUNCE_DURATION) {
34 isBouncing = false;
35 p.isBouncing = false;
36 }
37
38 //boost code
39 if (boostHandOff == true) {
40 boostTime = 0f;
41 boostOn = true;
42 p.offTrack = false;
43 p.boostOn = true;
44 p.SetBoost();
45 }
46
47 if (boostOn == true) {
48 boostTime += (Time.deltaTime * 100);
49 boostHandOff = false;
50 controller.Move(boostV3 * Time.deltaTime);
51 }
52
53 if (boostOn == true && boostTime >= BOOST_DURATION) {
54 boostOn = false;
55 p.boostOn = false;
56 p.SetNorm();
57 p.flame.SetActive(false);
58 }
59
60 //jump code
61 if (controller.isGrounded == true) {
62 cm.jumping.jumping = false;
63 p.isJumping = false;
64 isJumping = false;
65 }
66
67 if (jumpHandOff == true) {
68 p.offTrack = false;
69 cm.jumping.jumping = true;
70 p.isJumping = true;
71 isJumping = true;
72 }
73
74 if (isJumping == true) {
75 jumpHandOff = false;
76 controller.Move(jumpV3 * Time.deltaTime);
77 }
78
79 //gravity code
80 if ((controller.isGrounded == false || cm.movement.velocity.y > 0) && isJumping == true) {
81 jumpV3.y -= gravity * Time.deltaTime;
82 }
83
84 if (player != null && player.transform.position.y >= Utilities.MAX_XFORM_POS_Y && cm.movement.velocity.y > Utilities.MIN_XFORM_POS_Y) {
85 cm.movement.velocity.y -= gravity * Time.deltaTime;
86 } else if (controller.isGrounded == false || cm.movement.velocity.y > 0 || p.player.transform.position.y > 0) {
87 cm.movement.velocity.y -= gravity * Time.deltaTime;
88 }
89
90 if (controller.isGrounded == false) {
91 cm.movement.velocity.y -= gravity * Time.deltaTime;
92 }
93 }
Listing 5-18CollideScript Main Method Details 2
Update
方法由 Unity 游戏引擎调用游戏的每一帧。关于CollideScript
类,Update
方法负责将主动修改器应用于悬停赛车。该方法具有我们之前见过的相同的安全检查。第 6–12 行的代码块用于检查游戏的状态,如果存在特定的游戏状态,则退出该方法。
我们遇到的第一个修改器是第 14 行的反弹修改器。第 15–25 行的代码用于启动反弹修改器并播放声音效果。我要提一下,这个弹跳音效和用PerformBounce
方法播放的是不一样的。声音效果是从碰撞的汽车上播放的,而不是像这种情况下当前玩家的汽车。注意在第 17-18 行,CollideScript
类的修改器状态与当前玩家的PlayerState
实例保持一致。如果反弹修改器处于活动状态,则执行第 27–31 行的代码。这段代码应用了反弹修改器,第 30 行;阻止它再次启动,第 29 行;并监控它活动的时间,第 28 行。
反弹修改器逻辑的最后一部分是第 33–36 行的一段代码,它在BOUNCE_DURATION
到期后关闭修改器。boost 修饰符代码从第 38 行开始。这部分修改代码与反弹代码非常相似。让我们看一下起始代码,第 39–45 行。如果boostHandOff
标志为真,则boostTime
被重置,第 40 行,并且boostOn
标志都被设置为真,第 41-42 行。在第 43 行,当前玩家的悬浮赛车的加速被打开。
接下来,在第 47 行,处理一个主动增强修改器。增强修改器的持续时间在第 48 行递增,并且通过将boostHandOff
标志设置为假来防止修改器重新打开。改性剂应用于第 50 行。下一个 boost 修饰符代码块在第 53–58 行。如果增强持续时间到期,增强修改器被停用,第 54–56 行。
在第 60 行,跳转修饰符部分开始。第 61–65 行有一段代码。这个代码的目的是当汽车接触地面时关闭跳跃修改器。与该方法中处理的其他修改器不同,当重力将汽车拉回到地面时,“跳跃”修改器关闭。跳跃修改器由jumpHandOff
标志以类似于反弹和加速修改器的方式启动。
在第 74–77 行,应用了跳转修饰符。第 79–92 行的最后一个代码块用于向汽车施加重力,并使其返回地面。跳跃力也会随着时间的推移而减弱,以帮助汽车从跳跃中漂浮回来。这就结束了对该类的 update 方法的回顾。最后要看的主要方法是OnControllerColliderHit
碰撞事件处理程序。让我们来看看。
001 public void OnControllerColliderHit(ControllerColliderHit hit) {
002 if (BaseScript.IsActive(scriptName) == false) {
003 return;
004 }
005
006 if (hit.gameObject.CompareTag(Utilities.TAG_UNTAGGED)) {
007 return;
008 } else if (hit.gameObject.CompareTag(Utilities.TAG_HITTABLE)) {
009 lockAxisY = false;
010 PerformHit(hit.gameObject, hit);
011 } else if (hit.gameObject.CompareTag(Utilities.TAG_HITTABLE_NOY)) {
012 lockAxisY = true;
013 PerformHit(hit.gameObject, hit);
014 lockAxisY = false;
015 } else if (hit.gameObject.CompareTag(Utilities.TAG_PLAYERS)) {
016 if (hit != null && hit.gameObject != null) {
017 PerformBounce(hit.gameObject, hit);
018 }
019 } else if (hit.gameObject.CompareTag(Utilities.TAG_BOOST_MARKER)) {
020 if (p.boostOn == true || p.aiIsPassing == false) {
021 PerformBoost(hit.gameObject, hit, 0);
022 }
023 } else if (hit.gameObject.CompareTag(Utilities.TAG_SMALL_BOOST_MARKER)) {
024 if (p.boostOn == true || p.aiIsPassing == false) {
025 PerformBoost(hit.gameObject, hit, 1);
026 }
027 } else if (hit.gameObject.CompareTag(Utilities.TAG_TINY_BOOST_MARKER)) {
028 if (p.boostOn == true || p.aiIsPassing == false) {
029 PerformBoost(hit.gameObject, hit, 2);
030 }
031 } else if (hit.gameObject.CompareTag(Utilities.TAG_MEDIUM_BOOST_MARKER)) {
032 if (p.boostOn == true || p.aiIsPassing == false) {
033 PerformBoost(hit.gameObject, hit, 3);
034 }
035 } else if (hit.gameObject.CompareTag(Utilities.TAG_TINY_BOOST_2_MARKER)) {
036 if (p.boostOn == true || p.aiIsPassing == false) {
037 PerformBoost(hit.gameObject, hit, 4);
038 }
039 } else if (hit.gameObject.CompareTag(Utilities.TAG_JUMP_MARKER)) {
040 if (p.isJumping == false) {
041 PerformJump(hit.gameObject, hit);
042 }
043 } else if (hit.gameObject.CompareTag(Utilities.TAG_HEALTH_MARKER)) {
044 if (audioPowerUp != null) {
045 if (audioPowerUp.isPlaying == false) {
046 audioPowerUp.Play();
047 }
048 }
049
050 if (p.damage - 1 >= 0) {
051 p.damage -= 1;
052 }
053
054 p.aiHasGainedLife = true;
055 p.aiHasGainedLifeTime = 0;
056 hit.gameObject.SetActive(false);
057 lastHealthMarker = hit.gameObject;
058 Invoke(nameof(RecreateHealthMarker), Random.Range(Utilities.MARKER_REFRESH_MIN, Utilities.MARKER_REFRESH_MAX));
059 } else if (hit.gameObject.CompareTag(Utilities.TAG_GUN_MARKER)) {
060 if (audioPowerUp != null) {
061 if (audioPowerUp.isPlaying == false) {
062 audioPowerUp.Play();
063 }
064 }
065
066 if (p.ammo <= Utilities.MAX_AMMO) {
067 p.ammo += Utilities.AMMO_INC;
068 }
069
070 p.gunOn = true;
071 p.ShowGun();
072 hit.gameObject.SetActive(false);
073 lastGunMarker = hit.gameObject;
074 Invoke(nameof(RecreateGunMarker), Random.Range(Utilities.MARKER_REFRESH_MIN, Utilities.MARKER_REFRESH_MAX));
075 } else if (hit.gameObject.CompareTag(Utilities.TAG_INVINC_MARKER)) {
076 if (audioPowerUp != null) {
077 if (audioPowerUp.isPlaying == false) {
078 audioPowerUp.Play();
079 }
080 }
081
082 p.invincOn = true;
083 p.invincTime = 0;
084 p.ShowInvinc();
085 hit.gameObject.SetActive(false);
086 lastInvcMarker = hit.gameObject;
087 Invoke(nameof(RecreateInvcMarker), Random.Range(Utilities.MARKER_REFRESH_MIN, Utilities.MARKER_REFRESH_MAX));
088 } else if (hit.gameObject.CompareTag(Utilities.TAG_ARMOR_MARKER)) {
089 if (audioPowerUp != null) {
090 if (audioPowerUp.isPlaying == false) {
091 audioPowerUp.Play();
092 }
093 }
094
095 p.armorOn = true;
096 hit.gameObject.SetActive(false);
097 lastArmorMarker = hit.gameObject;
098 Invoke(nameof(RecreateArmorMarker), Random.Range(Utilities.MARKER_REFRESH_MIN, Utilities.MARKER_REFRESH_MAX));
099 }
100 }
Listing 5-19CollideScript Main Method Details 3
方法是处理冲突和决定采取什么行动的中心点。处理的第一种碰撞是与未标记对象的碰撞。在这段代码中,什么都不做,方法返回。接下来是可打碰撞类型。在lockAxisY
字段被设置为真之后,这个修饰符被应用于第 10 行。
可碰撞碰撞类型之后是一个类似的碰撞类型,HittableNoY,除了它不在应用的修改器向量中使用 Y 轴力。第 12 行在调用PerformHit
方法之前将lockAxisY
字段设置为真。随后,第 14 行上的lockAxisY
被设置回假。在第 15–18 行,处理反弹修改器。这个修改器与其他修改器略有不同,它适用于被碰撞的玩家的车,而不是当前玩家的车。
从第 19 行到第 38 行的代码块用于处理增强标记冲突。可以处理五种不同类型的增强标记。我将在这里列出它们以及它们相关的速度。除了传递给PerformBoost
方法的模式值之外,所有的条目都是相同的。请注意,如果修改器已经打开,或者汽车处于超车模式,则助推修改器不会触发。
-
助推器马克:200
-
SmallBoostMarker: 50
-
TinyBoostMarker: 25
-
MediumBoostMarker: 100
-
TinyBoostMarker2: 15
最后一个物理修饰符从第 39 行开始。如果当前玩家的悬停赛车还没有跳跃,那么在第 41 行执行跳跃修改器。剩下的碰撞类型都是游戏战斗模式特有的。在战斗模式中,悬浮赛车可以使用自动射击系统互相射击。在战斗模式比赛中,赛道上有标记可以激活生命值、枪械、护甲和无敌属性。
健康标记是第 43 行的OnControllerColliderHit
方法处理的第一个战斗模式标记。每个战斗模式标记被触发时都会发出声音。这可以在第 44–48 行的健康标记中看到。如果玩家有伤害需要治疗,这在 50-52 行处理。在行 54 和 55 上进行更多的玩家状态调整。接下来的三行代码很重要,因为它们会出现在每一个战斗模式标记中。
这段代码的作用是停用标记,存储对标记的引用,然后安排一个方法调用来重新激活标记。剩下的战斗模式标记(枪、无敌和盔甲)有相似的结构代码。通读剩余的方法代码,并确保在继续之前理解它。这就结束了对OnControllerColliderHit
方法和CollideScript
主方法评审部分的评审。在下一节中,我们将看一看本课程的演示场景。
演示:碰撞脚本
CollideScript
类有一个有趣的演示场景,名为“DemoCollideScript”。您可以在“项目”面板的“场景”文件夹中找到该场景。在你打开它之前,让我稍微讲一下这个场景是如何工作的。几秒钟后,你就可以控制悬浮赛车了。演示场景中有许多不同的对象可以与之交互。从赛车的起始位置开始,在左侧,有许多可以尝试的加速标记。它们被一组红色柱子包围着。
紧挨着它,在右边,在两个紫色柱子之间有一组跳跃坡道。这些斜坡会触发跳跃修改器,让你检查它是如何工作的。在这些特征之后是一组绿色柱子。这些柱子实际上标志着一系列的路点和一个人工智能控制的对手,悬停赛车。这需要一点点努力,但是你可以把车排成一排,然后把它们撞在一起。这是汽车反弹修改器工作的一个例子。
在交互特征的中心线之外,有两叠油桶。你可以撞上它们,让它们飞起来。这是一个可点击修饰符的例子。最后但同样重要的是,有一系列的战斗模式标记排列在远处的墙上。如果你撞到这些,它们会消失一段时间。其中一个甚至可以启动汽车的枪。尝试一下。它们构成了我们在本课中讨论过的战斗模式标记修改器。
这节课的复习到此结束。我们要看的下一个类是第二个集中式交互类;我们将回顾并总结游戏中所有碰撞驱动的交互和游戏机制。随着我们完成越来越多的游戏功能,请花点时间回头看看游戏规约列表。
课堂回顾:CarSensorScript
我们要看的最后一个交互类是CarSensorScript
。该脚本为汽车传感器供电,该传感器用于跟踪当前玩家的悬停赛车前方和附近的对手汽车。使用这种传感器设置,如果后面的汽车在足够长的时间内足够近地跟踪一辆汽车,它可以触发“超车”修改器。
CarSensorScript
的另一个职责是运行模拟射击另一辆悬停赛车的自动枪。如果后面的车有弹药,并能让它前面的车保持在传感器内,直到目标跟踪完成,枪走火,就会发生这种情况。随机掷骰子决定命中,如果命中的车没有更多生命值,作为惩罚,它会返回几个点。我们将使用以下课堂回顾模板来介绍本课程:
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
让我们来看看一些代码!
静态/常量/只读类成员:CarSensorScript
CarSensorScript
类有许多我们需要查看的静态和只读类字段。
public static float BASE_BOOST = 200.0f;
public static float BASE_NON_BOOST = 25.0f;
public static string AUDIO_SOURCE_NAME_GUN_SHOT = "explosion_dirty_rnd_01";
public static string AUDIO_SOURCE_NAME_TARGETTING = "alien_crickets_lp_01";
public static readonly float TRIGGER_TIME_DRAFTING = 2.5f;
public static readonly float TRIGGER_TIME_PASSING = 2.5f;
Listing 5-20CarSensorScript Static/Constants/Read-Only Class Members 1
列出的前两个字段BASE_BOOST
和BASE_NON_BOOST
用于跟踪增强和非增强默认力。后续字段AUDIO_SOURCE_NAME_GUN_SHOT
和AUDIO_SOURCE_NAME_TARGETTING
类字段用于加载AudioSource
组件,并应反映所用音频资源的名称。集合中的最后两个字段TRIGGER_TIME_DRAFTING
和TRIGGER_TIME_PASSING
,用于控制通过游戏机制的时间和持续时间。
public static float TRIGGER_SPEED_PASSING = 0.90f;
public static readonly float SAFE_FOLLOW_DIST = 80.0f;
public static readonly float GUN_SHOT_DIST = 160.0f;
public static readonly float GUN_RELOAD_TIME = 500.0f;
public static readonly float MIN_TARGET_TO_FIRE_TIME = 100.0f;
public static readonly float MAX_EXPLOSION_TIME = 120.0f;
Listing 5-21CarSensorScript Static/Constants/Read-Only Class Members 2
列出的第一个字段TRIGGER_SPEED_PASSING
,用于触发汽车超车游戏机械师。作为触发要求的一部分,你需要有至少 90%的最大速度。SAFE_FOLLOW_DIST
字段是你可以跟随对手的车触发路过的机械师的最大距离。GUN_SHOT_DIST
是仍然能够向你前面的汽车开枪的最大距离。
GUN_RELOAD_TIME
字段是为下一次射击重新装弹所需的时间,以毫秒为单位。下一个字段表示将汽车锁定在传感器上并向其开火所需的最短时间。MAX_EXPLOSION_TIME
代表运行枪击爆炸效果的最长时间。这就是本复习部分的结论。接下来,我们将看看该类的其余字段。
类字段:CarSensorScript
CarSensorScript
类有几个类字段供我们查看。
//***** Class Fields *****
private AudioSource audioGunShot = null;
private AudioSource audioTargetting = null;
private ArrayList cars = null;
private GameObject player = null;
Listing 5-22CarSensorScript Class Fields 1
前两个字段是对连接到玩家汽车上用作音效的音频组件的引用,audioGunShot
和audioTargetting
。这个集合中列出的下一个字段是cars ArrayList
实例。该数据结构用于跟踪当前在汽车传感器中的对手。player
类字段用于保存对玩家的GameObject
实例的引用。我们将回顾的下一组类字段是那些由SetBoostVectors
方法使用的字段。让我们看看。
//***** Internal Variables: SetBoostVectors *****
private float absX = 0;
private float absZ = 0;
private Vector3 passLeftV3 = Vector3.zero;
private Vector3 passGoV3 = Vector3.zero;
private Vector3 passV3 = Vector3.zero;
Listing 5-23CarSensorScript Class Fields 2
集合中列出的前两个字段是通过SetBoostVectors
方法在力计算中使用的absX
和absZ
字段。接下来的三个字段都是Vector3
实例:passLeftV3
、passGoV3
和passV3
。这些字段用于设置向量的力,这些向量用于在超车时使汽车绕过另一辆汽车。接下来我们将看看PerformShot
方法使用的类字段。
//***** Internal Variables: PerformShot *****
private PlayerState p2 = null;
private int r2 = 0;
Listing 5-24CarSensorScript Class Fields 3
前面列出的字段用于查找“射击”玩家的状态信息,以潜在地应用射击命中修改器。p2
字段存储对相关玩家状态对象的引用,而整数r2
用于支持在确定命中时使用的随机骰子滚动。下一组,也是最后一组要检查的字段由 class’ Update
方法使用。
//***** Internal Variables: Update *****
private Collider obj = null;
private int i2 = 0;
private int l2 = 0;
private bool tb = false;
private Vector3 t1 = Vector3.zero;
private Vector3 t2 = Vector3.zero;
private float dist = 0.0f;
private float moveTime = 0.0f;
private bool explosionOn = false;
private float explosionTime = 0.0f;
private Collider target = null;
Listing 5-25CarSensorScript Class Fields 4
Collider
字段obj
用于引用Collider
对象,当汽车传感器与另一个玩家的悬停赛车相撞时,该对象被记录下来。i2
和l2
字段用于控制通过传感器采集的汽车列表的循环。tb
字段是一个布尔标志,用于指示当前玩家的汽车应该开启自动超车技工。t1
和t2
字段用于确定被跟踪车辆的距离。dist
字段用于保存当前玩家的悬停赛车和被跟踪的汽车之间的计算距离。
接下来,moveTime
字段用于控制如何随时间应用传递机制。接下来的三个字段与枪击机械师有关。这些字段跟踪在拍摄过程中是否需要运行任何效果。explosionOn
字段表示应该显示枪击爆炸效果。explosionTime
字段跟踪爆炸效果已经运行了多长时间。最后,target
区域代表被射击的汽车。在下一个复习部分,我们将看一下课程的相关方法大纲。
相关的方法大纲/类头:CarSensorScript
通过BaseScript
类的扩展,CarSensorScript
是一个MonoBehaviour
,它有许多主方法和支持方法供我们回顾。方法概述如下。
//Main Methods
void Start();
void OnTriggerEnter(Collider otherObj);
void OnTriggerExit(Collider otherObj);
void Update();
//Support Methods
public void SetBoostVectors();
public void PerformGunShotAttempt(Collider otherObj);
public void CancelTarget();
Listing 5-26CarSensorScript Pertinent Method Outline/Class Headers 1
下面列出了CarSensorScript
类的导入语句和头文件。
using System.Collections;
using UnityEngine;
public class CarSensorScript : BaseScript {}
Listing 5-27CarSensorScript Pertinent Method Outline/Class Headers 2
接下来,我们将看看类的支持方法。
支持方法详细信息:CarSensorScript
CarSensorScript
类有一些支持方法,用于支持汽车通过和射击游戏机制。我们先来看看SetBoostVectors
方法。
01 public void SetBoostVectors() {
02 if (p == null) {
03 return;
04 } else if (BaseScript.IsActive(scriptName) == false) {
05 return;
06 }
07
08 absX = Mathf.Abs(p.cm.movement.velocity.x);
09 absZ = Mathf.Abs(p.cm.movement.velocity.z);
10 passLeftV3 = Vector3.zero;
11 passGoV3 = Vector3.zero;
12 passV3 = Vector3.zero;
13
14 if (absX > absZ) {
15 passGoV3.x = BASE_BOOST;
16 if (p.cm.movement.velocity.x < 0) {
17 passGoV3.x *= -1;
18 }
19
20 passLeftV3.z = BASE_NON_BOOST;
21 if (p.cm.movement.velocity.z < 0) {
22 passLeftV3.z *= -1;
23 }
24
25 passV3.z = passLeftV3.z;
26 passV3.x = passGoV3.x;
27 } else {
28 passGoV3.z = BASE_BOOST;
29 if (p.cm.movement.velocity.z < 0) {
30 passGoV3.z *= -1;
31 }
32
33 passLeftV3.x = BASE_NON_BOOST;
34 if (p.cm.movement.velocity.x < 0) {
35 passLeftV3.x *= -1;
36 }
37
38 passV3.x = passLeftV3.x;
39 passV3.z = passGoV3.z;
40 }
41 }
Listing 5-28CarSensorScript Support Method Details 1
SetBoostVectors
方法用于设置一个Vector3
实例的某些力分量,该实例用于使当前玩家的悬停赛车超过其前面的目标汽车。如果不满足某些先决条件,第 2–6 行的代码会阻止该方法执行任何工作。我们要看的下一小段代码准备了方法的局部变量。absX
和absZ
类字段被设置为当前玩家的速度。我们只关心 X 和 z 的水平轴。
随后,在传递游戏机制中使用的三个Vector3
对象在第 10-12 行被重置,为当前计算的结果做准备。所涉及的速度是绝对值。我们这样做是为了简化汽车行驶方向的检测。如果 X 轴是第 14 行的主要部分,我们执行第 15–26 行的代码。因为 X 轴是主导轴,我们将推断它是悬停赛车移动的主要方向。
因此,我们将passGoV3
场的 X 分量的速度设置为BASE_BOOST
速度。在第 16–18 行,我们考虑给定速度的原始符号。向前的速度向量已经设定好了,但是我们需要一点侧向运动来帮助经过的车绕过它经过的车。在第 20 行,passLeftV3
字段将其 Z 轴速度设置为BASE_NON_BOOST
的值。同样,第 21–23 行考虑了原始速度的符号。
在第 25–26 行,我们计算出 X 和 Z 轴的速度,并将它们存储在passV3
字段中。第 28–39 行的代码遵循与我们刚刚检查的代码相同的模式,除了我们在这里使用 Z 轴。我们将在Update
方法回顾中看到这些向量的使用。下一个要审查的方法是PerformGunShotAttempt
方法。
01 public void PerformGunShotAttempt(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (otherObj.gameObject.CompareTag(Utilities.TAG_PLAYERS)) {
07 Utilities.LoadPlayerInfo(GetType().Name, out PlayerInfo pi2, out int playerIndex2, out p2, otherObj.gameObject, gameState, false);
08 if (p2 != null) {
09 r2 = Random.Range(1, 6);
10
11 explosionOn = true;
12 explosionTime = 0f;
13 if (p2 != null && p2.gunExplosion != null) {
14 p2.gunExplosion.SetActive(true);
15 }
16
17 if (audioGunShot != null) {
18 if (audioGunShot.isPlaying == false) {
19 audioGunShot.Play();
20 }
21 }
22
23 if (r2 == 1 || r2 == 2 || r2 == 4 || r2 == 5) {
24 p2.isHit = true;
25 p2.isMiss = false;
26 p2.isMissTime = 0f;
27 p2.PerformGunShotHit();
28 } else {
29 p2.isHit = false;
30 p2.isMiss = true;
31 p2.isHitTime = 0f;
32 }
33
34 CancelTarget();
35 }
36 }
37 }
Listing 5-29CarSensorScript Support Method Details 2
PerformGunShotAttempt
方法开始的方式与您预期的差不多,第 2–4 行的安全检查防止方法在类没有正确配置的情况下做任何工作。我们检查第 6 行的参数otherObj
是否是一个玩家对象。在第 7 行,标准的实用方法调用加载了GameState
和PlayerState
引用,除了在这种情况下,我们将它应用于otherObj
方法参数。在第 9 行,随机数被生成并存储在r2
字段中。
因为开枪是为了执行射击,所以我们将explosionOn
字段设置为真,将explosionTime
字段设置为零。如果玩家和爆炸效果被定义,爆炸效果在第 14 行被激活。接下来,在第 17-21 行,播放一个声音效果来表示发生了枪击。随后,我们必须应用射击的结果,第 23–32 行。如果命中掷骰的结果是 1、2、4 或 5,则该击球是命中。很有可能。如果没有,射击是未命中的,并且对目标汽车没有任何影响。
在第 24–27 行,点击被记录在目标汽车上,相关玩家的PlayerState
对象的PerformGunShotHit
方法被调用。在第 29–31 行,处理未命中。最后但同样重要的是,调用CancelTarget
方法来清除目标系统。
01 public void CancelTarget() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (audioTargetting != null) {
07 audioTargetting.Stop();
08 }
09
10 target = null;
11 if (p != null) {
12 p.aiHasTarget = false;
13 p.aiHasTargetTime = 0f;
14 p.aiCanFire = false;
15 }
16 }
Listing 5-30CarSensorScript Support Method Details 3
CancelTarget
方法开始时很像我们期望的快速安全检查。任何音频都在第 7 行停止。target
类字段被设置为空,当前玩家的目标字段在第 11–15 行被重置。这就结束了对类的支持方法的回顾。
主要方法细节:CarSensorScript
CarSensorScript
的主要方法通常更适合 Unity 游戏引擎或碰撞检测事件使用的回调方法。让我们看看这个类的主要方法。
01 void Start() {
02 cars = new ArrayList();
03 base.PrepPlayerInfo(this.GetType().Name);
04 if (BaseScript.IsActive(scriptName) == false) {
05 Utilities.wrForce(scriptName + ": Is Deactivating...");
06 return;
07 } else {
08 player = p.player;
09 AudioSource[] audioSetDst = Utilities.LoadAudioResources(GetComponentsInParent<AudioSource>(), new string[] { AUDIO_SOURCE_NAME_GUN_SHOT, AUDIO_SOURCE_NAME_TARGETTING });
10 if (audioSetDst != null) {
11 audioGunShot = audioSetDst[0];
12 audioTargetting = audioSetDst[1];
13 }
14 }
15 }
Listing 5-31CarSensorScript Main Method Details 1
Start
方法从初始化汽车ArrayList
开始,汽车ArrayList
用于保存由汽车传感器跟踪的悬停赛车的参考,第 2 行。在第 3 行,基类的更复杂的配置方法PrepPlayerInfo
被调用来初始化GameState
和PlayerState
引用。如果类配置以某种方式失败,该方法在第 4–7 行打印一些调试文本后返回。如果类别配置成功,玩家类别字段被设置为当前玩家的游戏对象,第 8 行。
通过调用实用方法LoadAudioResources
,在第 9 行设置找到的音频资源数组。该方法将一组AudioSource
组件和一组目标字符串作为参数。如果有结果要处理,我们提取射击和瞄准游戏机制的音效,第 11-12 行。我们要看的下一个方法处理类的冲突事件。
01 void OnTriggerEnter(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (p != null && otherObj.CompareTag(Utilities.TAG_PLAYERS)) {
07 if (cars.Contains(otherObj) == false) {
08 cars.Add(otherObj);
09 }
10
11 if (cars.Count > 0) {
12 p.SetDraftingBonusOn();
13 }
14 }
15 }
Listing 5-32CarSensorScript Main Method Details 2
当玩家的汽车在赛道上比赛时,CarSensorScript
接收来自不同物体的碰撞事件。如果类配置有错误,该方法会被转义而不做任何工作。如果被碰撞的游戏对象有一个设置为“玩家”的标签,那么执行第 7-13 行的代码。如果遇到的玩家还没有被目标系统注册,它将被添加到第 8 行的cars ArrayList
中。第 11-13 行的最后一小段代码激活了一个小的制图奖励,如果当前玩家的追踪系统中有任何汽车的话。接下来,我们来看看OnTriggerExit
法。
01 void OnTriggerExit(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (p != null && otherObj.CompareTag(Utilities.TAG_PLAYERS)) {
07 if (cars.Contains(otherObj) == true) {
08 cars.Remove(otherObj);
09 }
10
11 if (cars.Count == 0) {
12 p.SetDraftingBonusOff();
13 }
14
15 if (target == otherObj) {
16 CancelTarget();
17 }
18 }
19 }
Listing 5-33CarSensorScript Main Method Details 3
当对手的汽车退出当前玩家的汽车传感器时,OnTriggerExit
方法触发。标准安全检查代码可在第 2–4 行找到。如果离开悬停赛车传感器的物体被标记为玩家的游戏物体,将执行第 7–17 行的代码。如果cars
列表包含给定的游戏对象,第 7 行,它将从玩家的跟踪系统中删除。在第 11-13 行,如果没有被跟踪的汽车,绘图奖励被关闭。
看看第 15–17 行的代码。如果退出跟踪传感器的玩家的汽车是目标,则在第 16 行调用CancelTarget
方法。我们最后要复习的主要方法是CarSensorScript
类Update
法。该方法由 Unity 游戏引擎在每个游戏帧调用一次。这个方法有点长,所以我将把它分成一些代码片段让我们看看。
01 void Update() {
02 if (p == null) {
03 return;
04 } else if (BaseScript.IsActive(scriptName) == false) {
05 return;
06 } else {
07 if (gameState != null) {
08 if (gameState.gamePaused == true) {
09 return;
10 } else if (gameState.gameRunning == false) {
11 return;
12 }
13 }
14 }
15
Listing 5-34CarSensorScript Main Method Details 4
第一段代码来自前面列出的Update
方法的开头,它处理在不满足某些先决条件的情况下对方法调用的转义。这段代码与我们之前看到的代码块略有不同。在这种情况下,如果游戏暂停或没有运行,要小心防止方法做任何工作。
16 //Process Car Sensor Targets
17 if (p.aiPassingMode == 0 && p.aiIsPassing == false) {
18 l2 = cars.Count;
19 tb = false;
20 for (i2 = 0; i2 < l2; i2++) {
21 obj = (Collider)cars[i2];
22 t1 = obj.gameObject.transform.position;
23 t2 = player.transform.position;
24 dist = Vector3.Distance(t1, t2);
25
26 //Auto Passing Check
27 if (dist <= SAFE_FOLLOW_DIST && p.speed >= (TRIGGER_SPEED_PASSING * p.maxSpeed)) {
28 tb = true;
29 break;
30 }
31
32 if (gameState.gameSettingsSet == 2) {
33 //No Gun Play
34 continue;
35 }
36
37 //Targeting Check
38 if (dist <= GUN_SHOT_DIST && p.gunOn == true && p.ammo > 0 && p.aiHasTarget == false && p.aiIsReloading == false) {
39 target = obj;
40 p.aiHasTarget = true;
41 p.aiHasTargetTime = 0f;
42
43 if (audioTargetting != null) {
44 if (audioTargetting.isPlaying == false) {
45 audioTargetting.Play();
46 }
47 }
48 } else if (dist <= GUN_SHOT_DIST && p.gunOn == true && p.ammo > 0 && p.aiHasTarget == true && p.aiCanFire == true && p.aiIsReloading == false) {
49 p.aiHasTarget = false;
50 p.aiHasTargetTime = 0f;
51 p.aiCanFire = false;
52 p.ammo--;
53
54 if (p.ammo <= 0) {
55 p.ammo = 0;
56 p.HideGun();
57 }
58
59 if (audioTargetting != null) {
60 audioTargetting.Stop();
61 }
62
63 PerformGunShotAttempt(obj);
64 p.aiIsReloading = true;
65 p.aiIsReloadingTime = 0f;
66 } else if (dist > GUN_SHOT_DIST) {
67 if (audioTargetting != null) {
68 audioTargetting.Stop();
69 }
70 target = null;
71 p.aiHasTarget = false;
72 p.aiHasTargetTime = 0f;
73 p.aiCanFire = false;
74 }
75 } //end for loop
76
Listing 5-35CarSensorScript Main Method Details 5
在前面列出的下一段代码中,Update
方法处理当前被汽车定位系统跟踪的悬停赛车。如果当前玩家的汽车不在超车模式,第 17 行,局部变量l2
被目标系统当前跟踪的汽车数量更新,同时布尔标志tb
被设置为假,第 18–19 行。第 20–21 行用于在被跟踪的车辆上循环。接下来,第 21–24 行设置目标对象,目标的位置,当前玩家的位置,最后在第 24 行设置目标和玩家之间的距离。在第 27 行检查自动通过游戏机制。
如果目标在安全跟随距离内,并且当前游戏者移动得足够快,则自动通过标志被触发,并且 for 循环从第 28 和 29 行中断。在第 32-35 行,有一个检查来确保当前的比赛支持战斗模式的游戏机制;如果没有,我们跳过目标玩家的条目,第 34 行。检查瞄准系统以查看当前是否有目标,行 38。目标和玩家状态信息在第 39–41 行更新。这个代码块通过设置目标来启动定位过程。
接下来,在第 48 行,如果目标悬停赛车仍然在射程内,并且当前玩家的车可以开火,那么就进行射击。第 49–52 行的一小块代码重置了玩家的汽车传感器脚本的瞄准和发射区域。玩家的弹药在 52 线减少。在第 54-57 行,我们检查当前玩家是否没有弹药了。如果是这样,我们要确保弹药设置为零,然后藏起枪。目标声音效果在第 59–61 行停止。在第 63 行执行注射,在第 64–65 行设置重新加载状态。
这段代码中的最后一段代码从第 66 行到第 74 行。这段代码用于关闭目标定位。目标声音效果被停止,第 68 行,并且第 70 行的target
字段被设置为空。玩家状态目标字段在第 71–73 行被重置。我们要看的下一段代码处理 hover racer 的自动通过代码的开始。
77 //Auto Passing Start
78 if (tb == true) {
79 p.aiPassingTime += Time.deltaTime;
80 if (p.aiPassingTime > TRIGGER_TIME_PASSING && p.aiPassingMode == 0) {
81 p.aiPassingMode = 1;
82 p.aiIsPassing = true;
83 p.aiPassingTime = 0f;
84 moveTime = 0f;
85 p.SetBoost();
86 p.SetCurrentSpeed();
87 SetBoostVectors();
88 }
89 } else {
90 p.aiPassingMode = 0;
91 p.aiIsPassing = false;
92 p.aiPassingTime = 0f;
93 moveTime = 0f;
94 p.SetNorm();
95 p.SetCurrentSpeed();
96 }
97 } //main if statement
98
Listing 5-36CarSensorScript Main Method Details 6
如果先前在Update
方法中设置的tb
字段在第 78 行被设置为真,则启动自动传递,并且第 79 行的aiPassingTime
字段递增。经过所需的时间后,传递模式从 0 变为 1,并执行第 81–87 行的代码。看一下代码,确保它对你有意义。如果tb
标志为假,则执行第 90–95 行的代码,关闭任何通过模式标志并重置任何计时器。接下来,我们将看看自动传球游戏机制的应用。
099 //Auto Passing Applied
100 if (p.aiIsPassing == true) {
101 moveTime += Time.deltaTime * 100;
102 if (p.aiPassingMode == 1) {
103 p.controller.Move(passLeftV3 * Time.deltaTime);
104 if (moveTime >= 50) {
105 p.aiPassingMode = 2;
106 moveTime = 0;
107 }
108 } else if (p.aiPassingMode == 2) {
109 p.controller.Move(passGoV3 * Time.deltaTime);
110 if (moveTime >= 100) {
111 p.aiPassingMode = 0;
112 p.aiIsPassing = false;
113 p.aiPassingTime = 0f;
114 p.SetNorm();
115 moveTime = 0f;
116 }
117 }
118 }
119
120 //Auto Passing End
121 if (p.isJumping == true) {
122 p.aiPassingMode = 0;
123 p.aiIsPassing = false;
124 p.aiPassingTime = 0f;
125 p.SetNorm();
126 moveTime = 0f;
127 }
128
Listing 5-37CarSensorScript Main Method Details 7
CarSensorScript
的Update
方法中的下一段代码负责应用自动传球游戏机制。在第 100 行,如果aiIsPassing
布尔标志被设置为真,我们开始更新第 101 行的moveTime
字段。如果aiPassingMode
字段等于 1,第 102 行,当前玩家的汽车向左移动 50 毫秒,然后aiPassingMode
被设置为 2 并且moveTime
字段被重置以跟踪自动超车游戏机制中的下一次移动的持续时间。
如果超车模式的值为 2,行 108,那么当前玩家的汽车在接下来的 100 毫秒内向前推进,行 109 和 110。在 100 毫秒的时间间隔到期后,通过模式、速度和移动时间都被重置,第 111-115 行。第 121–127 行的最后一位自动通过代码负责在当前玩家的悬停赛车跳跃时关闭自动通过模式。这就把我们带到了自动传递代码的末尾。接下来,我们将用一些与目标相关的代码来结束方法回顾。
129 //Targetting to Fire
130 if (p.aiHasTarget == true && p.aiIsReloading == false) {
131 p.aiHasTargetTime += Time.deltaTime * 100;
132 if (p.aiHasTargetTime >= MIN_TARGET_TO_FIRE_TIME) {
133 p.aiHasTargetTime = 0f;
134 p.aiCanFire = true;
135 }
136 } else if (p.aiIsReloading == true) {
137 p.aiIsReloadingTime += Time.deltaTime * 100;
138 if (p.aiIsReloadingTime >= GUN_RELOAD_TIME) {
139 p.aiIsReloading = false;
140 p.aiIsReloadingTime = 0f;
141 }
142 }
143
144 //Targetting Gun Explosion Effect
145 if (explosionOn == true) {
146 explosionTime += Time.deltaTime * 100;
147 }
148
149 if (explosionOn == true && explosionTime >= MAX_EXPLOSION_TIME) {
150 explosionOn = false;
151 explosionTime = 0f;
152 p.isHit = false;
153 p.isMiss = false;
154 if (p != null && p.gunExplosion != null) { // && p.gunExplosionParticleSystem != null)
155 p.gunExplosion.SetActive(false);
156 //p.gunExplosionParticleSystem.emit = false;
157 }
158 }
159 } //method end
Listing 5-38CarSensorScript Main Method Details 8
我们要查看的最后一段代码处理目标责任,如重新加载,检查当前汽车是否可以开火,以及显示枪击爆炸效果(如果粒子效果已经实现)。我把一些粒子效果的定制留给了你。如果跟踪系统有一个目标并且枪没有重新装弹,则执行第 130–136 行的代码。这段代码在第 134 行将玩家的aiCanFire
字段设置为 true。第 136 到 142 行的代码处理枪的重新加载机制的时间。第 145–147 行的代码处理爆炸效果持续时间的跟踪。第 144–158 行的最后一个代码块处理持续时间到期后关闭爆炸效果。
在继续之前,请务必仔细阅读并理解这些代码。这就把我们带到了主方法回顾部分的末尾。接下来,我们将看看CarSensorScript
的实际演示。
演示:CarSensorScript
CarSensorScript
类有点复杂,所以实际上有两个演示场景供我们回顾。像往常一样,对汽车的控制需要几秒钟的时间。第一个演示场景命名为“DemoCarSensorScriptAutoPass”。你可以在“场景”文件夹的“项目”面板中找到它。在这个场景中,如果你慢慢靠近你前面的悬停赛车,自动通过功能将会激活。尝试一下,并确保在尝试时考虑代码。
在第二个演示场景“democarsensorscriptshooting”中,您必须使用棋盘上两个战斗模式修改器中的一个来武装您的悬停赛车。接下来,靠近对手的车,等一会儿,直到你听到瞄准的声音效果。接下来,听听枪声。如果你开火多次,对手的车将会跳跃,因为它会重新定位到先前的航路点进行重新射击。
图 5-1
汽车传感器脚本演示场景 1 描述汽车传感器脚本射击演示场景的屏幕截图
图 5-2
汽车传感器脚本演示场景 2 描述汽车传感器脚本自动超车演示场景的屏幕截图
前面显示的屏幕截图描述了用于演示该类的两个不同场景。这就是我们课程复习的结论。
第二章结论
这就引出了本章的结论。让我们来看看我们在这里讨论过的材料。这应该总结了第二章游戏规范中列出的所有游戏交互驱动的游戏机制。
-
CollideScript:这个类是一个集中的交互点,支持一些不同的游戏机制:
-
未标记:忽略的游戏对象。
-
可击中的:碰撞时飞出的物体;想想“油桶”
-
助推:几个不同的助推标记,当碰撞时加速汽车。
-
跳跃:一个交互标记,当与玩家的车发生碰撞时,会导致玩家的车跳跃。
-
战斗模式标记:交互标记只在战斗模式下可用。这些标记控制战斗模式修改器,例如:
-
弹药
-
枪
-
健康
-
装甲
-
无敌
-
-
-
CarSensorScript:这个类是第二个集中的交互点,支持一些不同的游戏机制:
-
自动超车:以一定的速度和距离紧跟一辆车会触发自动超车游戏机制。
-
射击尝试:当你打开枪的时候,在足够长的时间内紧紧跟随一辆车,你的车会瞄准并试图射击你前面的车。
-
有了这些新的游戏机制,我们几乎有了一个完整的赛车游戏。我们需要的只是一个中央游戏状态、输入处理程序和一些助手类。请注意不同的脚本组件如何通过我们在使用中看到的集中式GameState
和PlayerState
类实例来控制当前玩家的汽车和 AI 控制的汽车。这种查找玩家状态数据的方法有助于悬停赛车手与他们的环境以及其他人进行交互。在下一章,我们将放慢一点速度,看看一些助手类。