Skip to main content

每个系统都设计为具有特定的职责集,并尽可能少地了解其他系统。

概述

  • PlayerDataManager 为每个进入世界的玩家分配 PlayerData 对象。
  • 当 PlayerData 对象进入起始门检查点时,他们刚刚进入的赛道开始跟踪时间,并激活下一个检查点。
  • 当 PlayerData 对象通过最后一个检查点时,他们的时间被添加到记分板。
  • 如果 PlayerData 对象进入增强道具触发器,PlayerModsManager 会临时更改其速度和/或跳跃能力,在设定时间后重置为默认值。
  • 如果 PlayerData 对象进入重生触发器,赛道会将其重生到最后通过的检查点处。

以下部分描述了组合在一起构成完整体验的程序和脚本。

玩家

每个加入世界的玩家都会获得一个"PlayerData"对象来管理其状态和赛道进度。PlayerDataManager 分配 PlayerData 对象,这些对象可以触发 OnPlayerDataEnter 程序。

PlayerDataManager​

可以在场景中"Udon"对象下的"PlayerDataManager"GameObject 上找到此程序。它有两个重要的公共变量: dataPool:对此管理器所在对象上的 VRC Object Pool 组件的引用。当玩家加入世界时,此管理器将尝试为玩家生成一个 PlayerData 对象,并赋予其所有权。 followCam:对玩家跑赛道时跟随在玩家上方的摄像机的引用。在此设置,以便 PlayerDataManager 在刷新时可将引用分配给每个 PlayerData 对象。

当更改工具窗口中的"玩家数量(Number of Players)"选项时,所有现有的 PlayerData 对象将从场景中移除,然后新的副本将作为 PlayerDataManager 的子对象添加。每个对象都将正确设置其公共变量,对象池将更新以容纳所有新的 PlayerData 对象。

PlayerObject​

PlayerObject 预制体具有 Rigidbody 和 Capsule Collider 组件,用于触发增强道具、障碍物等。它位于自定义层级 CoursePlayer,仅与 CourseTrigger 碰撞以与障碍物和增强道具交互。它还带有一个包含重要程序的 UdonBehaviour:

PlayerData​

此程序是跑赛道的玩家与所有其他系统之间的主要连接器。其变量包括: timeElapsed:同步浮点数,由赛道程序在玩家穿过终点门时更新。当它改变时,PlayerData 对象的所有者将在记分板上显示此时间,以便本地查看最新时间。ScoreManager 对象的所有者将看到此更改,并将玩家的新时间和显示名称添加到记分板。

isRacing:布尔值,当玩家进入起始门时由赛道程序设置为 true。当玩家进入终点门、使用菜单手动重生或赛道调用 Reset 时设置为 false。由赛道使用,详见该程序。

rigidbody:程序在 Start 时缓存,无需在检查器中设置。在每次 Update 期间,它会移动到玩家的位置和旋转。

player:对本地玩家实际 VRCPlayerApi 对象的引用。在此程序上的同步 playerId 更改时缓存。用于获取玩家的 displayName。

timeDisplay:对显示本地玩家最新时间的 UdonBehaviour 的引用。

scoreManager:对 ScoreManager UdonBehaviour 的引用。当该对象的所有者收到刚完成赛道的 PlayerData 对象的 timeElapsed 更改时,它会将 ScoreManager 对象上的公共变量 scoreToProcess 设置为一个字符串,该字符串将 displayName 和 elapsedTime 组合为要处理的单个字符串。

scoreManagerObject:对持有 ScoreManager 程序 UdonBehaviour 的 GameObject 的引用。用于确保只在 ScoreManager 对象的所有者上运行分数处理逻辑。我们无法从 UdonBehaviour 引用获取此 GameObject,因此在此包含它。

followCam:对跟随玩家跑赛道的 CinemachineVirtualCamera 的引用。程序将自身的 Transform 设置为摄像机的 follow 和 lookAt 目标,并在 isRacing 变化时更改此摄像机的优先级。

OnPlayerDataEnter​

此程序用于需要检测 PlayerData 对象进入其触发碰撞器的对象。我们使用自定义层级 CoursePlayer 和 CourseTrigger 确保只有特定对象会触发此碰撞器。当触发时,它会触发内部事件 OnPlayerDataEnter 来执行多种操作。此程序具有以下变量:

fxPrefab:在 Trigger 时生成的 GameObject,用于播放声音、显示粒子等,让玩家知道发生了某事。

program:目标 UdonBehaviour,包含要在 Trigger 时运行的事件。此 program 包含检查点、增强道具、障碍物等的特定逻辑。

eventName:要在目标 program 上运行的事件名称。

deactivateOnTrigger:此对象是否应在单次 Trigger 后停用自身。这对于检查点和其他每次跑赛道只应激活一次的项目很有用。

lastCollider:启动触发逻辑的碰撞器,在调用 Trigger 前临时缓存,用于在需要时查找 PlayerData UdonBehaviour。

