Files
zeling_v2/Docs/Design/58_SpeedrunModeSystem.md
2026-05-08 11:04:00 +08:00

329 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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;
}
/// <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}";
}
}
}
```
### SpeedrunServiceMonoBehaviour 驱动层)
```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<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
```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 联动§7HUD 可隐藏,改由 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*