# 58 · 速通模式系统(Speedrun Mode System)
> **命名空间** `BaseGames.Speedrun`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.World`(SceneLoader)· `BaseGames.UI`(HUD)
> **关联** 10_UISystem · 46_PlatformIntegration · 52_CompletionEndingDesign · 42_DebugCheatSystem
---
## 目录
1. [速通模式设计原则](#1-速通模式设计原则)
2. [计时系统:RTA vs IGT](#2-计时系统rta-vs-igt)
3. [SpeedrunTimer 实现](#3-speedruntimerimplementation)
4. [速通分类(Category)](#4-速通分类category)
5. [分段计时(Split)](#5-分段计时split)
6. [速通 HUD](#6-速通-hud)
7. [Livesplit 集成](#7-livesplit-集成)
8. [速通规则合规性设计](#8-速通规则合规性设计)
9. [存档数据分离](#9-存档数据分离)
10. [速通社区沟通规范](#10-速通社区沟通规范)
---
## 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 实现
```csharp
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;
}
///
/// 每帧由 SpeedrunService MonoBehaviour 调用
///
public void Tick(float unscaledDeltaTime)
{
if (!IsRunning || IsPaused) return;
CurrentIGTMs += (long)(unscaledDeltaTime * 1000f);
}
///
/// 暂停 IGT(加载/死亡/暂停菜单)
///
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 驱动层)
```csharp
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`),可在运行时从分类列表中选择。
---
## 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
```csharp
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 接入方式
```csharp
// 可选功能,仅 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*