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

12 KiB
Raw Permalink 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 联动§7HUD 可隐藏,改由 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