12 KiB
58 · 速通模式系统(Speedrun Mode System)
命名空间
BaseGames.Speedrun
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.World(SceneLoader)·BaseGames.UI(HUD)
关联 10_UISystem · 46_PlatformIntegration · 52_CompletionEndingDesign · 42_DebugCheatSystem
目录
- 速通模式设计原则
- 计时系统:RTA vs IGT
- SpeedrunTimer 实现
- 速通分类(Category)
- 分段计时(Split)
- 速通 HUD
- Livesplit 集成
- 速通规则合规性设计
- 存档数据分离
- 速通社区沟通规范
1. 速通模式设计原则
| 原则 | 含义 |
|---|---|
| 不影响普通游戏 | 速通功能完全可选,默认关闭,普通玩家完全感知不到 |
| 社区驱动 | 分类和规则与速通社区协商(SRC / Discord),不单方面制定 |
| IGT 优先 | 官方认可 IGT(游戏内时间)为主要记录计时,消除平台差异 |
| 防止内置作弊 | 速通模式启用期间,调试秘籍和无敌模式自动禁用 |
| 透明计时逻辑 | IGT 的计算逻辑开源或在文档中完整公开,社区可验证 |
2. 计时系统:RTA vs IGT
| 计时类型 | 全称 | 计算方式 | 用途 |
|---|---|---|---|
| RTA | Real Time Attack | 从新建存档到通关的实际时钟时间(挂钟) | 社区排行榜标准,所有分类通用 |
| IGT | In-Game Time | 游戏主循环运行时间,加载屏幕/暂停/对话期间停止计时 | 消除平台差异(SSD vs HDD 加载时间不同) |
| Load-Removed Time (LRT) | — | RTA 减去所有加载时间(由第三方工具从视频中统计) | 部分分类使用,不由游戏内置 |
IGT 的暂停条件
以下情况 IGT 停止计时:
- 场景加载中(
SceneLoader.IsLoading == true) - 暂停菜单打开
- 过场动画播放中(
CutscenePlayer.IsPlaying == true) - 死亡动画 + 重生动画播放中(从死亡判定到控制权交还玩家)
OnApplicationPause(true)时(切换至后台)
以下情况 IGT 继续计时:
- 对话文本滚动中(玩家可以跳过,对话时间属于游戏时间)
- Boss 被击败后的等待动画(玩家可以移动)
- 地图界面打开时(玩家选择暂停地图查看,不属于强制中断)
3. SpeedrunTimer 实现
namespace BaseGames.Speedrun
{
[CreateAssetMenu(menuName = "BaseGames/Speedrun/SpeedrunTimer")]
public class SpeedrunTimer : ScriptableObject
{
// 当前 Session 的 IGT 累计(毫秒精度)
[field: SerializeField, ReadOnly]
public long CurrentIGTMs { get; private set; }
// 本 Session 开始时的 UTC 时间戳(用于 RTA 计算)
public DateTimeOffset RTAStart { get; private set; }
public bool IsRunning { get; private set; }
public bool IsPaused { get; private set; }
private long _segmentStartMs;
public void StartRun()
{
CurrentIGTMs = 0;
RTAStart = DateTimeOffset.UtcNow;
IsRunning = true;
IsPaused = false;
}
/// <summary>
/// 每帧由 SpeedrunService MonoBehaviour 调用
/// </summary>
public void Tick(float unscaledDeltaTime)
{
if (!IsRunning || IsPaused) return;
CurrentIGTMs += (long)(unscaledDeltaTime * 1000f);
}
/// <summary>
/// 暂停 IGT(加载/死亡/暂停菜单)
/// </summary>
public void Pause() => IsPaused = true;
public void Resume() => IsPaused = false;
public void EndRun()
{
IsRunning = false;
IsPaused = false;
}
// 格式化输出(HUD 显示用)
public string FormatIGT()
{
var ts = TimeSpan.FromMilliseconds(CurrentIGTMs);
return ts.Hours > 0
? $"{ts.Hours:D1}:{ts.Minutes:D2}:{ts.Seconds:D2}.{ts.Milliseconds / 10:D2}"
: $"{ts.Minutes:D2}:{ts.Seconds:D2}.{ts.Milliseconds / 10:D2}";
}
public string FormatRTA()
{
var elapsed = DateTimeOffset.UtcNow - RTAStart;
return elapsed.Hours > 0
? $"{elapsed.Hours:D1}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}"
: $"{elapsed.Minutes:D2}:{elapsed.Seconds:D2}";
}
}
}
SpeedrunService(MonoBehaviour 驱动层)
public class SpeedrunService : MonoBehaviour
{
[SerializeField] SpeedrunTimer _timer;
[SerializeField] SpeedrunConfigSO _config;
// 订阅场景加载事件、死亡事件、暂停事件
void OnEnable()
{
SceneLoader.OnLoadStart += () => _timer.Pause();
SceneLoader.OnLoadComplete += () => _timer.Resume();
PlayerEvents.OnDeath += () => _timer.Pause();
PlayerEvents.OnRespawned += () => _timer.Resume();
UIEvents.OnPauseMenuOpen += () => _timer.Pause();
UIEvents.OnPauseMenuClose += () => _timer.Resume();
}
void Update()
{
if (_config.speedrunModeActive)
_timer.Tick(Time.unscaledDeltaTime);
}
}
4. 速通分类(Category)
| 分类名 | 简称 | 定义 | 结束条件 |
|---|---|---|---|
| Any% | any | 以最快速度通关,不限手段 | 任意结局的结局动画结束 |
| True% | true | 以最快速度达成 True Ending | True Ending 的结局动画结束 |
| 100% | 100 | 完成度达到 100.0% 后通关 | 全收集 + 任意结局 |
| Low% | low | 不主动拾取任何升级道具后通关(仅基础能力) | Any% 结束条件,但跑者自行遵守不捡升级的规则 |
| NG+ | ngp | NG+3 环境下以最快速度通关 | NG+3 通关 |
分类配置存储在 SpeedrunConfigSO.categories(List<SpeedrunCategorySO>),可在运行时从分类列表中选择。
5. 分段计时(Split)
5.1 预设 Split 节点
| Split ID | 触发条件 | 对应游戏节点 |
|---|---|---|
split_forest_boss |
Forest Boss 首次击败 | 森林区域 Boss 击倒 |
split_cave_enter |
Cave 区域首次进入 | 进入地穴场景 |
split_cave_boss |
Cave Boss 击败 | 地穴 Boss 击倒 |
split_ability_dash |
获得冲刺能力 | ability_unlocked (Dash) |
split_ruins_boss |
Ruins Boss 击败 | 废墟 Boss 击倒 |
split_abyss_enter |
进入深渊 | 进入 Abyss 场景 |
split_abyss_boss |
Abyss Boss 击败 | 深渊 Boss 击倒 |
split_true_end_req |
满足 True Ending 条件 | EndingGate 判断为 TrueEnding |
split_final_boss |
最终 Boss 进入阶段3 | 最终 Boss Phase3 开始 |
split_finish |
通关 | 结局动画触发 |
5.2 SplitRecord
public struct SplitRecord
{
public string SplitId;
public long IGTAtSplit; // ms
public long RTAAtSplit; // Unix ms
public long SegmentTime; // 距上一个 Split 的 IGT
}
分段数据在 Session 结束后保存到 {persistentDataPath}/Speedrun/run_YYYY-MM-DD.json,不占用主存档槽。
6. 速通 HUD
速通模式激活时,主 HUD 顶部中央显示 SpeedrunHUD(参见 10_UISystem §20):
┌────────────────────────────────────────────────────────────────┐
│ IGT 00:42:17.83 RTA 00:45:31 Split cave_boss +00:03 │
└────────────────────────────────────────────────────────────────┘
- IGT:主计时,大字显示
- RTA:旁边小字,灰色
- Split:最近一个 Split 与 PB(个人最佳)的差值(绿色=快于PB / 红色=慢于PB)
- 若与 Livesplit 联动(§7),HUD 可隐藏,改由 Livesplit 显示
HUD 位置与可见性选项
| 选项 | 默认值 |
|---|---|
| 位置 | 顶部居中 |
| 显示 RTA | 是 |
| 显示 Split 差值 | 是 |
| 透明度 | 80% |
| 字体大小 | 中 |
以上选项在设置→速通选项菜单中可调整(独立于主游戏设置)。
7. Livesplit 集成
Livesplit 是速通社区最常用的外部计时工具,可通过 Livesplit Server 插件与游戏通信。
7.1 接入方式
// 可选功能,仅 PC 平台,非控制台平台忽略
public class LivesplitConnector : MonoBehaviour
{
[SerializeField] SpeedrunConfigSO _config;
TcpClient _client;
StreamWriter _writer;
void Awake()
{
if (!_config.livesplitIntegration) return;
TryConnect();
}
async void TryConnect()
{
try
{
_client = new TcpClient();
await _client.ConnectAsync("127.0.0.1", 16834); // Livesplit Server 默认端口
_writer = new StreamWriter(_client.GetStream()) { AutoFlush = true };
}
catch { /* Livesplit 未运行时静默忽略 */ }
}
public void SendSplit() => SendCommand("split");
public void SendReset() => SendCommand("reset");
public void SendStart() => SendCommand("starttimer");
private void SendCommand(string cmd) => _writer?.WriteLine(cmd);
}
7.2 自动 Split 发送
在每个 SpeedrunSplitTrigger 组件的 OnSplitTriggered 事件中调用 LivesplitConnector.SendSplit()。
8. 速通规则合规性设计
游戏内置的功能需满足速通社区的合规要求:
| 规则 | 游戏端实现 |
|---|---|
| 不允许使用 Debug 菜单 | 速通模式激活时,42_DebugCheatSystem 强制禁用所有秘籍 |
| 不允许使用无敌模式 | SpeedrunService 在 Awake 中检查并重置无敌状态 |
| 允许 Sequence Break | 不封堵绕过 AbilityGate 的路径(速通的乐趣之一),但 49_AntiSoftlockSystem 仍然工作 |
| 允许跳过对话 | 所有对话均可按键跳过(默认行为,见 15_DialogueSystem) |
| IGT 不包含 Loading 时间 | 由 §2 中的暂停条件保证 |
| 随机数种子透明 | 伪随机使用固定种子 UnityEngine.Random.InitState 时,种子值在设置界面可见 |
9. 存档数据分离
速通记录独立于主存档,存储在专用位置:
{persistentDataPath}/Speedrun/
├── pb_any.json ← 各分类的个人最佳(不与主存档互通)
├── pb_true.json
├── pb_100.json
├── run_2026-04-01_01.json ← 单次跑的完整分段记录
└── run_2026-04-01_02.json
速通 PB 数据不保存在 SaveData 中,防止:
- NG+ 或新存档操作意外重置 PB
- 主存档损坏时 PB 也一并丢失
成就系统集成(见 46_PlatformIntegration §5):速通通关触发速通专属成就(如"Sub-30分钟通关"),成就数据通过平台 SDK 保存(Steam/Nintendo),不依赖游戏内速通存档。
10. 速通社区沟通规范
| 时间节点 | 行动 |
|---|---|
| 封测阶段 | 邀请 1~2 名知名速通玩家参与封测,提前发现计时漏洞 |
| 首发前 2 周 | 在 speedrun.com 上创建游戏页面,与版主协商分类定义 |
| 首发日 | 发布官方速通指南(含 IGT 定义、暂停条件说明)到 SRC 论坛 |
| 重大补丁 | 如果补丁修复了影响速通的 glitch,提前通知社区,等待社区讨论是否区分旧/新版本 |
| 分类变更 | 不单方面增减分类,必须与 SRC 版主协商 |
本文档版本 1.0 · 2026-04 · 关联 52_CompletionEndingDesign / 46_PlatformIntegration / 42_DebugCheatSystem