fxSpawn:用于设置将要生成的 FX 位置的 Transform。如果未设置,默认为带有碰撞器的对象的 Transform。当穿过碰撞器时想在其他位置触发烟花效果时很有用,例如我们在终点门所做的。

sendPlayerData:布尔值,决定是否尝试传递触发逻辑的 PlayerData 程序。在进入起始门时使用,也可能用于其他用途。

当检测到 PlayerData 碰撞器触发进入时,此程序执行以下操作:

  • 如果设置了 eventName 变量,则检查 sendPlayerData 是否为 true。如果是,则尝试将目标 UdonBehaviour 程序上的 playerData 变量设置为刚刚碰撞的 UdonBehaviour。
  • 然后在目标程序上运行事件 eventName。
  • 如果此程序上的 fxPrefab GameObject 已设置(未保留为默认的'self'),则实例化预制体的副本,并根据 fxSpawn 变量设置其位置和旋转。
  • 如果 deactivateOnTrigger 为 true,则将此 GameObject 设置为非活动状态。

赛道与检查点

这是项目的核心,您需要穿过这些门和检查点来完成计时赛。

Course

此程序位于 CourseManager 对象上,管理本地玩家的计时赛状态。它没有任何同步变量——只了解正在跑赛道的本地玩家。

在 Start 时,它调用 Reset 来正确设置自身。 如果玩家自行重生,赛道将 Reset。

在 Reset 时,我们关闭所有检查点触发器(起始门除外,它保持开启)。我们通过遍历 checkpoints 数组中的每个 GameObject,查找每个 Trigger Collider,并对其 GameObject 调用 SetActive(索引 0 设为 true,其他设为 false)来实现。 我们还将 nextIndex 设为 -1,将 isRacing 设为 false。

在 StartRace 时,我们:

  • 从当前时间设置 startTime
  • 将 isRacing 设为 true
  • 将 nextIndex 设为 1(因为比赛从通过检查点 0 开始)

当检查点被触发时,它将 Course 上的 nextIndex 设置为其自身索引加 1。这会触发 Course 程序上的 nextIndexChange 事件,从而激活下一个检查点的 GameObject。

在 Update 期间,我们检查玩家是否 isRacing,如果是,则获取运行经过的时间并将其设置在 timeDisplay Text 对象上。

在 FinishRace 时,我们:

  • 将 isRacing 设为 false
  • 将目标 PlayerData 程序上的 timeElapsed 设置为当前时间减去 startTime
  • 将 playerData 设为 null,因为我们不再有玩家在跑赛道
  • 等待 resetDelay 秒,然后 Reset 赛道

在 Respawn 时,我们检查玩家是否 isRacing。如果是,则将玩家送回最后一个检查点的变换位置。如果不是,则将玩家传送到足够低的位置,使其被世界重生回原始生成点之一。

ObstacleCourseData​

此自定义脚本仅保存对 ObstacleCourseAsset 的引用,包含关于赛道的所有信息,如使用哪些预制体、玩家数量、默认速度等。它由工具窗口加载,因此场景中应有一个。您应创建自己的副本,这样在更新项目到较新版本的包时不会被覆盖。只需复制现有资源即可,这样能确保默认值正确。

Checkpoint​

每个检查点对象都有一个索引,代表其在计时赛中的顺序,通过工具窗口放置检查点或修改其顺序时自动设置。 它们具有一个带有 OnPlayerDataEnter 程序的触发碰撞器,该程序调用我们在示例预制体中名为"UdonProgram"的对象上的 Checkpoint 程序。该程序很简单,可以触发三个可能的事件:

StartRace 会将 Course 程序上的 playerData 变量设置为刚刚进入此检查点的 UdonBehaviour。Course 将在那时开始比赛。

Trigger 会将 Course 程序上的 nextIndex 变量设置为 index + 1。

FinishRace 将简单地调用 Course 程序上的 FinishRace。

分数

没有友好竞争的计时赛算什么?分数系统同步最新跑步的名称和时间,以及实例中迄今为止的最佳成绩。

ScoreManager​

此程序位于"Udon"GameObject 下的"ScoreManager"对象上。它使用队列系统处理传入的分数并同步它们。它本身没有任何同步变量,而是依靠 ScoreFields 来同步值。当您更改"显示的分数数量"时,工具窗口会自动填充这些字段。

在 Start 时,此程序调用自身的 Render 事件一次。

在 Render 时,程序调用 scoreCam 上的 Render,将其当前视图渲染到 RenderTexture,用于赛道各处显示当前分数。

当此对象的所有者上的 scoreToProcess 更改时,我们调用 MakeRoom,然后调用 ProcessNextScore。这是可行的,因为实例中的每个玩家在有人完成跑步时都会收到 timeElapsed 的更新,如果他们是所有者,该程序将更新此对象上的 scoreToProcess。

在 MakeRoom 时,我们检查 scoreFields 是否已满,如果已满,我们将迭代地向下复制值以在顶部腾出空间。

