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

12 KiB
Raw Blame History

58 · 速通模式系统(Speedrun Mode System

命名空间 BaseGames.Speedrun
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.WorldSceneLoader)· BaseGames.UIHUD
关联 10_UISystem · 46_PlatformIntegration · 52_CompletionEndingDesign · 42_DebugCheatSystem


目录

  1. 速通模式设计原则
  2. 计时系统:RTA vs IGT
  3. SpeedrunTimer 实现
  4. 速通分类(Category
  5. 分段计时(Split
  6. 速通 HUD
  7. Livesplit 集成
  8. 速通规则合规性设计
  9. 存档数据分离
  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 实现

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 驱动层)

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.categoriesList<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