六、助手类
在这一章中,我们将回顾支持悬停赛车游戏不同特性的各种辅助类。一般来说,辅助类是较小的类,用于辅助某些游戏相关的进程。总的来说,这一章应该没有前一章那么紧张,因为大多数课程都是简单直接的。在某些情况下,我会试着提前记下来,我们会放弃课堂复习模板,以换取更短、更直接的过程。我们将回顾的前两个类用于处理飞出轨道的汽车或物体。
课堂回顾:DestroyScript
DestroyScript
类负责处理从棋盘上掉落的游戏对象。不要让它们无限下降,你应该设计你的关卡来处理出现在奇怪地方的物体。悬浮赛车的默认赛道旨在处理物体和悬浮赛车从赛道甚至棋盘上掉落的情况。让我们来看看一些代码!在这种情况下,我们将在一个复习步骤中列出整个班级。
1 public class DestroyScript : WaterResetScript {
2 public override void OnTriggerEnter(Collider otherObj) {
3 if (otherObj.gameObject.CompareTag(Utilities.TAG_PLAYERS)) {
4 base.OnTriggerEnter(otherObj);
5 } else {
6 Destroy(otherObj.gameObject);
7 }
8 }
9 }
Listing 6-1DestroyScript Class Review 1
注意,该类扩展了WaterResetScript
类。这里定义并被覆盖的唯一方法是OnTriggerEnter
方法。这个方法在与另一个单位GameObject
碰撞时触发。如果碰撞的对象被标记为玩家对象,则调用基类方法。对于任何其他类型的游戏对象,该对象被销毁,第 6 行。正如你所看到的,这个类是为清理事物而设计的。接下来我们来看看WaterResetScript
级。
课堂回顾:WaterResetScript
当悬停选手飞入水障碍时,WaterResetScript
级负责将他们推回赛道。该课程比我们刚刚复习的DestroyScript
稍长,因此我们将采用 stand 课程复习流程:
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
WaterResetScript
类扩展了BaseScript
类,因此是一个MonoBehaviour
。没有静态的类成员、枚举或类字段,所以我们将从相关的方法概述部分开始类回顾过程。
相关的方法大纲/类头:WaterResetScript
WaterResetScript
类的相关方法概述如下。
//Main Methods
void Start();
public virtual void OnTriggerEnter(Collider otherObj);
//Support Methods
public void ProcessWaterReset(Collider otherObj);
Listing 6-2WaterResetScript Pertinent Method Outline/Class Headers 1
接下来,我将列出该类的导入语句和声明。
using UnityEngine;
public class WaterResetScript : BaseScript {}
Listing 6-3WaterResetScript Pertinent Method Outline/Class Headers 2
这就引出了方法和类声明大纲的结论。下一个要复习的部分是类的支持方法。
支持方法详细信息:WaterResetScript
WaterResetScript
类有一个支持方法供我们回顾,如下所示。
01 public void ProcessWaterReset(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 pi, out int playerIndex, out p, otherObj.gameObject, gameState, false);
08 if (p != null) {
09 if (p.waypoints != null && p.waypoints.Count > 0) {
10 //move car to waypoint center
11 if (p.aiWaypointIndex - 5 >= 0) {
12 p.aiWaypointIndex -= 5;
13 } else if (p.aiWaypointIndex - 4 >= 0) {
14 p.aiWaypointIndex -= 4;
15 } else if (p.aiWaypointIndex - 3 >= 0) {
16 p.aiWaypointIndex -= 3;
17 } else if (p.aiWaypointIndex - 2 >= 0) {
18 p.aiWaypointIndex -= 2;
19 } else if (p.aiWaypointIndex - 1 >= 0) {
20 p.aiWaypointIndex -= 1;
21 } else {
22 p.aiWaypointIndex = 0;
23 }
24
25 if (p.aiWaypointIndex >= 0 && p.aiWaypointIndex < p.waypoints.Count) {
26 p.MoveToCurrentWaypoint();
27 }
28
29 p.offTrack = false;
30 p.offTrackTime = 0;
31 }
32 }
33 }
34 }
Listing 6-4WaterResetScript Support Method Details 1
在第 2–4 行,如果检测到配置问题,该方法将被转义。第 6 行检查碰撞对象是否被标记为“玩家”。如果是这样,碰撞对象的关联玩家的玩家状态被加载到第 7 行。关键结果存储在从BaseScript
类继承的p
类字段中。如果第 8 行定义了p
,如果第 9 行定义了轨迹的航点数据,我们将查找之前的航点索引。在第 11–23 行,我们从当前航路点索引后面的五个索引开始寻找前一个航路点。
如果找到有效的索引,则更新PlayerState
类的aiWaypointIndex
字段的值。既然已经选择了一个航路点索引,我们再次检查它确实是有效的,第 25 行;然后我们将碰撞玩家的悬停赛车移动到第 26 行的航路点索引处。最后,玩家的离轨状态在第 29–30 行被重置。
主要方法详细信息:WaterResetScript
WaterResetScript
类有两个主要的方法供我们回顾:Start
和OnTriggerEnter
方法。我将它们枚举如下,然后我们快速回顾一下。
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 virtual void OnTriggerEnter(Collider otherObj) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05 ProcessWaterReset(otherObj);
06 }
Listing 6-5WaterResetScript Main Method Details 1
在 Unity 游戏引擎中,Start
方法作为MonoBehaviour
生命周期的一部分被调用。在这种情况下,该方法作为MonoBehaviour
初始化的一部分被调用。调用Prep
方法为当前玩家加载GameState
和PlayerState
信息。如果遇到任何问题,该类将按名称标记为非活动。对这个结果的快速测试在第 3 行执行,然后在第 4 行记录一些日志。这是我们以前见过的BaseScript
class’ Start
方法的一个标准、简单的扩展实现。
前面列出的第二个主要方法是OnTriggerEnter
回调方法。响应冲突事件时调用此方法。如果脚本处于非活动状态,该方法不做任何工作就返回,第 2–4 行。如果没有,那么在第 5 行调用ProcessWaterReset
方法。这就是我们对主要方法回顾的结论。接下来,我们将看看课堂演示部分。
演示:WaterResetScript
因为DestroyScript
扩展了WaterResetScript
,所以我决定在这个回顾部分演示这两个脚本。DestroyScript
在功能上与WaterResetScript
非常相似,所以我们不会在这里明确演示。然而,我们将明确展示WaterResetScript
的作用。
进入“项目”面板,找到“场景”文件夹。找到“DemoWaterResetScript”场景,但暂时不要打开它。我想先描述一下演示场景是如何工作的。几秒钟后,你就可以控制悬浮赛车了。在游戏窗口内点击,并确保它被激活输入。汽车前面会有两根绿色的柱子,开车穿过它们,瞄准紫色柱子之间的跳跃点。你将在空中飞行一会儿,直到你撞上隐形的WaterResetScript
启动的飞机。
这个游戏对象在“层级”面板中被标记为“水重置”。该脚本将在碰撞时触发,并将玩家送回几个航路点。除了它会破坏任何与它碰撞并且没有“玩家”标签的物体之外,DestroyScript
类的功能是一样的。在继续之前,先玩一会演示场景,确保您理解它是如何工作的。下一个要复习的课程是控制 hover racer 的引擎音效。
课程回顾:工程描述
EngineWhineScript
类负责控制引擎音效的音频、音量、音高。脚本组件调整AudioSource
组件的回放属性,以更快的速度创建更大、更高音调的引擎声音。花点时间在我们复习过的所有其他课程的背景下思考这个课程。有什么突出的吗?
您可能已经注意到的一件事是,脚本组件在某种程度上是分隔开的。它们在Update
方法调用或其他碰撞事件回调方法期间对自己进行配置并对游戏对象进行调整。让我们从以下复习步骤开始课堂复习:
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
事不宜迟,我们来看看一些代码吧!
静态/常量/只读类成员:EngineWhineScript
EngineWhineScript
类有一个静态字段供我们查看,随后列出。
public static string AUDIO_SOURCE_NAME_WHINE = "car_idle_lp_01";
Listing 6-6EngineWhineScript Static/Constants/Read-Only Class Members 1
列出的字段具有将用于引擎音效的AudioSource
组件的名称。接下来,我们将看看其余的类字段。
类字段:EngineWhineScript
EngineWhineScript
类有一个由类的‘Update
方法内部使用的字段。
//***** Internal Variables: Update *****
private float pTmp = 0.0f;
Listing 6-7EngineWhineScript Class Fields 1
该类使用pTmp
字段来保存玩家速度百分比的副本。该值用于设置由audioS
基类字段引用的AudioSource
组件的pitch
和volume
。
相关的方法大纲/类头:EngineWhineScript
EngineWhineScript
类的相关方法概述如下。
//Main Methods
void Start();
void Update();
Listing 6-8EngineWhineScript Pertinent Method Outline/Class Headers 1
这里列出了类导入语句和声明。
using UnityEngine;
public class EngineWhineScript : BaseScript { }
Listing 6-9EngineWhineScript Pertinent Method Outline/Class Headers 2
在接下来的复习部分,我们将看看这个类的主要方法。
主要方法详细信息:EngineWhineScript
EngineWhineScript
类有两个主要的方法让我们复习。这两种方法应该看起来很熟悉。让我们来看看。
01 void Start() {
02 base.PrepPlayerInfo(this.GetType().Name);
03 if (BaseScript.IsActive(scriptName) == false) {
04 Utilities.wrForce(scriptName + ": Is Deactivating...");
05 return;
06 }
07
08 audioS = Utilities.LoadAudioResources(GetComponents<AudioSource>(), new string[] { AUDIO_SOURCE_NAME_WHINE })[0];
09 if (audioS != null) {
10 audioS.volume = 0.2f;
11 audioS.pitch = 0.2f;
12 }
13 }
Listing 6-10EngineWhineScript Main Method Details 1
这个Start
方法比默认的标准方法稍微复杂一些,默认的标准方法只通过调用基类的Prep
方法来加载游戏状态信息。这个方法调用更复杂的PrepPlayerInfo
基类方法。这个方法也将载入玩家状态信息和游戏状态。它还使用LoadAudioResources
实用程序方法来查找并加载对名称与AUDIO_SOURCE_NAME_WHINE
静态类字段匹配的AudioSource
组件的引用。如果在第 9 行找到了音频资源,那么音效的音量和音高将被调整为默认值 0.2。
注意,每个扩展了BaseScript
类并使用了两种类准备方法之一Prep
或PrepPlayerInfo
的类都调用了GameState
类的PrepGame
方法。这种冗余的原因是我们无法保证哪个GameObject
的Start
方法会被首先调用。为了安全起见,游戏中的每个脚本组件都会检查以确保游戏已经作为类配置的一部分准备好了。接下来我们来看看Update
方法。
01 void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (p != null) {
07 pTmp = p.speedPrct;
08 if (audioS != null) {
09 audioS.pitch = Mathf.Clamp(pTmp * 4.1f, 0.5f, 4.1f); //p is clamped to sane values
10 audioS.volume = Mathf.Clamp(pTmp * 0.6f, 0.2f, 0.6f);
11 }
12 }
13 }
Listing 6-11EngineWhineScript Main Method Details 2
第 2–4 行中的Update
方法被转义,就像我们一次又一次看到的一样。在第 6 行,我们检查是否定义了PlayerState
实例p
。如果是这样的话,那么我们用第 7 行中的 hover racer 的当前速度百分比更新pTmp
类字段。因为我们明确地调整了 hover racer 的引擎声音,我们只能在音频资源audioS
已经被正确定义的情况下继续,第 8 行。如果字段不为空,那么我们调整第 9 行和第 10 行的发动机呜呜声效果的pitch
和volume
。注意,这些值是基于pTmp
字段的。换句话说,音高和音量是基于悬停赛车的当前速度。这就把我们带到了主方法回顾部分的末尾。在下一节中,我们将看看本课程的演示场景。
演示:引擎描述
这个类的演示场景可以在“项目”面板的“场景”文件夹中找到。找到名为“DemoEngineWhineScript”的场景。这个演示简单而直接。绕着棋盘开,尽可能快地跑。请注意屏幕左下角显示的音高和音量值。请记住,这些值是基于悬停赛车的当前速度。这就是我们对EngineWhineScript
班复习的总结。我们要看的下一个类是LapTime
类。
课堂回顾:LapTime
LapTime
类不是基于MonoBehaviour
的类;只是一个飞机老 C# 类。该类负责包装赛道时间信息,并提供了将该类的单圈时间与字符串表示形式进行序列化和反序列化的方法。这节课有些长,但我还是觉得可以一口气复习完。接下来我们将直接复习该课程。
01 public class LapTime {
02 public string time = "";
03 public int timeNum = 0;
04 public int track = 0;
05 public int type = 0; //0=easy, 1=battle, 2=classic
06 public int diff = 0; //0=low, 1=med, 2=high
07 public int lap = 0;
08
09 public string Serialize() {
10 string t = time;
11 string tN = timeNum + "";
12 string tr = track + "";
13 string ty = type + "";
14 string df = diff + "";
15 string l = lap + "";
16 string col = "~";
17 return (t + col + tN + col + tr + col + ty + col + df + col + l);
18 }
19
20 public override string ToString() {
21 string ret = "";
22 ret += "Time: " + time + "\n";
23 ret += "Time Number: " + timeNum + "\n";
24 ret += "Track: " + track + "\n";
25 ret += "Type: " + type + "\n";
26 ret += "Difficulty: " + diff + "\n";
27 ret += "Lap: " + lap + "\n";
28 return ret;
29 }
30
31 public bool Deserialize(string s) {
32 if (s != null && s != "") {
33 char[] c = "~".ToCharArray();
34 string[] cs = s.Split(c);
35 if (cs.Length == 6) {
36 time = cs[0] + "";
37 timeNum = int.Parse(cs[1]);
38 track = int.Parse(cs[2]);
39 type = int.Parse(cs[3]);
40 diff = int.Parse(cs[4]);
41 lap = int.Parse(cs[5]);
42 return true;
43 } else {
44 return false;
45 }
46 } else {
47 return false;
48 }
49 }
50 }
Listing 6-12LapTime Class Review 1
第 1 行显示了LapTime
类声明。类别字段列在第 2–7 行。第一个字段time
,保存当前圈速的字符串表示。timeNum
字段将圈速表示为一个大整数。track
字段是一个整数值,表示正在哪个赛道上比赛。type
字段是一个表示比赛类型的整数值:简单赛、战斗赛或经典赛。接下来,diff
字段用于跟踪比赛的难度:低、中、高。最后,我们有lap
类字段。这表示与当前分段时间相关的分段数。
LapTime
类有三个方法让我们复习。前两个方法,Serialize
和ToString
,用于将类表示为字符串。Serialize
方法将类字段的所有字符串表示转换成一个长字符串,其条目由col
变量分隔,第 17 行。ToString
方法类似于Serialize
方法。
在ToString
方法中,类字段被给定标签并由换行符分隔,第 21–27 行。结果字符串在第 28 行返回。我们最后要复习的方法是Deserialize
法。此方法反转序列化过程,并根据解码的数据更新类字段的值。传入的字符串参数在第 34 行被拆分。如果找到了期望的长度,数据被转换并赋值,第 36–41 行。方法返回一个布尔值,指示反序列化过程成功。我们将跳过这个类的演示部分,因为我们将在接下来复习LapTimeManager
类时看到它的使用。
课堂回顾:LapTimeManager
LapTimeManager
类负责管理一组记录的圈速。与LapTime
级类似,LapTimeManager
级不是MonoBehaviour
;只是一个飞机老 C# 类。LapTimeManager
类足够复杂,使用我们以前用过的更结构化的类评审过程会更好。
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
关于这个类没有什么特别要讨论的,所以事不宜迟,让我们看看一些代码吧!
静态/常量/只读类成员:LapTimeManager
LapTimeManager
类有两个静态成员供我们查看。
public static int CLEANING_START_INDEX = 33;
public static int LAST_NOT_CLEANED_INDEX = 32;
Listing 6-13LapTimeManager Static/Constants/Read-Only Class Members 1
列出的第一个字段用于标记索引,该索引将在尝试从分段时间管理器中清除条目时使用。列出的第二个字段是类的清理过程不会清理的最后一个索引。这意味着,随着保存的分段时间的增加和清理代码的执行,只有前 32 个条目将被保留。条目 33 及以上都要进行清理。注意,LapTimeManager
和LapTime
类没有扩展BaseScript
类。你能想出原因吗?
主要原因是这些类不是 Unity MonoBehaviours
,脚本组件。它们是标准的 C# 类。因此,类是通过调用类构造函数和其他类配置方法来初始化的。然而,MonoBehaviour
s 是 Unity 游戏引擎的组件,由组件生命周期回调方法初始化,如Awake
和Start
。这些初始化方法作用于真实的游戏环境,并且必须能够在运行时处理缺失的配置资源。这就是为什么我们支持禁用扩展了MonoBehaviour
类功能的类,方法是将该类注册为非活动的。让我们转到班上的其他领域。
类字段:LapTimeManager
LapTimeManager
类有一些公共字段和一些私有字段,用作局部方法变量。
//***** Class Fields *****
public List<LapTime> lapTimes = new List<LapTime>();
public LapTime lastEntry = null;
public LapTime bestEntry = null;
//***** Internal Variables: FindBestLapTime *****
private LapTime retFbt = null;
private int lFbt = 0;
private int iFbt = 0;
//***** Internal Variables: CleanTimes *****
private LapTime ltFbt = null;
private int lCt = 0;
private int iCt = 0;
Listing 6-14LapTimeManager Class Fields 1
集合中的第一个类字段是lapTimers
列表。该列表负责存储个人单圈时间记录。以下两个字段用于参考重要的单圈时间。lastEntry
字段保存上次记录的单圈时间,而bestEntry
字段保存最佳记录的单圈时间。下一组类字段由FindBestLapTime
和CleanTimes
方法使用。retFbt
字段是一个用于保存返回值的LapTime
实例。
lFbt
和iFbt
字段是循环控制变量,用于循环记录的圈速列表。ltFbt
字段是另一个LapTime
实例,用于在遍历一个列表时保存当前的圈速。最后,lCt
和iCt
字段用于在清除旧条目时控制记录圈速列表的循环。接下来的两组字段由Serialize
和Deserialize
方法使用。
//***** Internal Variables: Serialize/ToString *****
private string retSer = "";
private int lSer = 0;
private int iSer = 0;
private string retTs = "";
private int lTs = 0;
private int iTs = 0;
//***** Internal Variables: Deserialize *****
private char[] cDes = null;
private string[] csDes = null;
private int lDes = 0;
private int iDes = 0;
private string tmpDes = "";
private LapTime ltDes = null;
Listing 6-15LapTimeManager Class Fields 2
前面列出的第一个字段retSer
是一个字符串,用于构建记录的圈速的序列化列表。在序列化过程中,lSer
和iSet
字段被用作循环控制变量。类似地,retTs
字段用于构建类的 to-string 表示,lTs
和iTs
字段在 to-string 操作中用作循环控制变量。下一组字段由Deserialize
方法使用。
cDes
字段用于表示分割字符,该字符用于分割字符串形式的圈速列表。随后,csDes
字段用于保存所述分割的结果。lDes
和iDes
字段是在反序列化过程中使用的循环控制变量。tmpDes
字段用于在集合循环时保存反序列化的字符串。最后,ltDes
字段是一个LapTime
实例,用于将字符串反序列化为存储在lapTimes
列表中的LapTime
对象。在下一节中,我们将看看这个类的相关方法大纲。
相关的方法大纲/类头:LapTimeManager
LapTimeManager
类的相关方法概述如下。
//Main Methods
public string Serialize();
public bool Deserialize(string s);
public override string ToString();
public void CleanTimes();
//Support Methods
public void AddEntry(LapTime lt);
public LapTime FindBestLapTimeByLastEntry();
public LapTime FindBestLapTime(int track, int type, int diff, int timeNum);
public string ToStringShort();
Listing 6-16LapTimeManager Pertinent Method Outline/Class Headers 1
接下来,我将列出该类的导入语句和声明。注意这个类没有扩展 Unity 的MonoBehaviour
类。
using System.Collections.Generic;
public class LapTimeManager {}
Listing 6-17LapTimeManager Pertinent Method Outline/Class Headers 2
在下一个复习部分,我们将看看类的支持方法。
支持方法详细信息:LapTimeManager
LapTimeManager
类有一些支持方法供我们回顾。让我们来看看。
01 public void AddEntry(LapTime lt) {
02 lapTimes.Add(lt);
03 lastEntry = lt;
04 }
01 public LapTime FindBestLapTimeByLastEntry() {
02 return FindBestLapTime(lastEntry.track, lastEntry.type, lastEntry.diff, lastEntry.timeNum);
03 }
01 public LapTime FindBestLapTime(int track, int type, int diff, int timeNum) {
02 retFbt = null;
03 ltFbt = null;
04 if (lapTimes != null) {
05 lFbt = lapTimes.Count;
06 for (iFbt = 0; iFbt < lFbt; iFbt++) {
07 ltFbt = (LapTime)lapTimes[iFbt];
08 if (ltFbt != null) {
09 if (ltFbt.track == track && ltFbt.type == type && ltFbt.diff == diff) {
10 if (ltFbt.timeNum < timeNum) {
11 retFbt = ltFbt;
12 }
13 }
14 }
15 }
16 }
17 bestEntry = retFbt;
18 return retFbt;
19 }
01 public string ToStringShort() {
02 return "Lap Times: " + lapTimes.Count;
03 }
Listing 6-18LapTimeManager Support Method Details 1
列出的第一种方法AddEntry
用于将新的单圈时间记录添加到当前单圈时间列表的第 2 行。注意,当调用这个方法时,lastEntry
字段被更新,第 3 行。我们要回顾的下一个方法是一个方便的方法,FindBestLapTimeByLastEntry
,它用关于最后一次单圈时间的信息调用类’FindBestLapTime
方法,第 2 行。这就把我们带到了FindBestLapTime
方法。
FindBestLapTime
方法用于搜索已知圈速列表,以找到匹配比赛设置(赛道、类型和难度参数)且持续时间最短的条目。在第 2–3 行,方法变量retFbt
和ltFbt
被重置为空。在第 4 行,如果lapTimes
字段不为空,那么我们遍历条目,搜索持续时间最短的匹配项,即第 5–6 行。lapTimes
列表中的每个条目被分配给第 7 行的ltFbt
局部变量。如果分段时间条目已定义,与搜索标准匹配,并且持续时间较短,则分段时间被设置为该方法返回的数据,第 8–11 行。
集合中的最后一个方法是ToStringShort
方法。这个方法非常简单,它返回一个字符串,指示由该类管理的圈数。如您所见,这个类在处理单圈时间数据时非常有用。接下来,我们将看看LapTimeManager
类的主要方法。
主要方法细节:LapTimeManager
LapTimeManager
类有几个主要的方法用于管理存储在类中的圈速记录。我在这里一组列出主要的方法。
01 public string Serialize() {
02 retSer = "";
03 if (lapTimes != null) {
04 lSer = lapTimes.Count;
05 for (iSer = 0; iSer < lSer; iSer++) {
06 if (lapTimes[iSer] != null) {
07 retSer += ((LapTime)lapTimes[iSer]).Serialize();
08 if (iSer < lSer - 1) {
09 retSer += "^";
10 }
11 }
12 }
13 }
14 return retSer;
15 }
01 public bool Deserialize(string s) {
02 lapTimes = new List<LapTime>();
03 if (s != null && s != "") {
04 cDes = "^".ToCharArray();
05 csDes = s.Split(cDes);
06 if (csDes != null && csDes.Length > 0) {
07 lDes = csDes.Length;
08 for (iDes = 0; iDes < lDes; iDes++) {
09 tmpDes = csDes[iDes];
10 if (tmpDes != null) {
11 ltDes = new LapTime();
12 ltDes.Deserialize(tmpDes);
13 lapTimes.Add(ltDes);
14 }
15 }
16 return true;
17 } else {
18 return false;
19 }
20 } else {
21 return false;
22 }
23 }
01 public override string ToString() {
02 retTs = "";
03 if (lapTimes != null) {
04 lTs = lapTimes.Count;
05 for (iTs = 0; iTs < lTs; iTs++) {
06 if (lapTimes[iTs] != null) {
07 retTs += "Lap Time Entry: " + (iTs + 1) + "\n";
08 retTs += ((LapTime)lapTimes[iTs]).ToString() + "\n";
09 }
10 }
11 }
12 return retTs;
13 }
01 public void CleanTimes() {
02 if (lapTimes != null && lapTimes.Count > 1) {
03 if (lapTimes.Count > LAST_NOT_CLEANED_INDEX) {
04 lCt = lapTimes.Count;
05 for (iCt = CLEANING_START_INDEX; iCt < lCt; iCt++) {
06 lapTimes.Remove(lapTimes[iCt]);
07 lCt--;
08 }
09 }
10 }
11 }
Listing 6-19LapTimeManager Main Method Details 1
该组中的第一个方法是Serialize
方法。该方法用于将LapTimeManager
类转换为字符串表示,这样它就可以存储在游戏的玩家首选项中。这将允许它在游戏的多次使用中保持不变。为什么要经历这些序列化的麻烦呢?为什么不直接存储数据呢?事实证明,依靠序列化更容易也更强大,因为它简化了游戏、记录的圈数和 Unity 的PlayerPrefs
简单数据持久化类之间的交互。
如果你回想一下我们对LapTime
类的回顾,你会记得它也支持字符串形式的相互转换。这意味着你可以将一个字符串转换成一个 C# 对象实例,反之亦然。这里的强大之处在于,您可以通过编程方式与类进行交互,并且只需一次方法调用,就可以将类转换为字符串形式。了解这一点后,让我们看看圈速管理器如何利用这一功能。
用于返回值的变量在第 2 行被重置。如果定义了lapTimes
字段,我们准备循环控制变量并循环存储的圈速,第 3-5 行。如果第 6 行定义了lapTimes
条目,我们将序列化该类并将其添加到第 7–10 行的retSer
变量中。如果我们有更多的条目要处理,我们会在它们之间添加一个特殊的字符,‘^’。结果是存储在LapTimeManager
类中的分段时间的序列化。
在Deserialize
方法中,我们反转这个过程,将一个字符串,一个圈数字符串的序列化列表,转换成一个LapTime
对象实例的列表。在该方法的第 2 行重置了lapTimes
类字段。如果方法参数 s 已定义并有数据,我们准备方法变量,第 4-5 行。如果在特殊字符上分割的字符串返回结果,我们准备循环控制变量并继续在分割的字符串上循环。tmpDes
变量用于保存第 9 行的每个字符串片段。
在第 10–12 行,如果定义了条目,我们将它用作新的lapTime
实例的Deserialize
方法的参数。新配置的LapTime
实例被添加到第 13 行的lapTimes
类字段中。该方法的其余部分返回一个布尔值,表明操作成功。当这个方法完成时,我们将把这个类恢复到它的Serialize
方法被调用时的状态。
ToString
方法用于为日志记录创建类的字符串表示。方法返回变量retTs
在第 2 行被重置,如果lapTimes
字段被定义,我们在第 3–5 行遍历列表。每个条目都被处理并附加到第 6–9 行的返回变量中。生成的字符串在第 12 行返回。接下来,我们将看看我们如何修剪记录的圈速。
CleanTimes
方法用于修剪由 manager 类存储的圈数。如果有分段时间要处理,并且条目的数量超过了指定的数量,我们准备循环处理超出的条目,第 3–5 行。对于发现的每个超额条目,我们删除第 6–7 行的值并减少循环控制变量。这就结束了我们对LapTimeManager
类的回顾。接下来,我们将检查这个类实际上是如何使用的。
演示:LapTimeManager
为了演示LapTimeManager
类,以及随后的LapTime
类,我们将看看一些类的功能在实际游戏中是如何使用的。首先,我们将看到如何使用管理器来保存单圈时间数据。如果你加载了GameState
类并搜索了LogLapTime
方法,看看这个方法的最后几行代码。
1 lapTimeManager.AddEntry(lt);
2 lapTimeManager.CleanTimes();
3 lapTimeManager.FindBestLapTimeByLastEntry();
4 PlayerPrefs.SetString("LapTimes", lapTimeManager.Serialize());
Listing 6-20LapTimeManager Demonstration 1
这有点乏味,但那是因为我们的类封装得很好,并且在设计上很专注。在前面列出的例子中,一个新的圈时间条目被添加到列表中。然后,管理器清理这个列表,以确保它的长度有上限。最后,单圈时间的序列化列表存储在 Unity APIPlayerPrefs
类的第 4 行。接下来,我们将看看如何使用反序列化。
1 string tmpStr = PlayerPrefs.GetString("LapTimes", "");
2 lapTimeManager = new LapTimeManager();
3 if (tmpStr != null && tmpStr != "") {
4 Utilities.wr("Found lap times: " + tmpStr);
5 lapTimeManager.Deserialize(tmpStr);
6 }
Listing 6-21LapTimeManager Demonstration 2
序列化过程的逆过程也一样简单。我们加载最后存储的圈速字符串;如果不存在,则返回一个空字符串,第 1 行。然后我们初始化lapTimeManager
类字段,并检查是否有数据要处理,第 2–3 行。我们找到的数据被记录下来,并且通过调用Deserialize
方法来恢复lapTimeManager
类。我们的LapTimeManager
课复习到此结束。下一个要复习的课是PopupMsgTracker
课。
课堂回顾:PopupMsgTracker
PopupMsgTracker
类用于跟踪 HUD 屏幕上显示的弹出通知,以响应某些跟踪事件。因为多个通知可能会快速连续地触发,所以我们最终可能会显示多个通知图像。为了帮助我们管理这些信息,我们引入了PopupMsgTracker
类。让我们来看看。因为这个课程非常简洁,我们将放弃通常的课程复习过程,只列出下一个课程。
1 public class PopupMsgTracker {
2 public int index = 0;
3 public Image image = null;
4 public int posIdx = 0;
5 public bool movingUp = false;
6 public int type = 0;
7 }
Listing 6-22PopupMsgTracker Class Review 1
这个类非常简单;它只是一组类字段。让我们快速复习一下。第一个字段是一个整数值index
,它用于跟踪图像通知在通知垂直列表中的位置。下一个类字段是 Unity Image
类的一个实例。该字段引用屏幕上显示的图像,即实际通知。下一个字段posIdx
是一个整数值,表示通知的位置索引,或者在可用显示槽的预设列表中的显示槽的索引。
下一个类字段是布尔标志,指示该通知在可用显示槽列表中向上移动,movingUp
。最后,type
字段是一个整数值,表示该类正在跟踪的通知类型:警告、修饰符、帮助消息等。这就是这门课的全部内容。注意,它不是基于MonoBehaviour
类;只是一个普通的老 C# 类。我们要看的下一个类是集中式实用程序类。但是在我们开始之前,让我们看一个PopupMsgTracker
类的演示。
演示:PopupMsgTracker
为了演示PopupMsgTracker
类,我们将看看GameHUDNewScript
类是如何使用它的。这个脚本负责在游戏中的 HUD 上显示通知等。让我们看看如何使用消息跟踪器类。
1 public void UndimYourHit() {
2 if (imgYourHit.gameObject.activeSelf == false) {
3 imgYourHit.gameObject.SetActive(true);
4 imgYourHit.gameObject.transform.position = posPopupMsg[posPopupMsgIdx];
5 AddPopupMsgTracker(imgYourHit, posPopupMsgIdx, 0);
6 CheckPopupMsgIdx();
7 }
8 }
1 public void AddPopupMsgTracker(Image i, int idx, int tp) {
2 pt = new PopupMsgTracker();
3 pt.index = posPopupMsgVis.Count;
4 pt.posIdx = idx;
5 pt.image = i;
6 pt.type = tp;
7 posPopupMsgVis.Add(pt);
8 posPopupMsgIdx++;
9 }
Listing 6-23PopupMsgTracker Demonstration 1
我们要看的第一个方法是UndimYourHit
方法。这种方法被 HUD 用来在战斗模式比赛中显示“你被击中了”的通知。如果第 2 行的通知图像不可见,请在第 3 行激活它。通知图像的位置基于由第 4 行的posPopupMsgIdx
字段跟踪的可用显示槽来设置。请注意,索引用于在显示位置数组中查找设置的显示位置。
接下来,在第 5 行,通过调用AddPopupMsgTracker
方法来跟踪弹出通知。最后一行代码检查下一个posPopupMsgIdx
值是否有效,如果无效,则将索引值循环回零。这将重置下一个通知的显示位置。列出的下一个演示方法是AddPopupMsgTracker
方法。请注意,索引是通知在可见通知列表中的位置,第 3 行。
posIdx
字段被设置为参数idx
,它是posPopupMsgIdx
的值。这表示通知的实际呈现位置,第 4 行。弹出消息被添加到第 7 行的可视通知列表中,并且popupMsgIdx
随后在第 8 行递增。这就是我们对PopupMsgTracker
课的总结。接下来,我们将注意力集中在Utilities
类上。
课程回顾:实用工具
Utilities
类也不是MonoBehaviour
。它只是一个普通的 C# 类,碰巧处理一些重要的职责。Utilities 类用作中央初始化、日志记录和设置类。该类被设计为一个静态的快速访问类。因此,它只有静态类成员。我们将稍微调整一下我们的复习过程,以考虑到这个类的静态性质。我们将采用以下审查步骤:
-
静态类字段
-
静态类方法
-
示范
我们将从查看类的静态字段开始。让我们看看我们有什么。
静态类成员:实用程序
有相当多的静态类字段需要查看,所以我们将分组查看它们。
public static bool LOGGING_ON = true;
public static string SOUND_FX_JUMP = "buzzy_jump_01";
public static string SOUND_FX_BOUNCE = "cute_bounce_01";
public static string SOUND_FX_BOOST = "rocket_lift_off_rnd_06";
public static string SOUND_FX_POWER_UP = "powerup_01";
public static string TAG_TRACK_HELP_SLOW = "TrackHelpSlow";
public static string TAG_TRACK_HELP_TURN = "TrackHelpTurn";
public static string TAG_PLAYERS = "Players";
Listing 6-24Utilities Static Class Fields 1
前面列出了我们要查看的第一组静态字段。第一个条目LOGGING_ON
,控制类的日志功能。下一个字段SOUND_FX_JUMP
,是游戏中使用的目标跳跃音效的名称。SOUND_FX_BOUNCE
是用作反弹音效的目标反弹AudioSource
的名称。接下来的两个条目是目标AudioSource
的名字,分别用作游戏加速和加电机制的音效。
下一组三个字段以TAG_
开始,用于检查分配给游戏对象的标签。标签用于对游戏对象进行分类。它们是一种区分游戏对象的低级方法,通常用于碰撞处理。如果你回想一下到目前为止我们已经检查过的一些代码,我们已经检查了很多标签。前两个标签条目用于检测帮助系统标记的对象。最后一个条目TAG_PLAYERS
,是一个非常重要的字段。它用于确定一个游戏对象是否是一个悬浮赛车。
public static string TAG_UNTAGGED = "Untagged";
public static string TAG_HITTABLE = "Hittable";
public static string TAG_HITTABLE_NOY = "HittableNoY";
public static string TAG_BOOST_MARKER = "BoostMarker";
public static string TAG_SMALL_BOOST_MARKER = "SmallBoostMarker";
public static string TAG_TINY_BOOST_MARKER = "TinyBoostMarker";
public static string TAG_MEDIUM_BOOST_MARKER = "MediumBoostMarker";
public static string TAG_TINY_BOOST_2_MARKER = "TinyBoostMarker2";
Listing 6-25Utilities Static Class Fields 2
该集合中列出的第一个字段TAG_UNTAGGED
用于标识标记为未标记的游戏对象。有默认的系统标签,无标签是其中之一,然后还有用户自定义标签。默认情况下,Unity 游戏对象被标记为“未标记”。当我们回顾这些标签时,我会提到与它们相关的游戏机制。接下来的两个条目是用于可点击对象的标签。这种标签和游戏机制最明显的例子是油桶,当被碰撞时会飞起来。
接下来的几个条目都用于定义不同的增强标记。如果你还记得我们对游戏交互类的评论。助推标记以不同的速度加速玩家的车辆前进。使用这个系统,您需要做的全部工作就是更改一个增强标记,将其标签更改为不同的增强标记标签类型。
public static string TAG_JUMP_MARKER = "JumpMarker";
public static string TAG_HEALTH_MARKER = "HealthMarker";
public static string TAG_GUN_MARKER = "GunMarker";
public static string TAG_INVINC_MARKER = "InvincibilityMarker";
public static string TAG_ARMOR_MARKER = "ArmorMarker";
public static int MAX_AMMO = 6;
public static int AMMO_INC = 3;
Listing 6-26Utilities Static Class Fields 3
前一组中列出的接下来的五个字段都是与战斗模式–
相关的标签。每个标签用于识别不同的战斗模式标记,当碰撞时,修改玩家的状态。接下来的两个静态字段定义了玩家可以持有的最大弹药量,随后的AMMO_INC
字段显示了从枪/弹药标记添加的弹药量。还有一组字段需要查看,如下所示。
public static string NAME_GAME_STATE_OBJ = "GameState";
public static string NAME_PLAYER_ROOT = "HoverCar";
public static string NAME_START_ROOT = "StartPosition";
public static float MAX_XFORM_POS_Y = 50.0f;
public static float MIN_XFORM_POS_Y = 12.0f;
public static int MARKER_REFRESH_MIN = 60;
public static int MARKER_REFRESH_MAX = 90;
Listing 6-27Utilities Static Class Fields 3
下一个静态类字段很重要。我们以前在很多类中见过这个字段,在BaseScript
类的准备方法中。NAME_GAME_STATE_OBJ
字段用于查找同名的游戏对象。在 Hover Racers 游戏中,游戏状态是由GameState
类跟踪的。该类已被添加到同名的空 Unity GameObject
中。
这就是为什么游戏中的每一个MonoBehaviour
都会搜索GameState
游戏对象,看看它是否有一个GameState
脚本组件与之相关联。这样的例子可以在Utilities
类的准备方法中看到,它被BaseScript
类使用。接下来的两个字段MAX_XFORM_POS_Y
和MIN_XFORM_POS_Y
用于确定玩家的悬停赛车是否在空中,是否应该被重力拉下来。最后但并非最不重要的是两个字段,用于确定给定战斗模式标记的刷新时间。
当玩家的悬停赛车与标记碰撞时,该标记会消失几秒钟,然后再次可见。MARKER_REFRESH_MIN
和MARKER_REFRESH_MAX
字段用于生成标记再次可见之前等待的随机时间。这就结束了该类的静态字段。接下来,我们将看看这个类的静态类方法。
1 public static void wr(string s) {
2 if (LOGGING_ON) {
3 Debug.Log(s);
4 }
5 }
1 public static void wrForce(string s) {
2 Debug.Log(s);
3 }
1 public static void wr(string s, string sClass, string sMethod, string sNote) {
2 if (LOGGING_ON) {
3 Debug.Log(sClass + "." + sMethod + ": " + sNote + ": " + s);
4 }
5 }
1 public static void wrErr(string s) {
2 if (LOGGING_ON) {
3 Debug.LogError(s);
4 }
5 }
1 public static void wrErr(string s, string sClass, string sMethod, string sNote) {
2 if (LOGGING_ON) {
3 Debug.LogError(sClass + "." + sMethod + ": " + sNote + ": " + s);
4 }
5 }
Listing 6-28Utilities Static Class Methods 1
Utilities
类有许多有用的日志记录方法供我们研究。列出的第一个方法是wr
,用于记录与日志记录控制变量相关的调用。wrForce
方法用于记录必须显示的调用。这个方法忽略了类的日志控制字段LOGGING_ON
。列出的第二个版本接受更多的参数;它处理将格式化的文本写入日志,但在其他方面的功能与它的对应物类似。随后的两种方法,wrErr
和更复杂版本的wrErr
方法,使用方式与它们的非错误表亲完全相同。这里的主要区别是wr
方法调用 Unity 的Debug.Log
方法,而wrErr
调用Debug.LogError
方法。接下来,让我们看看课堂的音频支持方法。
01 public static void SafePlaySoundFx(AudioSource audioS, string sClass, string sMethod, string sNote, string name) {
02 Utilities.wr("Playing sound " + name, sClass, sMethod, sNote);
03 SafePlaySoundFx(audioS);
04 }
01 public static void SafePlaySoundFx(AudioSource audioS) {
02 if (audioS != null) {
03 if (audioS.isPlaying == false) {
04 audioS.Play();
05 }
06 }
07 }
01 public static AudioSource[] LoadAudioResources(AudioSource[] audioSetSrc, string[] audioSetNames) {
02 AudioSource[] audioSetDst = null;
03 int count = 0;
04 if (audioSetSrc != null && audioSetNames != null) {
05 audioSetDst = new AudioSource[audioSetNames.Length];
06 for (int i = 0; i < audioSetSrc.Length; i++) {
07 AudioSource aS = (AudioSource)audioSetSrc[i];
08 for (int j = 0; j < audioSetNames.Length; j++) {
09 if (aS != null && aS.clip.name == audioSetNames[j]) {
10 Utilities.wr("Found audio clip: " + audioSetNames[j]);
11 audioSetDst[j] = aS;
12 count++;
13 break;
14 }
15 }
16
17 if (count == audioSetNames.Length) {
18 break;
19 }
20 }
21 }
22 return audioSetDst;
23 }
Listing 6-29Utilities Static Class Methods 2
前面列出的方法中的前两个条目支持集中式音频回放。第一个方法接受几个额外的参数,用于日志记录目的,第 2 行,然后在第 3 行调用简单版本的SafePlaySoundFx
方法。将我们的注意力引向接受单个参数的SafePlaySoundFx
方法,一个AudioSource
实例audioS
。此方法检查音频资源是否已定义,如果已定义,则播放声音。这一组中的最后一个方法是LoadAudioResources
方法。我们在加载音频资源的某些类Start
方法中遇到过几次这种方法。
该方法采用两个数组作为参数。第一个是要搜索的音频资源数组。第二个是字符串数组,这些字符串构成了要从第一个数组中提取的目标音频资源。这种方法相当简单。如果定义了方法参数,那么我们准备循环控制变量并在第 2–5 行返回数组。对于audioSetSrc
数组中的每个音频资源,我们循环遍历audioSetNames
数组来寻找匹配。如果找到一个,我们将它添加到结果数组中,并递增一个计数器,第 11–12 行。然后我们跳出内部循环,进入下一个音频资源。如果我们已经找到了我们要找的所有东西,我们就退出外部循环,第 17–19 行。该方法的结果在第 22 行返回,大概是在定位了所有需要的音频资源之后。接下来我们要回顾的一组方法是用来加载游戏和玩家状态数据的。让我们看一看。
01 public static object[] LoadStartingSet(string className, out GameState gameState) {
02 GameObject gameStateObj = GameObject.Find(Utilities.NAME_GAME_STATE_OBJ);
03 if (gameStateObj != null) {
04 gameState = gameStateObj.GetComponent<GameState>();
05 if (gameState != null) {
06 gameState.PrepGame();
07 return new object[] { gameStateObj, gameState, true };
08 } else {
09 Utilities.wrForce(className + ": gameState is null! Deactivating...");
10 return new object[] { gameStateObj, gameState, false };
11 }
12 } else {
13 Utilities.wrForce(className + ": gameStateObj is null! Deactivating...");
14 gameState = null;
15 return new object[] { gameStateObj, gameState, false };
16 }
17 }
01 public static object[] LoadStartingSetAndLocalPlayerInfo(string className, out GameState gameState, out PlayerInfo pi, out int playerIndex, out PlayerState p, GameObject g, bool inParent) {
02 GameObject gameStateObj = GameObject.Find(Utilities.NAME_GAME_STATE_OBJ);
03 if (gameStateObj != null) {
04 gameState = gameStateObj.GetComponent<GameState>();
05 if (gameState != null) {
06 gameState.PrepGame();
07 } else {
08 Utilities.wrForce(className + ": gameState is null! Deactivating...");
09 pi = null;
10 playerIndex = -1;
11 p = null;
12 return new object[] { gameStateObj, gameState, false, pi, playerIndex, p };
13 }
14 } else {
15 Utilities.wrForce(className + ": gameStateObj is null! Deactivating...");
16 gameState = null;
17 pi = null;
18 playerIndex = -1;
19 p = null;
20 return new object[] { gameStateObj, gameState, false, pi, playerIndex, p };
21 }
22
23 if (g != null) {
24 if (inParent) {
25 pi = g.GetComponentInParent<PlayerInfo>();
26 } else {
27 pi = g.GetComponent<PlayerInfo>();
28 }
29 if (pi != null) {
30 playerIndex = pi.playerIndex;
31 p = gameState.GetPlayer(playerIndex);
32 if (p != null) {
33 return new object[] { gameStateObj, gameState, true, pi, playerIndex, p };
34 } else {
35 Utilities.wrForce(className + ": p is null! Deactivating...");
36 p = null;
37 return new object[] { gameStateObj, gameState, false, pi, playerIndex, p };
38 }
39 } else {
40 Utilities.wrForce(className + ": pi is null! Deactivating...");
41 pi = null;
42 playerIndex = -1;
43 p = null;
44 return new object[] { gameStateObj, gameState, false, pi, playerIndex, p };
45 }
46 } else {
47 Utilities.wrForce(className + ": g is null! Deactivating...");
48 pi = null;
49 playerIndex = -1;
50 p = null;
51 return new object[] { gameStateObj, gameState, false, pi, playerIndex, p };
52 }
53 }
01 public static object[] LoadPlayerInfo(string className, out PlayerInfo pi, out int playerIndex, out PlayerState p, GameObject g, GameState gameState, bool inParent, bool verbose = false) {
02 if (g != null && gameState != null) {
03 if (inParent) {
04 pi = g.GetComponentInParent<PlayerInfo>();
05 } else {
06 pi = g.GetComponent<PlayerInfo>();
07 }
08 if (pi != null) {
09 playerIndex = pi.playerIndex;
10
11 p = gameState.GetPlayer(playerIndex);
12 if (p != null) {
13 return new object[] { pi, playerIndex, true, p };
14 } else {
15 if (verbose) {
16 Utilities.wrForce(className + ": p is null! Deactivating...");
17 }
18 p = null;
19 return new object[] { pi, playerIndex, false, p };
20 }
21 } else {
22 if (verbose) {
23 Utilities.wrForce(className + ": pi is null! Deactivating...");
24 }
25 pi = null;
26 playerIndex = -1;
27 p = null;
28 return new object[] { pi, playerIndex, false, p };
29 }
30 } else {
31 if (verbose) {
32 Utilities.wrForce(className + ": g is null! Deactivating...");
33 }
34 pi = null;
35 playerIndex = -1;
36 p = null;
37 return new object[] { pi, playerIndex, false, p };
38 }
39 }
Listing 6-30Utilities Static Class Methods 3
前面列出的三个方法是非常重要的实用方法,在游戏代码库的不同地方使用。第一个条目LoadStartingSet
由BaseScript
类的Prep
方法使用。这意味着它被用在许多扩展了BaseScript
类的类中。该方法的主要目的是定位GameState
游戏对象并找到相关的GameState
脚本组件。该方法返回一个结果数组,数组中的最后一项是指示操作成功的布尔值。
列出的下一个方法LoadStartingSetAndLocalPlayerInfo
,与我们刚刚审查的方法类似,只是它更进一步,在加载游戏状态后,它将寻找本地PlayerInfo
组件。该组件用于指示某个游戏对象属于哪个玩家。然后,该方法尝试加载与该玩家相关联的玩家状态数据。如果GameState
已经正确加载,第 24–38 行的代码定位相关的PlayerState
对象。看看这两种方法,确保你很好地理解了它们。如果遇到问题,这两种方法中的大部分代码都可以安全退出。
该组中列出的最后一种方法是LoadPlayerInfo
方法。该方法用于加载与游戏对象相关的玩家状态数据。请注意,该代码与前面介绍的方法的第二部分非常相似。本质上,这个过程使用来自一个PlayerInfo
对象实例的播放器索引来定位播放器的PlayerState
实例,该实例存储在GameState
类中。查找 Unity 组件时遇到的任何问题都会导致指示错误的响应。看看这些方法中使用的参数。特别注意使用out
参数来更新某些对象引用,而不依赖于方法的返回对象。接下来,让我们看看这个类的运行情况。
演示:实用程序
因为Utilities
类是普通的 C# 类,所以我们不会使用演示场景。相反,我们将回顾一些相关的代码,这些代码使用了我们刚刚回顾的一些方法。对于这个特定的演示,我们将看一下显示了运行中的Utilities
类的代码片段。下面的代码来自BaseScript
的 Start 方法。
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 6-31Utilities Demonstration 1
列出的第一种方法是Prep
方法。这个方法被大多数扩展了BaseScript
类的类调用。请特别注意第 3 行的方法调用。这里发生了三件事。用调用LoadStartingSet
方法返回的数组索引 2 中的值更新scriptActive
字段。另外,请注意,gameState
方法参数是用关键字out
修饰的。这意味着本地对象可以通过方法调用来更新。这样,我们可以更新许多重要的类字段,并引用操作的布尔结果,返回数组索引 2。不算太寒酸。
前面列出的第二种方法PrepPlayerInfo
与我们刚刚讨论的方法非常相似。它使用了LoadStartingSetAndLocalPlayerInfo
类的准备方法。请注意,gameState
、playerInfo
、playerIndex
和playerState
参数都是 out 方法参数,这意味着它们都可以通过方法调用来更新。这让我们可以在最小的空间内完成大量的配置步骤。我认为它很强大,很有效,但是看看代码有多不清晰。一些更长但更具描述性的东西会更合我的口味。在继续之前,请确保您理解了本课程。它非常重要,贯穿于整个代码库。接下来,我们将看看飞艇摄像机背后的代码。
班级点评:CameraFollowXz
CameraFollowXz
类是一个MonoBehaviour
类,设计用来跟随当前玩家的汽车在赛道上的移动。该课程相当简洁,但也有足够的复杂性,我们应该使用下列组中列出的标准课程复习步骤:
-
静态/常量/只读类成员
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
这个类没有任何枚举或支持方法,所以我们将省略这些部分。让我们从查看类的静态成员开始审查过程。
静态/常量/只读类成员:CameraFollowXz
CameraFollowXz
类有几个静态字段供我们讨论。我们开始吧。
public static readonly float BLIMP_FLY_HEIGHT = 110f;
public static readonly float BLIMP_FLY_MIN = 30f;
Listing 6-32CameraFollowXz Static/Constants/Read-Only Class Members 1
前面列出的第一个静态字段是BLIMP_FLY_HEIGHT
静态字段。该字段表示飞艇摄像机跟随悬停赛车时的高度。高度根据悬停赛车的速度进行调整。这意味着相机在较慢的速度下较低,在较快的速度下较高。下一个字段是BLIMP_FLY_MIN
字段。这个字段设置摄像机跟随玩家的最小高度。它包装了类的静态字段。接下来,让我们回顾一下该课程的剩余字段。
类字段:CameraFollowXz
这个类有三个字段供我们查看,如下所示。
private float x = 0.0f;
private float y = 0.0f;
private float z = 0.0f;
Listing 6-33CameraFollowXz Class Fields 1
该组类字段匹配一个Vector3
对象的组件值。这些字段用于根据当前玩家的位置和速度计算飞艇摄像机的位置。
相关的方法大纲/类头:CameraFollowXz
CameraFollowXz
类有下面的方法大纲。
//Main
void Start();
void Update();
Listing 6-34CameraFollowXz Pertinent Method Outline/Class Headers 1
随后,我将列出该类的导入语句和声明。
using UnityEngine;
public class CameraFollowXz : BaseScript {}
Listing 6-35CameraFollowXz Pertinent Method Outline/Class Headers 2
在下一个复习部分,我们将看看这个类的主要方法。
主要方法详细信息:CameraFollowXz
下面一组中列出的两个主要方法是类’Start
和Update
方法。让我们看看。
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 void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (gameState != null) {
07 p = gameState.GetCurrentPlayer();
08 if (p != null) {
09 if (p.player != null && p.player.transform != null) {
10 x = p.player.transform.position.x;
11 y = (p.player.transform.position.y + ((BLIMP_FLY_HEIGHT * p.speedPrct) + BLIMP_FLY_MIN));
12 z = p.player.transform.position.z;
13 transform.position = new Vector3(x, y, z);
14 }
15 }
16 }
17 }
Listing 6-36CameraFollowXz Main Method Details 1
Start
方法作为MonoBehaviour
生命周期的一部分被调用。它在组件的初始化阶段被调用。该方法的第 2 行调用基类的’Prep
’方法。此方法用于配置类并注册结果。在第 3 行检查类配置是否成功。这是扩展了BaseScript
类并且不需要玩家状态数据的类的标准初始化。
列出的第二个方法是类’Update
方法。如果组件处于活动状态,Unity 游戏引擎会在每个游戏帧中调用此方法。在第 2-4 行,我们有标准的安全检查。如果没有正确配置该类,那么该方法不做任何工作就退出。如果gameState
字段不为空,那么我们在第 7 行存储一个对当前玩家状态的引用。如果定义了当前玩家状态,第 8 行,并且定义了玩家的游戏对象和变换,那么我们基于当前玩家的位置行 10-13 设置x
、y
和z
类字段。
请注意,垂直轴y
是基于当前玩家的y
坐标加上由悬停赛车手的当前speedPrct
确定的值。这将导致当当前玩家的悬停赛车移动得更快时,飞艇摄像机以更高的高度跟随该赛车。然而,相机不会低于最低高度。在第 13 行,飞艇摄像机的位置被更新。这一行实际上会设置CameraFollowXz
脚本所附着的游戏对象的位置。我们只把它附加到一个Camera
物体上,但是它也可以和其他游戏物体一起工作。在下一节中,我们将看一下CameraFollowXz
类的演示。
演示:CameraFollowXz
CameraFollowXz
类的演示是一个名为“DemoCameraFollowXz”的场景。该场景位于“项目”面板的“场景”文件夹中。在运行场景之前,让我解释一下它是如何工作的。这个演示实际上非常类似于我们之前检查的“DemoCollideScript”场景。这里的主要区别是,这个场景有全套的菜单屏幕和游戏中的 HUD。
因此,场景将以一个可见的菜单屏幕开始。为了开始场景,你需要点击“轨迹 1”或“轨迹 2”按钮。你也可以点击游戏画面左下角的重启按钮。你会注意到,当这个演示场景开始时,我们可以看到一个完整的游戏 HUD,包括一个飞艇摄像机。飞艇摄像机将跟随当前玩家在棋盘上移动,移动的高度取决于悬浮赛车的速度。开车兜一圈,看看飞艇摄像机是如何工作的。我们将通过查看WaypointCompare
类来结束这一章。
课堂复习:航点比较
WaypointCompare
类用于对从当前轨迹加载的路点标记数组进行排序。该类相当简短和直接,所以我们将直接列出该类,如下所示,并对其进行回顾。让我们来看看一些代码!
01 public class WaypointCompare : IComparer {
02 private WaypointCheck obj1 = null;
03 private WaypointCheck obj2 = null;
04
05 public int Compare(object o1, object o2) {
06 obj1 = (WaypointCheck)o1;
07 obj2 = (WaypointCheck)o2;
08 if (obj1.waypointIndex > obj2.waypointIndex) {
09 return 1;
10 } else if (obj1.waypointIndex < obj2.waypointIndex) {
11 return -1;
12 } else {
13 return 0;
14 }
15 }
16 }
Listing 6-37CameraFollowXz Class Review 1
注意,WaypointCompare
类没有扩展 Unity 的MonoBehaviour
或我们的BaseScript
类。相反,因为用于类比较,WaypointCompare
类扩展了IComparer
接口。在第 2–3 行,列出了类别字段obj1
和obj2
。它们用于保存转换为适当类类型的比较对象。在第 5 行,定义了比较方法。它将两个通用对象作为比较的参数,并由数组排序方法自动调用。
注意,在第 6–7 行,方法参数被转换为WaypointCheck
对象实例。请注意,私有类字段被用作局部变量来避开垃圾收集器。第 8–14 行确定了两个WaypointCheck
对象之间的关系。与WaypointCheck
的waypointIndex
进行比较。根据它们之间的关系,指示顺序的方法返回 1、-1 或 0。让我们来看看这个类的一个演示。
演示:航路点比较
因为WaypointCompare
类不是MonoBehaviour
类的扩展,我们不会用场景来演示它。相反,我们将看看运行中的类。下面的代码片段来自于GameState
类的FindWaypoints
方法。
1 public WaypointCompare wpc = new WaypointCompare();
...
2 object[] ar = row.ToArray();
3 System.Array.Sort(ar, wpc);
Listing 6-38CameraFollowXz Demonstration 1
这个例子很简单。第 1 行显示了第 3 行中使用的wpc
类字段的声明。它接受一个对象数组,这些对象是从赛道的游戏对象(第 2 行)加载的WaypointCheck
实例。接下来,调用数组排序方法,第 3 行,使用路点数组和一个WaypointCompare
类实例作为参数。
第二章结论
在这一章中,我们花了一点时间来回顾一下 Hover Racers 游戏中的“其他”职业。这给我们介绍了一些有趣的话题。我将列出一些潜在的项目如下。花点时间去思考它们。
-
清理脚本:确保游戏对象在脱离棋盘或轨道时得到管理。此外,它们可以用于水障碍和其他赛道功能,以重置球员。
-
以编程方式调整声音:引擎声音控制用于增加游戏的真实感,并且是添加到工具箱的有用效果。
-
C# 支持类:使用普通的 C# 类,以快速、易用的实用程序的形式为游戏添加功能。
-
集中控制代码:
Utilities
类充当游戏设置和重要的核心配置功能的集中点。 -
相机技巧:我们用一个简单的
MonoBehaviour
连接到一个 UnityCamera
对象上,创建了一个飞艇相机效果。
在本章中,Unity 引擎有许多有用的应用。更上一层楼,创造一些你自己的。在下一章,我们将看看输入类。你应该意识到不同的组件,主要是脚本组件(MonoBehaviour
s),可以通过它们的Update
方法改变游戏对象——每一帧。请注意,对GameObject
或脚本组件的更改可以在代码的许多不同位置进行。我发现集中式设计模型,比如Utilities
类,对我来说更好,但是,也许一个更大的团队会从分布式模型中受益更多。你必须找到最适合你情况的方法。
七、输入类
在这一章中,我们将详细回顾用于驱动游戏控件的输入类。我应该提到的是,Hover Racers 支持键盘、鼠标、控制器和触摸输入。我们在这一章要看的类如下:
-
字符马达
-
FPS 输入控制器
-
MouseLookNew
-
GameState(仅触摸输入片段)
CharacterMotor
类可能是列出的类中最复杂的。它包含以下子类:
-
性格运动
-
字符电机滑动
-
角色机动跳跃
这些类用于管理悬停赛车运动的不同组件。剩下的类FPSInputController
和MouseLookNew
不太复杂,没有定义任何子类。让我们从CharacterMotor
的子类开始回顾。我们在本章中学习的类可以在下面列出的两个位置中找到:
-
\标准资源\角色控制器\源\脚本\
-
\标准资产\mmg_scripts\
如果你在寻找一个特定的类时遇到了麻烦,只需在 Unity 编辑器中使用“项目”面板的搜索栏来搜索它,或者在 Visual Studio 的“解决方案资源管理器”面板中寻找它。
课程回顾:角色运动
CharacterMotorMovement
类用于控制角色马达以移动一个对象。这个类很简洁,所以我们将放弃更冗长的类回顾过程,只在这里列出这个类。
01 [System.Serializable]
02 public class CharacterMotorMovement {
03 public float maxForwardSpeed = 3.0f;
04 public float maxSidewaysSpeed = 10.0f;
05 public float maxBackwardsSpeed = 10.0f;
06 public AnimationCurve slopeSpeedMultiplier = new AnimationCurve(new Keyframe(-90, 1), new Keyframe(0, 1), new Keyframe(90, 0.70f));
07
08 public float maxGroundAcceleration = 30.0f;
09 public float maxAirAcceleration = 20.0f;
10 public float gravity = 10.0f;
11 public float maxFallSpeed = 22.0f;
12
13 [System.NonSerialized]
14 public CollisionFlags collisionFlags;
15
16 [System.NonSerialized]
17 public Vector3 velocity;
18
19 [System.NonSerialized]
20 public Vector3 frameVelocity = Vector3.zero;
21
22 [System.NonSerialized]
23 public Vector3 hitPoint = Vector3.zero;
24
25 [System.NonSerialized]
26 public Vector3 lastHitPoint = new Vector3(Mathf.Infinity, 0, 0);
27 }
Listing 7-1CharacterMotorMovement Class Review 1
在我深入研究代码评审之前,我想花点时间提一下前面列出的类中使用的一些代码属性。属性用来明确地告诉 Unity 某些类字段的值应该被序列化并用来驱动“Inspector”面板值。硬币反面是System.NonSerialized
属性。此属性用于从序列化和显示过程中对通常会被序列化的字段进行转义。简而言之,“检查器”面板不会显示具有非序列化属性的字段,即使它们通常会显示。
在课堂评论上。CharactorMotor
类由一系列公共类字段组成。前三个字段用于设置影响悬停赛车运动的向前、向后和侧向速度的最大速度。请记住,作为游戏初始化过程的一部分,这些值在代码的其他部分进行了细化。字段是 Unity 的AnimationCurve
类的一个实例。它用于修改悬停赛车在地面遇到斜坡或斜面时的速度。
maxGroundAcceleration
类字段用于控制汽车的水平加速度。下一个字段,maxAirAcceleration
,以同样的方式工作,除了它在悬停赛车离开地面时应用。gravity
场被用来施加一个向下的力,使悬停赛车回到地面。maxFallSpeed
场控制汽车在重力作用下落回地面的速度。接下来,collisionFlag
字段用于报告悬停赛车的碰撞信息。
velocity
向量跟踪悬停赛车的当前速度。frameVelocity
字段类似,但在内部使用,大概是为了跟踪每一帧游戏的速度。最后列出的两个字段,hitPoint
和lastHitPoint
,用于在CharacterMotor
类中跟踪内部速度控制的运动过程中的碰撞点。接下来我们来看看CharacterMotorJumping
类。
课程回顾:角色摩托跳跃
CharacterMotorJumping
用于帮助管理跳跃过程中的角色移动。我在这里枚举一下班级,供大家复习。
01 [System.Serializable]
02 public class CharacterMotorJumping {
03 public bool enabled = true;
04 public float baseHeight = 1.0f;
05 public float extraHeight = 4.1f;
06 public float perpAmount = 0.0f;
07
08 public float steepPerpAmount = 0.5f;
09
10 [System.NonSerialized]
11 public bool jumping = false;
12
13 [System.NonSerialized]
14 public bool holdingJumpButton = false;
15
16 [System.NonSerialized]
17 public float lastStartTime = 0.0f;
18
19 [System.NonSerialized]
20 public float lastButtonDownTime = -100.0f;
21
22 [System.NonSerialized]
23 public Vector3 jumpDir = Vector3.up;
24 }
Listing 7-2CharacterMotorJumping Class Review 1
需要注意的是,我们正在复习的类并没有被游戏主动使用。我想覆盖它,所以你可以在你的下一个游戏中添加不同种类的跳跃。第一个字段enabled
表示跳转功能激活。以下两个类字段baseHeight
和extraHeight
设置跳跃基础高度和额外跳跃高度值。随后,在计算中使用perpAmount
和steepPerpAmount
来确定角色如何在斜坡和陡坡上跳跃。跳跃标志用于指示字符是否被标记为跳跃。
游戏中的其他类跟踪玩家状态并记录悬停赛车是否跳跃。在我们的例子中,与跳跃标记的碰撞决定了悬停赛车是否在跳跃。因为参赛者不会在按下按钮时跳跃,这很有趣,因为游戏没有使用默认的跳跃修改代码。因此,这个类可能无法准确描述角色的跳跃状态。下一个字段holdingJumpButton
是一个布尔值,表示跳转按钮被按下。最后但同样重要的是,jumpDir
字段指示跳转的方向。下一堂课,我们将看看模型的悬停赛车的滑动或侧向运动。
课程回顾:角色摩托滑行
CharacterMotorSliding
类类似于我们刚刚讨论过的子类。让我们来看看。
1 [System.Serializable]
2 public class CharacterMotorSliding {
3 public bool enabled = true;
4 public float slidingSpeed = 15.0f;
5 public float sidewaysControl = 1.0f;
6 public float speedControl = 0.4f;
7 }
Listing 7-3CharacterMotorSliding Class Review 1
正如我前面提到的,这个类用于控制一个悬停赛车的左右运动。第一个字段指示是否启用滑动。第二个条目slidingSpeed
表示滑动的速度,而sidewaysControl
字段用于跟踪玩家对横向移动的控制程度。最后,speedControl
域用于控制玩家可以在多大程度上影响悬浮赛车的滑行速度。子类评审到此结束。接下来,我们将仔细看看CharacterMotor
类。
课程回顾:角色马达
CharacterMotor
类负责悬停赛车的基本动作。这个类管理的跳跃和重力实际上被游戏禁用了。启用滑动和水平移动。这个类的顶部列出了一个代码属性:
[RequireComponent(typeof(CharacterController))]
这个代码属性表明这个MonoBehaviour
组件需要存在一个附加到同一个GameObject
的CharacterController
组件。这是通过 Unity 编辑器处理的。如果角色 motor 似乎知道 hover racer 的碰撞状态,那是因为它从CharacterController
类中获得了该信息,我们稍后将对此进行介绍。我还应该提到的是FPSInputController
脚本驱动了CharacterContoller
。这样,输入向量由FPSInputController
更新。
游戏中的输入和移动设置有点复杂,所以我们在这里概述一下。
图 7-1
Hover Racers 输入图
不算太坏。如果我们从头开始编写自己的控制器,这看起来可能会有点不同。但在这种情况下,我们使用的是 Unity 的一些传统控制器类,所以这就是我们所得到的。话虽如此,我们还是来略述一下我们的课堂复习吧。
-
类别字段
-
相关的方法大纲/类头
-
支持方法详细信息
-
主要方法详细信息
-
示范
没有静态类成员或枚举可言,所以我们将跳过审查过程中的这些部分。我们有一些工作要做;我们开始吧。
类别字段:角色马达
第一个CharacterMotor
类字段如下。
public bool aiOn = false;
bool canControl = true;
bool useFixedUpdate = true;
Listing 7-4CharacterMotor Class Fields 1
aiOn
字段是一个布尔标志,指示当前悬停赛车的 AI 控制是否有效。canControl
字段是另一个布尔标志,用于指示玩家是否可以控制悬停赛车。集合中的最后一个条目是useFixedUpdate
布尔。如果此字段设置为 true,该类将使用固定更新调用。固定更新呼叫仅仅是固定速率的更新呼叫。这不同于我们目前所见的标准Update
方法。标准的Update
方法在每个游戏帧被调用一次。然而,由于游戏帧速率的波动,其速率是可变的。对于重要的物理计算,我们使用FixedUpdate
回调方法,因为它更稳定可靠。
public Vector3 inputMoveDirection = Vector3.zero;
public bool inputJump = false;
public CharacterMotorMovement movement = new CharacterMotorMovement();
public CharacterMotorJumping jumping = new CharacterMotorJumping();
public CharacterMotorSliding sliding = new CharacterMotorSliding();
public bool grounded = true;
Listing 7-5CharacterMotor Class Fields 2
inputMoveDirection
矢量代表由FPSInputController
设定的运动方向。inputJump
字段是一个布尔标志,用于指示跳转按钮已被按下。通常这个输入会来自于FPSInputController
,但是我们已经改变了那个脚本的默认功能,使用了一个更适合赛车游戏的输入映射,禁用了基于按钮按压–
的跳跃。在inputJump
字段之后是用于模拟角色运动的三个字段。我们刚刚回顾了它们,所以我假设你对它们的内容有一个坚实的概念。最后,我们有接地字段,用来表示角色是接地的。
public Vector3 groundNormal = Vector3.zero;
private Vector3 lastGroundNormal = Vector3.zero;
private Transform tr;
private CharacterController controller;
Listing 7-6CharacterMotor Class Fields 3
我们要查看的最后一组类字段从groundNormal
向量开始。该字段是一个与地面垂直相交的Vector3
实例,用于确定地面坡度等。该字段之后是lastGroundNormal
字段,用于跟踪之前的地面法向量。Transform
、tr
用于在计算过程中保存对 Unity Transform
对象的引用。变换对象用于定位和缩放 Unity GameObjects
。
CharacterController
字段controller
是对所需的CharacterController
组件的引用。最终是控制器被这个类移动,它是碰撞数据的来源,指示角色被固定或遇到斜坡或陡坡。最后两个字段是我们多次遇到的标准类初始化步骤的一部分。接下来,我们将检查该类的相关方法。
相关的方法大纲/类头:字符马达
CharacterMotor
类有许多方法供我们研究,如下所列。
//Main Methods
void Awake();
private void UpdateFunction();
void FixedUpdate();
void Update();
void OnControllerColliderHit(ControllerColliderHit hit);
//Support Methods
private Vector3 ApplyInputVelocityChange(Vector3 velocity);
private Vector3 ApplyGravityAndJumping(Vector3 velocity);
private Vector3 GetDesiredHorizontalVelocity();
private Vector3 AdjustGroundVelocityToNormal(Vector3 hVelocity, Vector3 groundNormal);
private bool IsGroundedTest();
float GetMaxAcceleration(bool grounded);
float CalculateJumpVerticalSpeed(float targetJumpHeight);
bool TooSteep();
float MaxSpeedInDirection(Vector3 desiredMovementDirection);
Listing 7-7CharacterMotor Pertinent Method Outline/Class Headers 1
该类的导入语句和头文件如下。
using UnityEngine;
//Require a character controller to be attached to the same game object
[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("Character/Character Motor")]
public class CharacterMotor : BaseScript {}
Listing 7-8CharacterMotor Pertinent Method Outline/Class Headers 2
注意我们之前讨论过的RequireComponent
条目。该条目下面是一个 Unity 特有的代码属性,用于在查看CharacterMotor
组件所连接的GameObject
时定制“检查器”面板。在这种情况下,它只是添加一个列出了名称的菜单标题。在下一节,我们将看看类的支持方法。还要注意,这个类扩展了BaseScript
类,因此是一个MonoBehaviour
实例,具有BaseScript
类提供的所有默认功能。
支持方法详细信息:字符马达
类的支持方法相当复杂。有许多向量运算需要从代码和数学两方面进行大量解释。我不准备在这篇文章中深入讨论这些材料。我们将从一个更高的层次来回顾这些方法,并尝试关注这些方法实际上做了什么,而不是数学是如何工作的。我邀请你分解任何复杂的计算,自己对所涉及的数学做出决定。
我们要回顾的第一个方法是ArrayInputVelocityChange
方法。这个方法采用一个Vector3
速度参数,它应该代表输入驱动的速度向量。该方法考虑了地面坡度、滑动方向和速度。它强制执行最大速度变化,并检查角色是否在跳跃。现在,把你的注意力放在ApplyGravityAndJumping
方法上,快速浏览一下代码。ApplyGravityAndJumping
方法负责处理关于跳跃、地面坡度、最大下落速度和重力的垂直轴调整。请记住,这种方法并没有在游戏中使用,而是设计用于基于按钮按下–
的跳跃。悬停赛车游戏,因为是,使用基于碰撞的跳跃标记,而不是按钮按下跳跃控制。
GetDesiredHorizontalVelocity
方法负责获取inputMoveDirection
并确定角色在给定方向的最大速度。这种方法将基于斜坡速度乘数曲线修改汽车在斜坡上的最大速度,这是我们之前讨论过的CharacterMotorMovement
类字段。下面的方法AdjustGroundVelocityToNormal
用于通过一系列叉积矢量运算来调整地面速度矢量。这为基于输入的地面运动建立了一个清晰的、正确定向的矢量。
这个条目后面是IsGroundedTest
方法。该方法返回一个布尔值,指示角色是否接触地面。随后,GetMaxAcceleration
方法根据汽车的状态返回运动类的最大地面或空中加速度。根据targetJumpHeight
和当前重力计算出CalculateJumpVerticalSpeed
方法。TooSteep
方法使用groundNormal
向量和控制器字段的slopeLimit
来确定地面坡度对赛车来说是否太陡。
MaxSpeedInDirection
是一种用于确定角色在给定方向上移动的最大速度的方法,考虑了横向和向后的速度。同样,我们没有像通常那样详细地讨论支持方法,以避免冗长的数学讨论。在审查代码时,请关注所列出的支持方法的用法。这让我们得出了CharacterMotor
类的支持方法的结论。在下一个复习部分,我们将看看这个类的主要方法。
主要方法详细信息:字符马达
我们复习的第一个主要方法是Awake
法。Awake
方法是一种类似于Start
方法的 Unity 组件生命周期回调方法。Awake
和Start
方法的区别在于Start
方法仅在脚本被启用并且所有对象都被初始化之后才被调用,这样你就可以在加载脚本实例时调用Awake
的同时连接到它们。这两个函数都是在第一个Update
方法之前调用的,两者没有性能差异。
01 void Awake() {
02 controller = GetComponent<CharacterController>();
03 tr = transform;
04 base.PrepPlayerInfo(this.GetType().Name);
05 if (BaseScript.IsActive(scriptName) == false) {
06 Utilities.wrForce(scriptName + ": Is Deactivating...");
07 return;
08 } else {
09 aiOn = p.aiOn;
10 }
11 }
Listing 7-9CharacterMotor Main Method Details 1
前两行代码根据附加的、必需的CharacterController
组件和当前游戏对象的转换来设置controller
和tr
类字段。第 4 行的代码通过加载GameState
和PlayerState
数据来准备类。第 5–10 行的代码片段负责检查类的初始化是否成功。如果没有,该类打印一些日志并返回,第 6 行和第 7 行。如果初始化成功,则aiOn
字段被更新以匹配第 9 行上当前玩家的状态。下一个要回顾的主要方法是UpdateFunction
方法。
这个方法处理类的更新,并根据useFixedUpdate
字段的值由FixedUpdate
或Update
方法调用。通过这种方式,我们将实际的更新代码抽象为一个级别,这样它就可以被两个不同的回调方法访问。这减少了冗余。让我们跳到一些代码中。
01 private void UpdateFunction() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 // We copy the actual velocity into a temporary variable that we can manipulate.
07 Vector3 velocity = movement.velocity;
08
09 // Update velocity based on input
10 velocity = ApplyInputVelocityChange(velocity);
11
12 // Apply gravity and jumping force
13 velocity = ApplyGravityAndJumping(velocity);
14
15 // Save lastPosition for velocity calculation.
16 Vector3 lastPosition = tr.position;
17
18 // We always want the movement to be framerate independent. Multiplying by Time.deltaTime does this.
19 Vector3 currentMovementOffset = velocity * Time.deltaTime;
20
21 // Find out how much we need to push towards the ground to avoid losing grounding
22 // when walking down a step or over a sharp change in slope.
23 float pushDownOffset = Mathf.Max(controller.stepOffset, new Vector3(currentMovementOffset.x, 0, currentMovementOffset.z).magnitude);
24 if (grounded) {
25 currentMovementOffset -= pushDownOffset * Vector3.up;
26 }
27
28 // Reset variables that will be set by collision function
29 //movingPlatform.hitPlatform = null;
30 groundNormal = Vector3.zero;
31
32 // Move our character!
33 movement.collisionFlags = controller.Move(currentMovementOffset);
34
35 movement.lastHitPoint = movement.hitPoint;
36 lastGroundNormal = groundNormal;
37
38 // Calculate the velocity based on the current and previous position.
39 // This means our velocity will only be the amount the character actually moved as a result of collisions.
40 Vector3 oldHVelocity = new Vector3(velocity.x, 0, velocity.z);
41 movement.velocity = (tr.position - lastPosition) / Time.deltaTime;
42 Vector3 newHVelocity = new Vector3(movement.velocity.x, 0, movement.velocity.z);
43
44 // The CharacterController can be moved in unwanted directions when colliding with things.
45 // We want to prevent this from influencing the recorded velocity.
46 if (oldHVelocity == Vector3.zero) {
47 movement.velocity = new Vector3(0, movement.velocity.y, 0);
48 } else {
49 float projectedNewVelocity = Vector3.Dot(newHVelocity, oldHVelocity) / oldHVelocity.sqrMagnitude;
50 movement.velocity = oldHVelocity * Mathf.Clamp01(projectedNewVelocity) + movement.velocity.y * Vector3.up;
51 }
52
53 if (movement.velocity.y < velocity.y - 0.001) {
54 if (movement.velocity.y < 0) {
55 // Something is forcing the CharacterController down faster than it should.
56 // Ignore this
57 movement.velocity.y = velocity.y;
58 } else {
59 // The upwards movement of the CharacterController has been blocked.
60 // This is treated like a ceiling collision - stop further jumping here.
61 jumping.holdingJumpButton = false;
62 }
63 }
64
65 // We were grounded but just lost grounding
66 if (grounded && !IsGroundedTest()) {
67 grounded = false;
68
69 // We pushed the character down to ensure it would stay on the ground if there was any.
70 // But there wasn't so now we cancel the downwards offset to make the fall smoother.
71 tr.position += pushDownOffset * Vector3.up;
72 }
73 // We were not grounded but just landed on something
74 else if (!grounded && IsGroundedTest()) {
75 grounded = true;
76 }
77 }
Listing 7-10CharacterMotor Main Method Details 2
如果该类被标记为非活动的,则该方法被转义而不做任何工作,第 2–4 行。在第 2 行,我们将运动场的速度实例CharacterMotorMovement
复制到局部变量velocity
。运动向量可以通过任意数量的MonoBehaviour
进行调整。同样,它在方法开始时被复制到一个局部变量中。在这种情况下,可以认为速度向量是由玩家输入决定的。在第 10 行,我们通过ApplyInputVelocityChange
方法传递了velocity
向量。此方法调整向量的分量值,以说明方向、滑动、跳跃和地面坡度。
随后,通过ApplyGravityAndJumping
方法进一步调整velocity
向量。虽然游戏不使用这种方法,但如果启用,它会对velocity
向量应用跳跃、重力和陡坡探测。请记住,我们从玩家想要的输入开始改变这个向量,然后根据地形、滑动、跳跃和重力进行调整。最终,这个向量将被应用于悬停赛车,以在游戏运行时调整其位置。
回想一下,我们没有使用这个类的跳跃能力,而是选择使用基于碰撞检测的跳跃。接下来,在第 16 行设置lastPosition
变量。在第 19 行,通过乘以Time.deltaTime
的值,速度被转换为独立于帧速率的值。当您将速度向量乘以Time.deltaTime
的值时,您实际上是将向量分量的值更改为每秒的速率。向下倾斜的地形在第 23-26 行通过将悬停车手推向地面来处理。
在第 30 行,我们将groundNormal
类字段重置为零向量,为处理新的碰撞信息做准备。通过应用独立于帧的运动向量,悬停赛车在线 33 上移动。碰撞结果存储在运动对象的collisionFlags
字段中。随后,lastHitPoint
和lastGroundNormal
字段用当前的一组值更新。
旧的和新的水平速度是在第 40-42 行计算的。旧的速度基于velocity
字段的值。新的速度是基于悬停参赛者的当前位置与其先前位置(线 41)相比而计算的。注意,我们除以Time.deltaTime
的值,将分量值从独立于帧速率扩展到全值。根据更新的movement
速度矢量的 X 和 Z 分量值在第 42 行设置newHVelocity
。
第 46–63 行的代码负责考虑由于物体碰撞导致的不必要的运动方向,包括天花板检测,这由类的 jump 实现使用,但不是由整个游戏使用。最后一段代码,第 65–76 行,用于检测地面状态的变化,包括失去地面、起飞、获得地面、着陆。接下来我们将看看剩下的主要方法。
01 void FixedUpdate() {
02 if (useFixedUpdate) {
03 UpdateFunction();
04 }
05 }
01 void Update() {
02 if (!useFixedUpdate) {
03 UpdateFunction();
04 }
05 }
01 void OnControllerColliderHit(ControllerColliderHit hit) {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (hit.normal.y > 0 && hit.normal.y > groundNormal.y && hit.moveDirection.y < 0) {
07 if ((hit.point - movement.lastHitPoint).sqrMagnitude > 0.001 || lastGroundNormal == Vector3.zero) {
08 groundNormal = hit.normal;
09 } else {
10 groundNormal = lastGroundNormal;
11 }
12
13 movement.hitPoint = hit.point;
14 movement.frameVelocity = Vector3.zero;
15 }
16 }
Listing 7-11CharacterMotor Main Method Details 3
这个集合中列出的前两个方法是 Unity 回调方法FixedUpdate
和Update
。根据useFixedUpdate
字段的值,使用一种或另一种方法。我们要复习的最后一个方法是OnControllerColliderHit
方法。该方法用于确定groundNormal Vector3
字段的值。注意,移动对象的hitPoint
和frameVelocity
字段是在方法结束时设置的。
这门课中的许多计算都很复杂,难以想象,而且非常可怕。不要害怕!您可以通过添加调试和运行游戏来接近任何计算或字段值,以查看这些值如何对应于悬停赛车的实际运动。使用这些信息来确定如何使用该字段以及它包含什么类型的数据。接下来,我们将详细说明如何演示CharacterMotor
类。
演示:角色马达
演示CharacterMotor
类的最好方法是玩我们到目前为止看过的任何演示场景或实际游戏,场景“Main13”或“Main14”。当游戏在“游戏”面板中运行时,您可以查看“层级”面板并展开StartingSet
条目。选择HoverCar0
子条目。在“检查器”面板中找到“字符马达”条目并展开它。当汽车在赛道上行驶时,观察字段值的变化。
执行这个演示的一个非常好的方法是运行实际的游戏,让计算机模式运行,同时在“检查器”面板中仔细查看“角色运动”组件的值。试着看看当人工智能玩家在赛道上比赛时,某些值是如何变化的。我们要看的下一个类是FPSInputController
类。
课程回顾:FPSInputController
FPSInputController
类负责响应用户输入。基于输入,CharacterMotor
中的某些值被设置,这又导致CharacterController
移动角色。我们将使用以下复习步骤来涵盖本课程:
-
类别字段
-
相关的方法大纲/类头
-
主要方法详细信息
-
示范
我们要看的第一个复习部分是类字段。记住这个类的位置在下面的文件夹中:" \标准资产\角色控制器\资源\脚本"。
类字段:FPSInputController
FPSInputController
类有许多字段,帮助它管理在控制CharacterMotor
时要考虑的用户输入。
private CharacterMotor motor;
public bool aiOn = false;
public Vector3 directionVector = Vector3.zero;
public float directionLength = 0.0f;
public Vector3 inputMoveDirection = Vector3.zero;
public float touchSpeed = 0.0f;
public float touchSpeedDie = 0.065f;
Listing 7-12FPSInputController Class Fields 1
正如我们之前看到的,CharacterMotor
字段获取 FPS 输入控制器的运动向量,并在各种计算中使用它,最终以移动角色结束,在这种情况下,是一个悬停赛车模型。布尔标志aiOn
负责与其他输入类一起打开和关闭 AI 控制。下一个字段directionVector
,表示运动的方向。随后,directionLength
字段表示运动矢量的大小。inputMoveDirection
字段是一个表示输入移动方向的Vector3
实例。最后两个条目用于管理使用触摸输入时的加速。用于加速汽车的touchSpeed
字段和用于在触摸输入移除后减缓基于触摸的加速的touchSpeedDie
字段。接下来,我们将看看这个类的相关方法大纲。
相关的方法大纲/类头:FPSInputController
这门课只有两个主要的方法让我们复习。
//Main Methods
void Awake();
void Update();
Listing 7-13FPSInputController Pertinent Method Outline/Class Headers 1
该类的导入语句和声明如下。
using UnityEngine;
// Require a character controller to be attached to the same game object
[RequireComponent(typeof(CharacterMotor))]
[AddComponentMenu("Character/FPS Input Controller")]
public class FPSInputController : BaseScript {}
Listing 7-14FPSInputController Pertinent Method Outline/Class Headers 2
注意,类声明使用了“RequireComponent
”代码属性来要求将CharacterMotor
组件附加到父游戏对象。还要注意,这个类扩展了BaseScript
类,正如我们之前看到的,这个类又扩展了MonoBehaviour
类。在下一节中,我们将看看这个类的主要方法。
主要方法详细信息:FPSInputController
本节包含我们将详细讨论的两种方法,如下所示。
01 void Awake() {
02 motor = GetComponent<CharacterMotor>();
03 base.PrepPlayerInfo(this.GetType().Name);
04 if (BaseScript.IsActive(scriptName) == false) {
05 Utilities.wrForce(scriptName + ": Is Deactivating...");
06 return;
07 } else {
08 aiOn = p.aiOn;
09 }
10 }
01 void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 // Get the input vector from keyboard or analog stick
07 directionVector = Vector3.zero;
08 if (gameState.gamePaused == true) {
09 return;
10 } else if (gameState.gameRunning == false) {
11 return;
12 }
13
14 if (aiOn == true && p != null) {
15 if (p.pause == true) {
16 return;
17 }
18 directionVector = p.UpdateAiFpsController();
19 } else {
20 if (Input.touchSupported == true && gameState.accelOn == true) {
21 if (Input.touchSupported == true) {
22 if (gameState.accelOn == true) {
23 touchSpeed = 1.0f;
24 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, touchSpeed);
25 } else {
26 touchSpeed -= (touchSpeed * touchSpeedDie);
27 if (touchSpeed < 0.0) {
28 touchSpeed = 0.0f;
29 }
30 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, touchSpeed);
31 }
32 }
33 } else {
34 if (Input.GetAxis("Turn") < 0.0f) {
35 if (Input.GetAxis("Horizontal") < 0.0f) {
36 transform.Rotate(0, -1.75f, 0);
37 } else {
38 transform.Rotate(0, -1.25f, 0);
39 }
40 }
41
42 if (Input.GetAxis("Turn") > 0.0f) {
43 if (Input.GetAxis("Horizontal") > 0.0f) {
44 transform.Rotate(0, 1.75f, 0);
45 } else {
46 transform.Rotate(0, 1.25f, 0);
47 }
48 }
49
50 if (Input.GetAxis("Vertical") > 0.0f) {
51 touchSpeed = 1.0f;
52 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, touchSpeed);
53 } else if (Input.GetAxis("Vertical") < 0.0f) {
54 touchSpeed = -0.65f;
55 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, touchSpeed);
56 } else {
57 touchSpeed -= (touchSpeed * touchSpeedDie);
58 if (touchSpeed < 0.0f) {
59 touchSpeed = 0.0f;
60 }
61 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, touchSpeed);
62 }
63 }
64 }
65
66 if (directionVector != Vector3.zero) {
67 // Get the length of the direction vector and then normalize it
68 // Dividing by the length is cheaper than normalizing when we already have the length anyway
69 directionLength = directionVector.magnitude;
70 directionVector = directionVector / directionLength;
71
72 // Make sure the length is no bigger than 1
73 directionLength = Mathf.Min(1.0f, directionLength);
74
75 // Make the input vector more sensitive towards the extremes and less sensitive in the middle
76 // This makes it easier to control slow speeds when using analog sticks
77 directionLength = directionLength * directionLength;
78
79 // Multiply the normalized direction vector by the modified length
80 directionVector = directionVector * directionLength;
81 }
82
83 // Apply the direction to the CharacterMotor
84 inputMoveDirection = transform.rotation * directionVector;
85 motor.inputMoveDirection = inputMoveDirection;
86 }
Listing 7-15FPSInputController Main Method Details 1
我们要看的第一个方法是Awake
类初始化方法。第 2 行设置了motor
字段。接下来,在第 3 行,我们调用基类的准备方法的 player info 版本。它负责设置我们的标准字段、游戏和玩家状态数据,同时还注册操作的结果。在第 4 行,我们检查类的配置是否正确。如果没有,我们写一些日志并退出。如果是这样,我们用来自PlayerState
实例的当前值更新aiOn
字段,基类字段,p
,第 8 行。
我们要回顾的下一个方法是 Unity 引擎回调方法Update
。每个游戏帧调用一次这个方法。第 2-4 行现在应该非常熟悉了。代码是为了在类配置由于某种原因失败时对方法进行转义。在第 7–12 行,directionVector
被重置为零向量,如果游戏暂停或者没有运行,该方法被转义。AI 控制在第 14–19 行处理,调用第 18 行的UpdateAIFpsController
方法,完成计算下一个运动向量的大部分工作。
接下来,让我们看看第 20–33 行的一小段代码。这个代码负责处理触摸输入。在这种情况下,加速度由触摸输入的存在来控制,并且方向通过处理触摸输入的水平变化来处理。请注意第 34–48 行的代码。这段代码负责将 hover racer 左转或右转。
从第 50-64 行,我们确定输入是向前还是向后移动悬停赛车。请注意,在触摸输入的相同过程中,我们不会让汽车倒车,第 28 行。然而,当使用键盘或控制器输入时,我们可以反过来,第 54 行。看一下第 56 行到第 62 行的代码。你能弄清楚这段代码是做什么的吗?当没有检测到触摸时,它会慢慢地将车速降低到零。第 66-85 行的剩余代码块清除了方向向量,对播放器的最终应用发生在第 84-85 行。这就使我们结束了这一审查步骤。
演示:FPSInputController
很像CharacterMotor
类,FPSInputController
类没有专门的演示场景。相反,任何场景都可以。运行场景时,花点时间在 Unity 编辑器的“层次”面板中找到并展开“起始集”条目。选择子条目HoverCar0
,在“检查器”面板中找到“FPS 输入控制器”脚本组件条目并展开它。
现在回去玩游戏和/或演示场景,同时在检查器面板中观察职业属性的变化。这是一个很好的方法来了解实际的输入如何转化为类中的值和悬停赛车的运动。这节课的复习到此结束。接下来,我们将看看鼠标输入处理程序。
课堂回顾:MouseLookNew
MouseLookNew
类用于处理鼠标输入并将其转换为旋转数据,这样用户就可以用鼠标操纵悬停赛车。我们将使用以下步骤来复习本课程:
-
枚举
-
类别字段
-
相关的方法大纲/类头
-
主要方法详细信息
-
示范
这堂课短而甜。我们很快就会完成审查。我们将从查看类的枚举开始。
枚举:MouseLookNew
MouseLookNew
类有一个枚举用于讨论。正在讨论的枚举如下。
1 public enum RotationAxes {
2 MouseXAndY = 0,
3 MouseX = 1,
4 MouseY = 2
5 }
Listing 7-16MouseLookNew Enumerations 1
枚举用于描述输入驱动旋转的可用类型。我应该提到的是,仅仅因为它们被列在这里并不意味着游戏支持所有的选项。在任何情况下,RotationAxes
枚举都是根据鼠标输入描述某种旋转的便捷方式。接下来,我们将更详细地了解类字段如何模拟鼠标输入。
类字段:MouseLookNew
MouseLookNew
类主要有一些字段来帮助模拟一个灵敏度过滤的鼠标输入。让我们看看下面列出的类字段。
public RotationAxes axes = RotationAxes.MouseXAndY;
public float sensitivityX = 12.0f;
public float sensitivityY = 12.0f;
public float minimumX = -360.0f;
public float maximumX = 360.0f;
Listing 7-17MouseLookNew Class Fields 1
列出的第一个字段axes
用于描述应该检索哪些轴数据。这是一个描述性字段。它不驱动功能性;它反映了当前的配置。随后,有两个字段minimumX
和maximumX
,用于描述应用于 X 和 Y 轴输入的灵敏度。接下来的两个字段可用于约束 X 轴值。我将在这里列出剩余的字段。
public float minimumY = -60.0f;
public float maximumY = 60.0f;
public float mouseX = 0f;
public float mouseY = 0f;
public bool aiOn = false;
Listing 7-18MouseLookNew Class Fields 2
列出的前两个字段minimumY
和maximumY
可用于约束计算的 Y 轴值。接下来,mouseX
和mouseY
字段用于保存 X 和 Y 轴的原始鼠标输入数据。aiOn
字段是一个布尔标志,用于切换鼠标的 AI 控制。最后,rigidBodyTmp
字段用于引用当前玩家的刚体组件。
相关的方法大纲/类头:MouseLookNew
这个类只有两个方法让我们担心,但是我们将完成相关的方法回顾部分来更彻底。
//Main Methods
void Start();
void Update();
Listing 7-19MouseLookNew Pertinent Method Outline/Class Headers 1
该类的导入语句和声明如下。
using UnityEngine;
public class MouseLookNew : BaseScript {}
Listing 7-20MouseLookNew Pertinent Method Outline/Class Headers 2
注意,这个类扩展了BaseScript
类,这意味着它是一个MonoBehaviour
,具有一组标准的基本字段,用于插入游戏的游戏状态对象。在下一节中,我们将详细看看主要的方法。
主要方法细节:MouseLookNew
该类的主要方法在下一组中列出。我应该提到,如果设备支持触摸,触摸屏输入(如在屏幕上拖动一个手指)将被解释为鼠标操纵输入。让我们开始写代码吧!
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 aiOn = p.aiOn;
08 }
09
10 // Make the rigid body not change rotation
11 rigidBodyTmp = GetComponent<Rigidbody>();
12 if (rigidBodyTmp != null) {
13 rigidBodyTmp.freezeRotation = true;
14 }
15
16 if (Input.touchSupported == true) {
17 sensitivityX = 5.0f;
18 sensitivityY = 5.0f;
19 }
20 }
01 void Update() {
02 if (BaseScript.IsActive(scriptName) == false) {
03 return;
04 }
05
06 if (gameState.gamePaused == true) {
07 return;
08 } else if (gameState.gameRunning == false) {
09 return;
10 }
11
12 if (aiOn == true && p != null) {
13 if (p.pause == true) {
14 return;
15 }
16 p.UpdateAiMouseLook();
17 } else {
18 if (gameState.newTouch == false) {
19 mouseX = Input.GetAxis("Mouse X");
20 transform.Rotate(0, mouseX * sensitivityX, 0);
21 }
22 }
23 }
Listing 7-21MouseLookNew Main Method Details 1
这个方法有几个要点供我们讨论。在第 2–6 行,我们通过扩展需要游戏状态和玩家状态数据的BaseScript
类,对MonoBehaviour
进行了标准初始化,第 2 行。如果类的配置有任何问题,我们退出该方法。否则,我们切换 class’ aiOn
字段以匹配玩家的状态对象,第 7 行。接下来,在第 11-14 行,我们冻结游戏对象的Rigidbody
的旋转,如果它可用的话。一般来说,在这个游戏中,我们不会让玩家在 Y 轴以外的轴上旋转。最后,在第 16–19 行,如果当前设备支持触摸,我们将 X 轴和 Y 轴的灵敏度从 12.0f 降低到 5.0f。
演示:MouseLookNew
MouseLookNew
类的最佳演示是玩游戏或运行任何演示,同时在“Inspector”面板中监控当前玩家的MouseLookNew
组件。为此,启动游戏或演示场景,然后进入“层级”面板,找到名为StartingSet
的条目。展开它并选择HoverCar0
子条目。选择它,然后看看“检查”面板。
现在,找到“Mouse Look New”组件条目并展开它。回到游戏继续玩。请注意,当您在玩游戏时使用鼠标时,类的值会发生变化。请留意检查器中显示的是什么类型的值。这就是我们这节课复习的结论。接下来,我们将看看驻留在GameState
类中的一些触摸输入代码来结束这一章。
课程回顾:游戏状态(仅触摸输入片段)
GameState
类是游戏的主要控制类。因此,它提供了一个关于球员、HUD、菜单系统等数据交换的集中点。因为我们想要集中触摸输入数据,所以我们决定在GameState
类的Update
方法中包含触摸输入代码。让我们看看它是如何工作的。
01 if (Input.touchCount == 1) {
02 touchScreen = true;
03
04 if (Input.GetTouch(0).phase == TouchPhase.Began) {
05 newTouch = true;
06 accelOn = true;
07 } else if (Input.GetTouch(0).phase == TouchPhase.Moved) {
08 newTouch = false;
09 } else if (Input.GetTouch(0).phase == TouchPhase.Stationary) {
10 newTouch = false;
11 } else if ((Input.GetTouch(0).phase == TouchPhase.Ended || Input.GetTouch(0).phase == TouchPhase.Canceled)) {
12 newTouch = false;
13 accelOn = false;
14 }
15 } else {
16 newTouch = false;
17 accelOn = false;
18 }
Listing 7-22GameState Touch Input Snippet 1
第一行代码用于检测触摸屏上是否有一个手指。Hover Racers 的设计是通过一个手指来工作,当触摸屏幕时加速汽车,当没有触摸屏幕时减速。此外,当触摸屏幕时,向左或向右移动手指将使汽车向左或向右转向。在第 2 行上,touchScreen
布尔域被设置为真,以指示激活的触摸屏。
在第 4-6 行,如果新的触摸已经开始,布尔字段newTouch
和accelOn
被设置为真。如果手指已经移动,第 7 行,新的触摸布尔被设置为假。这表明应该处理触摸转向输入。类似地,如果触摸没有移动,newTouch
字段被设置为假。当触摸交互结束时,行 11,然后newTouch
和accelOn
字段被设置为假。第 16–17 行的最后一位代码用于在没有检测到输入时关闭触摸输入。
第二章结论
我们在这一章中已经讲了很多内容;具体来说,我们回顾了游戏输入处理背后的类。这些类接受触摸、鼠标、键盘和控制器输入,并使用它们来移动、转向和扫射悬停赛车。如果仔细观察,您会注意到所有的输入都是使用描述特定映射的字符串常量来查找的。例如,在MouseLookNew
类中,我们使用“鼠标 X”作为鼠标 X 轴输入。关于游戏使用的输入映射的更多信息将在后面的文本中提供。我们在本章中讨论的输入处理程序类如下:
-
字符电机:该类由
FPSInputController
驱动,驱动CharacterContoller
组件。该级别主要用于在考虑地面坡度的情况下控制地面的基本运动。在我们的例子中,滑动也是计算的一部分,但是跳跃和重力是无效的。 -
FPSInputController:这个类及其子类从不同的来源获取用户输入,并将其传递给
CharacterMotor
类,最终驱动CharacterController
和 hover racer 模型。 -
MouseLookNew:顾名思义,这个类使用鼠标输入来驾驶汽车。如果触摸屏输入在设备上处于活动状态,它还能够处理触摸屏输入来驾驶悬停赛车。
-
GameState(触摸输入):
GameState
类由于其集中化,是存储需要共享的数据的好地方。因此,该类用于检测触摸输入,设置几个类字段来指示触摸输入是活动的。
这些类使用的一些数学和向量计算很复杂。如果您计划对其进行更改,请花时间添加日志记录和其他代码来监视和理解您正在更改的代码是如何工作的。请记住,当你在 Unity 编辑器中玩游戏的时候,你可以并且应该监控脚本组件。请注意,当游戏停止时,使用“检查器”面板所做的数值更改将会丢失。在微调输入控件类字段时,请记住这一点。在下一章,我们将看看游戏的菜单系统类。