在 ProcessNextScore 时:

  • 将分数分解为 displayName 和时间以便格式化,然后将对应 ScoreField 上的 targetVarName 值设置为此值。此目标变量是同步的,因此我们以这种方式设置来为所有人更新。
  • 将此分数的时间与最高分时间进行比较,必要时更新 HighScoreField。
  • 将 scoreToProcess 的值设置为空字符串,准备处理下一个传入的分数。
  • 向所有人发送 Render 事件以更新其分数纹理。

ScoreField​

此程序使用简单有效的模式——它有一个名为 log 的公共同步变量。当日志更改时,它会将字段中的文本更新为新值。这样,当对象的所有者更新值时,这些值会为所有人同步和更新,其他程序可以轻松完成此操作。在我们的案例中,我们通过 ScoreManager 更新此值。

HighScoreField​

此程序使用与上述分数字段相同的模式,但还有一个同步的 score 浮点数,可用于比较分数并仅在新分数更好时更新。它还有一个"prefix(前缀)",是在任何更改前注入的字符串。在本例中,字符串"最高分:"被添加到传入字符串之前。

增强道具

为追求最高分的玩家提供速度和跳跃增强是很有趣的,您也可以将速度和跳跃惩罚用作障碍物的一部分,让玩家在策略上有所选择。通过工具窗口创建增强道具时,它们都作为"PlayerModsManager"对象的子对象放置。它们还会自动设置 PlayerModsManager UdonBehaviour,以便应用效果。

它们有一个非常简单的程序。当然,它是从 OnPlayerDataEnter 程序调用的,并有一个 Trigger 事件。其变量为:

playerModsManager:通过工具窗口创建增强道具时自动设置。用于实际应用效果。

speedChange:触发时应用于玩家速度的效果。0 跳过,正值增加速度,负值降低速度。

jumpChange:与 speedChange 相同,但用于跳跃冲量。

effectDuration:效果持续多长时间。

在 Trigger 时,如果 speedToProcess 不为 0,程序将设置 PlayerModsManager 上的 speedToProcess;如果 jumpToProcess 不为 0,则设置 jumpToProcess。为简化逻辑,我们将 amount(量)和 duration(持续时间)值打包到单个 Vector2 中,其中 x 是量,y 是持续时间。

PlayerModsManager​

有一个中心位置管理玩家能力的变化非常有用,特别是考虑到有人可能先获得持续 2 秒的"速度+3",然后又获得持续 3 秒的"速度-1"。在我们的程序中,速度修改相互抵消,跳跃修改也相互抵消。因此,在上面的例子中,一旦玩家触发"速度-1"增强道具,他们将重置为默认速度减 1,并开始新的 3 秒计时器。

该程序像 ScoreManager 一样使用队列工作。当 speedToProcess 更改时,它将计算要使用的新速度,将其应用于本地玩家的 VRCPlayerApi,并根据增强道具的 effectDuration 开始倒计时。程序在用户的 HUD 上显示修改并随时间淡出,以便玩家直观地了解剩余时间。当计时器结束时,它将 VRCPlayerApi 上的目标属性重置为默认值,这就是我们在此处存储和设置这些值而不是在"VRCWorldSettings"程序中的原因。

DestroyAfterXSeconds​

这个简单程序对于本地实例化的对象很有用,例如由 OnPlayerDataEnter 程序创建的 FX 预制体。它将确保对象自毁,因此不会留下数百个旧音效和粒子系统。

PlayClipFromArray​

此程序用于引入一些声音变化,例如用于 FX 预制体。您可以在此程序上设置一组 AudioClip,而不是单个 AudioClip,它在创建时会随机选择一个进行播放。也可用于脚步声程序。

障碍物

如果想挑战玩家,可以添加各种障碍物。我们包含了一些示例程序,欢迎制作自己的!

Autorotate​

此程序简单地旋转其所在的 Transform。您可以调整每个轴的 amount,它将乘以 Time.deltaTime 以确保平滑旋转。动画器性能更好,但这种方法在实验时有效。

SpawnedHazard​

此障碍物会降低与其接触的玩家的速度。您可以像增强道具一样设置 speedChange——x 是添加到玩家速度的量,y 是效果的持续时间。要将玩家速度降低 3 持续 1 秒,可将 speedChange 设置为 (-3,1)。它们在创建时按名称查找"PlayerModsManager"GameObject 和 UdonBehaviour——性能不是最优但可行。

HazardSpawner​

此程序使用 SendCustomEventDelayedSeconds 每隔 delay 秒生成障碍物。在示例项目中,我们使用略有不同的延迟来制造一个棘手的桶山供玩家躲避。

FallingBlock​

此程序是我们包含的唯一与玩家交互但不使用 OnPlayerDataEnter 的程序。这是因为我们需要知道玩家何时进入_和_何时退出,这是该程序无法处理的。当玩家进入时,我们使用 SendCustomEventDelayedSeconds 在 triggerTime 秒后运行 CheckForDrop。

在 CheckForDrop 时,如果玩家尚未退出碰撞器,它将设置其 Rigidbody 为非动力学,使其下落(玩家也随之掉落)。然后在 resetTime 秒后调用 Reset。

杂项

注入​

此项目有一个注入对某些组件的引用的系统。详见此处。