Skip to content

Latest commit

 

History

History
1462 lines (1156 loc) · 68.6 KB

File metadata and controls

1462 lines (1156 loc) · 68.6 KB

五、高级交互类

在这一章中,我们将看看两个重要的集中式交互处理程序:CollideScriptCarSensorScript类。这两个脚本完善了游戏的交互处理程序及其相关的游戏机制。CollideScript处理许多不同的交互,这些交互发生在不同的游戏对象与悬停赛车发生碰撞时。

CarSensorScript与此类似,但只适用于悬浮赛车。它的主要职责是处理不同类型的车对车交互。我们先来回顾一下CollideScript

课堂回顾:碰撞脚本

CollideScript类负责处理许多不同的碰撞交互和它们相关的游戏机制。我将在此列出支持的交互类型:

  1. 助推器标记

  2. SmallBoostMarker

  3. TinyBoostMarker

  4. MediumBoostMarker

  5. TinyBoostMarker2

  6. 跳跃标记

  7. 健康标记

  8. 枪标记

  9. 无敌标记

  10. 装甲标记

  11. 未加标签的

  12. 可击中的

  13. HittableNoY

  14. 演员

那是很多的交互。花点时间看一下。有什么让你印象深刻的吗?在大多数情况下,它们看起来很简单。不过,名为“Players”的列表看起来确实有点有趣。当我们复习CollideScript课程时,我们必须留意这一点。

正如我之前提到的,CollideScript是一个与玩家的悬浮赛车进行集中交互的游戏。在比赛过程中,当玩家的汽车与赛道上的不同物体发生碰撞时,游戏机制会激活以调整汽车的物理特性。我们将使用以下课堂回顾模板来介绍本课程:

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

  2. 类别字段

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

  4. 支持方法详细信息

  5. 主要方法详细信息

  6. 示范

关于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,是一个浮点值,用于增加跳跃的垂直力。接下来的两个类字段,minForcemaxForce,用于设置当玩家的赛车与可击中的物体发生碰撞时所施加的力的范围。随后的字段lockAxisY是一个布尔标志,控制在确定碰撞效果时是否使用 Y 轴(垂直轴)。bounceDampener场用于减少反弹事件中的力。

以下两个字段minBounceVelocityHorminBounceVelocityVer,用于确保反弹修改器有足够的力来真正反弹汽车。请注意,该修改器作用于水平轴和垂直轴。我应该注意到这个反弹修改器不同于我们之前讨论过的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脚本组件的父GameObjectAudioSource组件。这些声音效果被加载到类字段中,以便在该类处理碰撞交互时使用。我们要看的下一个字段块是在可碰撞物体碰撞时使用的。

//***** 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类的一个实例,用于检测可击对象是否应该被施加力。

接下来的两个条目moveDirectionrotateDirection用于确定碰撞后可击中的物体以何种方向和何种旋转飞离悬停赛车。最后,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冲突的引用。以下四个字段v3xyz,都是用来计算碰撞结果所涉及的力。计算结束时,矢量分量值xyz存储在矢量场v3,中。isBouncing字段是一个布尔标志,指示悬停赛车是否被反弹。

随后列出的字段useReflectuseInverse是用于改变反弹力计算的布尔标志。下一个字段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 行。下面列出的两种支持方法GetMinForceGetMaxForce非常相似。这两种方法都限制传入的力,并尊重力的符号。这些方法相当直接。通读它们,确保你理解这些方法是如何工作的。

下一个要回顾的方法是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 参数的主体,以查看值是否为空,或者bodyisKinematic标志是否设置为真。如果是的话,那么力和碰撞将不再影响刚体。我们尊重这一点,并在我们的代码中进行检查。

通过调用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 会阻止方法执行任何工作。反弹向量的xyz分量在第 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的另一个职责是运行模拟射击另一辆悬停赛车的自动枪。如果后面的车有弹药,并能让它前面的车保持在传感器内,直到目标跟踪完成,枪走火,就会发生这种情况。随机掷骰子决定命中,如果命中的车没有更多生命值,作为惩罚,它会返回几个点。我们将使用以下课堂回顾模板来介绍本课程:

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

  2. 类别字段

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

  4. 支持方法详细信息

  5. 主要方法详细信息

  6. 示范

