diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs b/Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs deleted file mode 100644 index 47fb9cd..0000000 --- a/Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace Gallop.Live -{ - /// - /// 舞台自带 的播放。 - /// - /// 为什么需要:live10149 的舞台 prefab 引用了 11 个脚本类,UmaViewer 里**只有 - /// StageController 存在**,另外 10 个(AnimationObjectController、MirrorBallProjector、 - /// MirrorReflection、CustomLensFlare、UnityLensFlareController、CustomProjector、 - /// LightProjection、WashLightController、BillboardController、ShaderParamController) - /// 全部缺失,那些 MonoBehaviour 反序列化后脚本引用为空,永远不执行。 - /// - /// 其中 AnimationObjectController 的缺席后果最直接:舞台上 283 个 Animation 组件 - /// **全部** `playAutomatically = true`,但**默认 clip(m_Animation)全部为空**。 - /// Unity 的 playAutomatically 播的是默认 clip,默认 clip 为空就什么都不播 —— - /// 原版是靠那个脚本显式 Play("clip名") 的。于是整个舞台的动画一个都没动: - /// 迪斯科灯球不转、wash 灯不扫、霓虹灯牌不动。 - /// - /// **只处理唯一解的情形。** clip 数量分布: - /// 1 个 clip → 4 个对象(mirrorball_flarelight、wash_ground_b/c/d),全是 `*_loop_000`, - /// 没有歧义,直接播。 - /// 3 个 clip → `loop_000` / `up_000` / `loop_001`,是「过渡 + 两个循环态」的状态机, - /// 选哪个、何时切换都没有依据。 - /// 5 个 clip → 272 个对象(占 96%,多半是观众席),同样未知。 - /// 后两类只打一次日志把 clip 名列出来,**不猜**。选择规则逆出来之后再补。 - /// - /// 播放时机走 OnEnable 而不是初始化时一次性 Play:受时间轴控制的灯(名字含 - /// blinklight/_wash_/laser/spotlight3d)初始是 inactive 的,要等轨道把它们打开, - /// 在 inactive 对象上调 Play() 不会生效。镜面球容器 - /// pfb_env_live10149_blinklight_mirrorball_flarelight 正好就是这种情况。 - /// - /// ── 2026-08-07 收窄:wash 灯的扫动 clip 不再 free-run ── - /// - /// 「只有一个 clip」说明的是**这个物件只有一个状态**,不等于**这个状态一直在播** —— - /// 后者正是缺失的 `AnimationObjectController` 负责决定的事,我把两者混为一谈了。 - /// - /// 实测反例:`wash_ground_b/c/d` 的单 clip 是 `swing_*` 节点 90°→0°→90° 的往返扫动, - /// 2.0 s 一个来回(12 盏完全同步)。放出来就是一排灯在疯狂摆头,而**原版 MV 里 - /// 地面 wash 灯是不扫的,只闪**。所以这三个不接管。 - /// - /// 镜面球保留:它的 clip 是绕 Y 单调转到 −359°,是自明的常转 idle,原版里球也确实在转。 - /// (转速是否 1.0 倍仍无依据,见 CLAUDE.md。) - /// - /// ⚠ 这条排除是**看参考视频得出的观察**,不是从 bundle 数据推出来的。 - /// 如果哪个舞台的 wash 灯真的该扫,需要新的证据才能放开,别照着这行反推。 - /// - public static class StageAnimationPlayer - { - /// - /// 扫动类灯具的名字特征。命中的不接管 —— 见类注释。 - /// 和 的 `_wash_` 判据同源,那里也是按名字识别 wash 灯的。 - /// - private static bool IsSweepRig(string name) => name.Contains("_wash_"); - - public static void Setup(StageController stage) - { - if (stage == null) return; - - int single = 0, ambiguous = 0, empty = 0, sweepSkipped = 0; - var loggedClipSets = new HashSet(); - - foreach (var anim in stage.GetComponentsInChildren(true)) - { - if (anim == null || !anim.playAutomatically) continue; - - if (IsSweepRig(anim.gameObject.name)) - { - if (sweepSkipped++ == 0) - Debug.Log($"[StageAnim] 跳过扫动类灯具(如 '{anim.gameObject.name}'):" + - "单 clip 只说明它只有一个状态,不说明该一直播;原版地面 wash 灯不扫只闪。"); - continue; - } - - var clips = new List(); - foreach (AnimationState st in anim) - { - if (st != null && st.clip != null) clips.Add(st.clip); - } - - if (clips.Count == 0) { empty++; continue; } - - if (clips.Count == 1) - { - // 唯一解。注意**不要**设 anim.clip、也不要 Play(): - // playAutomatically 是 true,一旦有了默认 clip,Unity 会用自己的时钟 - // 自动播放,于是 live 暂停/拖动进度条时舞台照转。必须由时间轴驱动。 - anim.Stop(); - var driver = anim.gameObject.GetComponent(); - if (driver == null) driver = anim.gameObject.AddComponent(); - driver.Bind(anim, clips[0]); - single++; - // 打出时长:镜面球那条 clip 离线量到 2.0s 转满 360°(30 RPM), - // 如果运行时 length 不是 2.0,说明转速问题出在 clip 解析而不是编排。 - Debug.Log($"[StageAnim] 接管 '{anim.gameObject.name}' ← {clips[0].name} " + - $"length={clips[0].length:F3}s wrapMode={clips[0].wrapMode}"); - continue; - } - - ambiguous++; - var names = new List(); - foreach (var c in clips) names.Add(c.name); - string key = string.Join(",", names); - if (loggedClipSets.Add(key)) - { - Debug.Log($"[StageAnim] '{anim.gameObject.name}' 有 {clips.Count} 个 clip," + - $"选择规则未知,**未播放**:{key}"); - } - } - - Debug.Log($"[StageAnim] 单 clip 已接管 {single} 个;扫动类跳过 {sweepSkipped} 个;" + - $"多 clip 未处理 {ambiguous} 个;无 clip {empty} 个"); - } - } - - /// - /// 缺失脚本的运行时普查,当进度表用:每按原签名实现一个原版脚本, - /// 「脚本丢失」就该下降对应的实例数(已验证 BillboardController 令 892→728,正好 −164)。 - /// - /// 签名清单、各类实例数、以及「为什么建同名类就能收到字段」见 CLAUDE.md - /// 「用原签名重建脚本」。一句话:Unity 按**类名 + namespace + 程序集名**解析 MonoScript, - /// 而 `umamusume.asmdef` 的程序集名与 bundle 里写的完全一致 —— 那 892 个空槽位 - /// 只是没人写,不是读不到。(这条最初记反了,是本普查的结果自己推翻的: - /// 存活的 `AssetHolder` 和 `StageController` 正是靠这个机制绑上的。) - /// - public static class StageMissingScriptCensus - { - public static void Dump(StageController stage) - { - if (stage == null) return; - - int slots = 0, missing = 0, alive = 0; - var aliveTypes = new Dictionary(); - - foreach (var tr in stage.GetComponentsInChildren(true)) - { - var comps = tr.GetComponents(); - foreach (var c in comps) - { - // MonoBehaviour 槽位里脚本丢失时,元素为 null。 - if (c == null) { slots++; missing++; continue; } - if (c is MonoBehaviour) - { - slots++; alive++; - string n = c.GetType().Name; - aliveTypes.TryGetValue(n, out int k); - aliveTypes[n] = k + 1; - } - } - } - - var parts = new List(); - foreach (var kv in aliveTypes) parts.Add($"{kv.Key}x{kv.Value}"); - Debug.Log($"[StageScripts] MonoBehaviour 槽位 {slots}:脚本丢失 {missing},存活 {alive}" + - (parts.Count > 0 ? $"({string.Join(", ", parts)})" : "") + - "。基线 892(live10149);每按签名实现一个类,丢失数应下降对应实例数。" + - "已验证:BillboardController 令 892→728(-164)。"); - } - } - - /// - /// 用 live 时间轴的时间手动采样 clip。 - /// - /// **不能用 Animation.Play()** —— 那跑的是 Unity 自己的时钟,结果是:live 暂停了舞台 - /// 照转、拖动进度条不跟随、播放速度和歌无关。舞台动画属于演出编排的一部分, - /// 必须和 同源, - /// 所以这里每帧设 AnimationState.time 再 Sample()。 - /// - /// 时间取 currentLiveTime,暂停时它不前进,拖动时它跳变,两种情况都自动正确。 - /// - public class StageAnimationAutoPlay : MonoBehaviour - { - private Animation _anim; - private AnimationClip _clip; - private string _clipName; - private float _length; - private bool _loop; - - public void Bind(Animation anim, AnimationClip clip) - { - _anim = anim; - _clip = clip; - _clipName = clip != null ? clip.name : null; - _length = clip != null ? clip.length : 0f; - // clip 自身的 wrapMode 决定循环与否;live10149 的 22 个 clip 里 16 个是 Loop。 - _loop = clip != null && (clip.wrapMode == WrapMode.Loop || clip.wrapMode == WrapMode.PingPong); - } - - private void LateUpdate() - { - if (_anim == null || _clip == null || _length <= 0f) return; - - var ctrl = Director.instance != null ? Director.instance._liveTimelineControl : null; - if (ctrl == null) return; - - float t = ctrl.currentLiveTime; - // TODO: PingPong 按 Loop 处理,来回摆的那一半没实现(live10149 上没有 PingPong clip)。 - float sampleTime = _loop ? Mathf.Repeat(t, _length) : Mathf.Clamp(t, 0f, _length); - - var state = _anim[_clipName]; - if (state == null) return; - state.enabled = true; - state.weight = 1f; - state.time = sampleTime; - _anim.Sample(); - // 采样完就关掉,避免 Animation 自己再按 Unity 时钟推进一次。 - state.enabled = false; - } - } -} diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageController.cs b/Assets/Scripts/umamusume/Gallop/Live/StageController.cs index 0e06060..4bdfab0 100644 --- a/Assets/Scripts/umamusume/Gallop/Live/StageController.cs +++ b/Assets/Scripts/umamusume/Gallop/Live/StageController.cs @@ -110,8 +110,12 @@ namespace Gallop.Live // 光柱/闪光类 shader 的混合状态被 bundle 材质里的 URP Lit 属性表压成了不透明,先修回来。 RestoreAuthoredBlendState(); - // 舞台自带 Animation 的播放(原版由缺失的 AnimationObjectController 负责)。 - StageAnimationPlayer.Setup(this); + // ⚠ 舞台自带的 283 个 Animation **有意不播**,不是漏了。 + // 它们全部 playAutomatically=true 但默认 clip 为空,原版靠 AnimationObjectController + // 显式 Play("clip名");而那个类实测**零序列化字段**,纯行为,方法体拿不到 —— + // 「播哪个 / 何时播 / 多快」没有任何数据能还原。曾写过一版替代品,结果是 + // wash 灯以 2 秒一个来回疯狂摆头、镜面球转速无从校验,遂整体撤除。 + // 理由与复活条件见 LIVE_TRACKS.md「缺 ground truth,已禁用」。 // 缺失脚本普查:每按签名实现一个原版脚本,「脚本丢失」计数就该下降对应实例数。 StageMissingScriptCensus.Dump(this); diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs b/Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs new file mode 100644 index 0000000..b6f9aca --- /dev/null +++ b/Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Gallop.Live +{ + /// + /// 缺失脚本的运行时普查,当进度表用:每按原签名实现一个原版脚本, + /// 「脚本丢失」就该下降对应的实例数(已验证 BillboardController 令 892→728,正好 −164)。 + /// + /// 签名清单、各类实例数、以及「为什么建同名类就能收到字段」见 CLAUDE.md + /// 「用原签名重建脚本」。一句话:Unity 按**类名 + namespace + 程序集名**解析 MonoScript, + /// 而 `umamusume.asmdef` 的程序集名与 bundle 里写的完全一致 —— 那 892 个空槽位 + /// 只是没人写,不是读不到。(这条最初记反了,是本普查的结果自己推翻的: + /// 存活的 `AssetHolder` 和 `StageController` 正是靠这个机制绑上的。) + /// + public static class StageMissingScriptCensus + { + public static void Dump(StageController stage) + { + if (stage == null) return; + + int slots = 0, missing = 0, alive = 0; + var aliveTypes = new Dictionary(); + + foreach (var tr in stage.GetComponentsInChildren(true)) + { + var comps = tr.GetComponents(); + foreach (var c in comps) + { + // MonoBehaviour 槽位里脚本丢失时,元素为 null。 + if (c == null) { slots++; missing++; continue; } + if (c is MonoBehaviour) + { + slots++; alive++; + string n = c.GetType().Name; + aliveTypes.TryGetValue(n, out int k); + aliveTypes[n] = k + 1; + } + } + } + + var parts = new List(); + foreach (var kv in aliveTypes) parts.Add($"{kv.Key}x{kv.Value}"); + Debug.Log($"[StageScripts] MonoBehaviour 槽位 {slots}:脚本丢失 {missing},存活 {alive}" + + (parts.Count > 0 ? $"({string.Join(", ", parts)})" : "") + + "。基线 892(live10149);每按签名实现一个类,丢失数应下降对应实例数。" + + "已验证:BillboardController 令 892→728(-164)。"); + } + } +} diff --git a/Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs.meta b/Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs.meta similarity index 100% rename from Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs.meta rename to Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs.meta diff --git a/CLAUDE.md b/CLAUDE.md index 107f13b..45fb103 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,7 +174,7 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的** | namespace | 类 | live10149 实例数 | 状态 / 已 dump 到的字段 | |---|---|---|---| -| `Gallop.Live` | `AnimationObjectController` | 282 | ❌ **别建,建了也没用**:实测序列化字段 **一个都没有**(282 个实例全空)。重建只会让「脚本丢失」892→446,拿到 0 bit 新信息 —— 它是纯行为类,播哪个 clip / 何时播 / 多快全在拿不到的方法体里。动画播放已由 `StageAnimationPlayer` 顶上,见下 | +| `Gallop.Live` | `AnimationObjectController` | 282 | ❌ **别建,建了也没用**:实测序列化字段 **一个都没有**(282 个实例全空)。重建只会让「脚本丢失」892→446,拿到 0 bit 新信息 —— 它是纯行为类,播哪个 clip / 何时播 / 多快全在拿不到的方法体里。顶替版已于 2026-08-07 撤除,舞台 Animation 现在**有意不播**,见下 | | `Gallop.Live` | `UnityLensFlareController` | 205 | ❌ 只有 1 个字段 `_enableAngleDegree`(0),本体不在这里 | | `Gallop.Live` | `BillboardController` | 164 | ✅ 已建(字段已读到,朝向行为待定)| | `Gallop.Live` | `WashLightController` | 27 | ❌ 24 个字段全是具体数值 + 两张投影贴图(`_projectionTexture`/`_cameraProjectionTexture`),缺的是 URP 下的投影实现 | @@ -219,17 +219,17 @@ MonoBehaviour 实例脚本为空。**但这些脚本是可以「接管」的** 而生效摄像机随 CameraSwitcher 每帧变 —— 切换后面片朝的是观众看不见的那台, 那台还在自己动,看上去就是无缘无故乱转。**凡是引用「当前摄像机」的舞台脚本都要注意这条。** -**舞台自带 Animation 的播放**(`StageAnimationPlayer`,顶替缺失的 `AnimationObjectController`): -283 个 `Animation` 组件全部 `playAutomatically = true` 但默认 clip 为空,Unity 因此什么都不播。 -播放不走 `Animation.Play()` —— 那用的是 Unity 自己的时钟,暂停和拖动进度条都会脱节; -改为每帧按 `LiveTimelineControl.currentLiveTime` 设 `AnimationState.time` 再 `Sample()`, -与演出时间同源。3 个 / 5 个 clip 的状态机选择规则没有依据,只打日志不猜。 +**舞台自带 Animation 有意不播**(283 个组件,2026-08-07 撤除了替代实现): +它们全部 `playAutomatically = true` 但默认 clip 为空,原版靠 `AnimationObjectController` +显式 `Play()`,而那个类**零序列化字段**,纯行为,方法体拿不到 —— 「播哪个 / 何时播 / 多快」 +没有任何数据能还原。写过一版 `StageAnimationPlayer` 顶替,策略是「只有一个 clip 就播」, +错在把「这物件只有一个**状态**」当成「这个状态**一直在播**」(后者正是那个缺失的类每帧做的 +决定):wash 灯 2 秒一个来回疯狂摆头(原版不扫只闪),镜面球转速无从校验。已整体撤除。 +**完整理由与复活条件见 `LIVE_TRACKS.md` →「舞台自带 Animation 为什么不播」专条。** -⚠ **「只有一个 clip」≠「这个 clip 一直在播」。** 前者说明这个物件只有一个**状态**, -后者是 `AnimationObjectController` 每帧做的**决定**。最初的接管规则把两者混为一谈, -结果 `wash_ground_b/c/d` 的扫动 clip(`swing_*` 节点 90°→0°→90°、2.0 s 一个来回、12 盏同步) -被 free-run,一排地面 wash 灯疯狂摆头 —— 而原版 MV 里它们不扫、只闪。现已按 `_wash_` 跳过。 -镜面球保留(单调转到 −359°,自明的常转 idle)。 +⚠ 撤除时唯一保留下来的结论:**驱动舞台动画不要用 `Animation.Play()`**, +那跑 Unity 自己的时钟,暂停/拖动进度条都会脱节;应每帧把 `AnimationState.time` 设成 +`LiveTimelineControl.currentLiveTime`(秒)再 `Sample()`。将来重做直接用这条。 ### 🔎 舞台上「行为不对」的第一嫌疑:缺组件,不是解析错 @@ -255,13 +255,14 @@ MonoBehaviour 实例是空的**(基线 892,已补 `BillboardController` −1 推论零:**先 dump 一眼那个缺失组件有没有字段,再决定值不值得重建。** `AnimationObjectController` 实测**零字段**(282 个实例全空),所以重建它拿不到任何信息, -`StageAnimationPlayer` 那些时机问题不可能靠「把原类建出来」解决 —— 这个方向是死的, +舞台动画的那些时机问题不可能靠「把原类建出来」解决 —— 这个方向是死的, 别再试。反过来 `CustomLensFlare` 字段齐全(还牵出 `LensFlareData`),就非常值得建。 判断只要一条命令,不要凭实例数多就以为收益大。 推论二:**顶替缺失组件时,要把「我在替谁做决定」写清楚**,并且区分 「从 bundle 数据推出来的」和「看参考视频观察到的」—— 后者是有效证据,但不是 ground truth, -换个舞台可能就不成立(`StageAnimationPlayer.IsSweepRig` 的注释就是按这个写的)。 +换个舞台可能就不成立。(反面教材:舞台动画的 `_wash_` 排除规则就是这么来的, +后来连同整个替代实现一起撤掉了 —— 观察能证伪一个实现,但支撑不起一个实现。) 反例也要记住:**并非所有舞台画面问题都是缺组件。** 光柱渲染成黑墙那次, 根因是材质被盖了 URP Lit 的属性表、`_DstBlend` 从 One 变成 Zero, @@ -306,9 +307,9 @@ MonoBehaviour 实例是空的**(基线 892,已补 `BillboardController` −1 6 条 quaternion 曲线(`mirrorball_a_000` + `b_000..004`),7 个关键帧, **恒定 −179.50°/s 绕 Y**(每个关键帧算出来都是这个数),2.0 s 一圈,wrapMode = Loop。 末帧 −359° 而不是 −360°,是为了循环接缝不重复一帧。 -- 这条 clip 正好是 `StageAnimationPlayer` 已接管的单-clip 唯一解之一,采样用的 - `currentLiveTime` 单位是秒(帧号 = ×60),所以**没有单位换算错误**, - 我们放出来就是 clip 写的 2.0 s 一圈。 +- 曾接管过这条 clip,采样用的 `currentLiveTime` 单位是秒(帧号 = ×60),**没有单位换算错误**, + 放出来就是 clip 写的 2.0 s 一圈 —— 但实机仍然偏快,这正是下面第 2 条教训的由来。 + 该实现已于 2026-08-07 撤除,球现在不转。 两条教训,方向相反,都要记: diff --git a/LIVE_AUDIT_2026-08-05.md b/LIVE_AUDIT_2026-08-05.md index b6e3be6..f7c364b 100644 --- a/LIVE_AUDIT_2026-08-05.md +++ b/LIVE_AUDIT_2026-08-05.md @@ -541,7 +541,7 @@ A5 一并做掉;剩 6(B 类残余:WashLight/Laser 的 `r.materials`)与 | 事项 | 结果 | |---|---| -| 舞台 Animation 全都不播 | 283 个 `Animation` 组件 `playAutomatically = true` 但默认 clip 为空,Unity 什么都不播;原版靠 `AnimationObjectController` 显式 `Play("clip名")`。已由 `StageAnimationPlayer` 顶上,**只接管单 clip 的唯一解情形**(4 个对象),3/5 个 clip 的状态机选择规则没依据,只打日志 | +| 舞台 Animation 全都不播 | 283 个 `Animation` 组件 `playAutomatically = true` 但默认 clip 为空,Unity 什么都不播;原版靠 `AnimationObjectController` 显式 `Play("clip名")`。**曾写 `StageAnimationPlayer` 顶替,2026-08-07 撤除** —— 那个类实测零序列化字段,「播哪个/何时播/多快」无从还原,顶替版把 wash 灯播成 2 秒一个来回。现状是有意不播,理由见 `LIVE_TRACKS.md` 专条 | | 按签名重建原版脚本 | 打通了。类名 + namespace + 程序集名对上就能让 Unity 真正反序列化字段 —— `BillboardController` 令 `[StageScripts]` 丢失数 892→728(正好 −164)。**但只拿得到数据,拿不到方法体**:`_rotationType` 的枚举语义随 IL2CPP 元数据一起没有 | | 地板发白(C4) | 查明是 `mirror_a`,实现 `Gallop.MirrorReflection` 后解决。详见 C4 条目 —— 这条的过程比结论值钱 | | BlinkLight 调色板槽位 | 槽位号 = `lightNNN_`/`alphaNNN_` 名字前缀,7 组实测无一例外。顺带修掉镜面球球体被 BlinkLight 涂成粉红的问题(`_MulColor0` 是 BgColor1 的通道,BlinkLight 排在它之后,抢了) | diff --git a/LIVE_DEV_MAP.md b/LIVE_DEV_MAP.md index 40b84ce..093690f 100644 --- a/LIVE_DEV_MAP.md +++ b/LIVE_DEV_MAP.md @@ -108,7 +108,7 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 |------|------| | `Gallop.MirrorReflection` | ✅ 已建并实现。第二摄像机 + 反射矩阵 + 斜投影 → RT → `_ReflectionTex`。**顺带解决了长期的「舞台地板发白」** —— `mirror_a` 的 `_ReflectionTex` 从没被赋值,Unity 代入白贴图 × 默认 `_ReflectionRate = 1.0` | | `Gallop.Live.BillboardController` | ⚠️ 已建,字段可读(`[StageScripts]` 丢失数 892→728)。朝向行为写过一版,实机是「一直转来转去而那东西不该转」,已默认关闭(`EnableRotation = false`)——`_rotationType` 语义和 up 轴取法都无依据。同批修掉一个确定 bug:目标摄像机被缓存,而生效摄像机随 CameraSwitcher 每帧变 | -| `Gallop.Live.AnimationObjectController` | ❌ **不要建**:实测**零序列化字段**(282 个实例全空),纯行为类,重建拿不到任何信息。其最直接的后果(283 个 `Animation` 一个都不播)由 `StageAnimationPlayer` 顶上:按 `currentLiveTime` 采样而不是 `Animation.Play()`,暂停/拖动进度条都跟随。但**「播哪个 clip」和「该不该播」都是它在决定**,我们只能顶前者——wash 灯扫动 clip 被 free-run 就是栽在后者(已按 `_wash_` 跳过) | +| `Gallop.Live.AnimationObjectController` | ❌ **不要建,也不要顶替**:实测**零序列化字段**(282 个实例全空),纯行为类,重建拿不到任何信息。后果是舞台 283 个 `Animation` 一个都不播(灯球不转/wash 不扫/灯牌不动)——这是**有意接受**的现状。顶替版 `StageAnimationPlayer` 已于 2026-08-07 撤除,理由见 `LIVE_TRACKS.md` 专条 | | `Gallop.RenderPipeline.CustomLensFlare` (+`LensFlareData`) | ❌ 未建,但**数据齐全、语义不受 IL2CPP 阻塞**——`LensFlareData` 是 Unity legacy Flare 的逐字翻版,URP 侧有 `LensFlareComponentSRP` 对应。199+5 个物件,星芒在原版截图里很显眼。**下一个该做的就是它** | | `Gallop.RenderPipeline.MirrorBallProjector` | ❌ 字段已 dump,但 4 个 projector 全在原点、共用一个材质、没有任何指向灯球的引用字段——绑定关系只在方法体里。**卡住,不是没做** | | `Gallop.Live.ShaderParam.ShaderParamController` | ⚠️ 实测是空操作(两个向量都是 (1,1,1,1)),从待办里划掉 | @@ -117,10 +117,10 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 > **镜面球转速:素材确定,倍率仍未知。** live10149 的 4 个 `MirrorBallProjector` 全部 > `MirrorBallIsLoopRotation = 0` / `LoopRotationSpeed = 0`,shader 的 loop-rotation 通路是 > 关的;真正在转的是 clip(恒定 **−179.50°/s 绕 Y**,2.0 s 一圈,7 帧算出来都是这个数), -> `StageAnimationPlayer` 按 `currentLiveTime`(秒)忠实地播它。 -> **但实机观察是「所有迪斯科灯球还是太快」** —— 所以原版要么有播放倍率,要么根本不用这条 -> clip 驱动球体。数据确定的是素材,不是原版怎么用素材。倍率属于不可知常量, -> 要定它只能对着参考视频标定。 +> 曾按 `currentLiveTime`(秒)忠实播过它,**实机观察是「所有迪斯科灯球还是太快」** —— +> 所以原版要么有播放倍率,要么根本不用这条 clip 驱动球体。数据确定的是素材, +> 不是原版怎么用素材;倍率属于不可知常量,要定它只能对着参考视频标定。 +> 2026-08-07 起播放实现整体撤除,球现在**不转**,见上一行的专条链接。 ### 内部系统 | 轨道 | 状态 | @@ -188,7 +188,7 @@ Toon shader 已移植,卡住的主要是全屏后处理和几个逆向未完 |------|------| | `Assets/Scripts/umamusume/Gallop/Live/Director.cs` | 事件订阅 + 所有 handler。写 shader 属性统一走共享的 `PropBlock`,且必须 Get→改→Set | | `Assets/Scripts/umamusume/Gallop/MirrorReflection.cs` | 平面镜反射(原版脚本按签名重建)| -| `Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs` | 舞台 Animation 播放 + 缺失脚本普查(`[StageAnim]` / `[StageScripts]` 日志)| +| `Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs` | 缺失脚本普查(`[StageScripts]` 日志),当重建进度表用 | | `Assets/Scripts/umamusume/Gallop/Live/PostEffect/PostFilmRendererFeature.cs` | PostFilm 的 URP Feature,默认 `_enableRendering = false` | | `Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineControl.cs` | 每帧插值 + 事件触发 | | `Assets/Scripts/umamusume/Gallop/Live/Cutt/LiveTimelineWorkSheet.cs` | 所有轨道字段声明 | diff --git a/LIVE_TRACKS.md b/LIVE_TRACKS.md index df3ca89..7d4bebc 100644 --- a/LIVE_TRACKS.md +++ b/LIVE_TRACKS.md @@ -208,9 +208,42 @@ originSessionId: 319ae839-0292-4b1e-80de-2f3c91b1f40e | 问题 | 为什么查不到 | |---|---| +| **舞台自带 Animation 的播放**(283 个组件,全曲全场景不播) | 见下方专条 | | PostFilm 的 `filmOptionParam` 几何语义 | 游戏自己的 PostFilm shader 不在 `shader` bundle(499 个按名字和属性签名都搜过),应在 app 本体 `resources.assets` | | MonitorControl 的 `dispID` → 资源 | 内容资源族已定位为 `live/uvmovie/gal_uvmovie__`(全库 219 个目录),但 song 1177 只有 `_light` 没有 `_001`,且 `monitorCameraPosKeys` 为 0,两条路都不通 | +#### 专条:舞台自带 Animation 为什么**不播**(2026-08-07 决定撤除实现) + +**现状**:舞台上 283 个 legacy `Animation` 组件一个都不播,这是**有意的**,不是漏了。 +迪斯科灯球不转、wash 灯不扫、霓虹灯牌不动,都属于这一条。 + +**机制**:这些组件全部 `playAutomatically = true`,但默认 clip(`m_Animation`)**全是空的**。 +Unity 的 playAutomatically 播的是默认 clip,为空就什么都不播 —— 原版靠 +`Gallop.Live.AnimationObjectController` 显式 `Play("clip名")`。 + +**为什么补不了**:`AnimationObjectController` 实测 **282 个实例、零序列化字段**。 +它是纯行为类,「播哪个 clip / 该不该播 / 播多快」全在方法体里,而方法体随 IL2CPP 元数据 +一起拿不到(见 `CLAUDE.md` → Blocked)。**按原签名重建它拿不到任何信息** —— +这条路是死的,别被 282 这个实例数勾回去。 + +**素材本身是全可读的**,这正是容易上当的地方:clip 曲线、时长、wrapMode 全能 dump。 +例如镜面球那条是恒定 −179.50°/s 绕 Y、2.0 s 一圈;wash_ground 那条是 `swing_*` 节点 +90°→0°→90°、2.0 s 一个来回、12 盏同步。**但「素材是什么」推不出「原版何时以何速播它」。** + +**试过一版替代品 `StageAnimationPlayer`,已整体撤除。** 它的策略是「只有一个 clip 就播」, +错在把「这物件只有一个**状态**」当成了「这个状态**一直在播**」—— 后者恰恰是那个缺失的类 +每帧做的决定。实机后果:一排地面 wash 灯 2 秒一个来回疯狂摆头(原版是不扫只闪), +镜面球虽然该转但转速无从校验。缩到最后它只让一颗球转,且转得对不对说不清 —— +**一个转错的替代品比不转更糟:既错,又让人以为这块已经做过了。** + +**唯一值得留下的结论**(将来任何人重做都适用): +驱动舞台动画**不要用 `Animation.Play()`** —— 那跑 Unity 自己的时钟,live 暂停时舞台照转、 +拖动进度条不跟随。正确做法是每帧把 `AnimationState.time` 设成 +`LiveTimelineControl.currentLiveTime`(单位是秒)再 `Sample()`,与演出时间同源。 + +**什么条件下可以重做**:拿到参考视频逐首标定(属于经验值,不是 ground truth,且大概率 +一舞台一套),或者 IL2CPP 元数据解锁(见 `CLAUDE.md` → Blocked)。在那之前**不做**。 + ### 与时间轴无关的渲染问题 | 现象 | 状态 |