让我们来看看一些代码!

静态/常量/只读类成员: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_BOOSTBASE_NON_BOOST用于跟踪增强和非增强默认力。后续字段AUDIO_SOURCE_NAME_GUN_SHOTAUDIO_SOURCE_NAME_TARGETTING类字段用于加载AudioSource组件,并应反映所用音频资源的名称。集合中的最后两个字段TRIGGER_TIME_DRAFTINGTRIGGER_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

前两个字段是对连接到玩家汽车上用作音效的音频组件的引用,audioGunShotaudioTargetting。这个集合中列出的下一个字段是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方法在力计算中使用的absXabsZ字段。接下来的三个字段都是Vector3实例:passLeftV3passGoV3passV3。这些字段用于设置向量的力,这些向量用于在超车时使汽车绕过另一辆汽车。接下来我们将看看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对象,当汽车传感器与另一个玩家的悬停赛车相撞时,该对象被记录下来。i2l2字段用于控制通过传感器采集的汽车列表的循环。tb字段是一个布尔标志,用于指示当前玩家的汽车应该开启自动超车技工。t1t2字段用于确定被跟踪车辆的距离。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 行的代码会阻止该方法执行任何工作。我们要看的下一小段代码准备了方法的局部变量。absXabsZ类字段被设置为当前玩家的速度。我们只关心 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 行,标准的实用方法调用加载了GameStatePlayerState引用,除了在这种情况下,我们将它应用于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被调用来初始化GameStatePlayerState引用。如果类配置以某种方式失败,该方法在第 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方法。我们最后要复习的主要方法是CarSensorScriptUpdate法。该方法由 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

CarSensorScriptUpdate方法中的下一段代码负责应用自动传球游戏机制。在第 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”中,您必须使用棋盘上两个战斗模式修改器中的一个来武装您的悬停赛车。接下来,靠近对手的车,等一会儿,直到你听到瞄准的声音效果。接下来,听听枪声。如果你开火多次,对手的车将会跳跃,因为它会重新定位到先前的航路点进行重新射击。

img/520710_1_En_5_Fig1_HTML.png

图 5-1

汽车传感器脚本演示场景 1 描述汽车传感器脚本射击演示场景的屏幕截图

img/520710_1_En_5_Fig2_HTML.png

图 5-2

汽车传感器脚本演示场景 2 描述汽车传感器脚本自动超车演示场景的屏幕截图

前面显示的屏幕截图描述了用于演示该类的两个不同场景。这就是我们课程复习的结论。

第二章结论

这就引出了本章的结论。让我们来看看我们在这里讨论过的材料。这应该总结了第 2 章游戏规范中列出的所有游戏交互驱动的游戏机制。

  1. CollideScript:这个类是一个集中的交互点,支持一些不同的游戏机制:

    1. 未标记:忽略的游戏对象。

    2. 可击中的:碰撞时飞出的物体;想想“油桶”

    3. 助推:几个不同的助推标记,当碰撞时加速汽车。

    4. 跳跃:一个交互标记,当与玩家的车发生碰撞时,会导致玩家的车跳跃。

    5. 战斗模式标记:交互标记只在战斗模式下可用。这些标记控制战斗模式修改器,例如:

      1. 弹药

      2. 健康

      3. 装甲

      4. 无敌

  2. CarSensorScript:这个类是第二个集中的交互点,支持一些不同的游戏机制:

    1. 自动超车:以一定的速度和距离紧跟一辆车会触发自动超车游戏机制。

    2. 射击尝试:当你打开枪的时候,在足够长的时间内紧紧跟随一辆车,你的车会瞄准并试图射击你前面的车。

有了这些新的游戏机制,我们几乎有了一个完整的赛车游戏。我们需要的只是一个中央游戏状态、输入处理程序和一些助手类。请注意不同的脚本组件如何通过我们在使用中看到的集中式GameStatePlayerState类实例来控制当前玩家的汽车和 AI 控制的汽车。这种查找玩家状态数据的方法有助于悬停赛车手与他们的环境以及其他人进行交互。在下一章,我们将放慢一点速度,看看一些助手类。