From 662ba936aaa1fa4f5e2bd3ca5345ee240ddec329 Mon Sep 17 00:00:00 2001 From: ritenseki Date: Fri, 7 Aug 2026 14:19:00 +0800 Subject: [PATCH] Remove StageAnimationPlayer: a stand-in with guessed timing is worse than nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its premise was that a single clip is an unambiguous instruction to play. It is not — it means the object has one state, while whether that state is running was AnimationObjectController's per-frame decision, and that class has zero serialized fields, so no amount of dumping recovers it. What was left was policy invented here: the wash lights whipping through a 2-second sweep the original does not perform, and a mirror ball turning at a rate nothing can validate. After today's exclusions it took over exactly one object on live10149. Two hundred lines to spin one ball at a speed we cannot check, in a subsystem that also claims — by existing — that this area has been dealt with. That last part is the real cost: a wrong stand-in reads as done and stops anyone looking again. So the stage's 283 Animation components now play nothing, deliberately. The disco balls do not turn, the wash lights do not sweep, the neon signs do not move. That is the honest state given what is recoverable, and it is the prefab author's pose rather than our invention. The file is renamed to StageMissingScriptCensus.cs, keeping only the census that was sharing it — that one is a genuine progress meter for rebuilt signatures and never had anything to do with animation playback. One finding survives the deletion and is written into LIVE_TRACKS.md so a future attempt starts from it rather than rediscovering it: do not drive stage animation with Animation.Play(), which runs on Unity's own clock and desyncs the moment the live is paused or the scrubber moves. Set AnimationState.time from LiveTimelineControl.currentLiveTime (seconds) and Sample(). The same entry records why the feature is off, what would be needed to revive it (per-stage calibration against reference footage, or IL2CPP metadata), and that the clip assets themselves are fully readable — which is the trap, since knowing the asset says nothing about when the original plays it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UzXXkLzVC8pNA3JjyUGMbq --- .../Gallop/Live/StageAnimationPlayer.cs | 211 ------------------ .../umamusume/Gallop/Live/StageController.cs | 8 +- .../Gallop/Live/StageMissingScriptCensus.cs | 50 +++++ ....meta => StageMissingScriptCensus.cs.meta} | 0 CLAUDE.md | 33 +-- LIVE_AUDIT_2026-08-05.md | 2 +- LIVE_DEV_MAP.md | 12 +- LIVE_TRACKS.md | 33 +++ 8 files changed, 113 insertions(+), 236 deletions(-) delete mode 100644 Assets/Scripts/umamusume/Gallop/Live/StageAnimationPlayer.cs create mode 100644 Assets/Scripts/umamusume/Gallop/Live/StageMissingScriptCensus.cs rename Assets/Scripts/umamusume/Gallop/Live/{StageAnimationPlayer.cs.meta => StageMissingScriptCensus.cs.meta} (100%) 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)。在那之前**不做**。 + ### 与时间轴无关的渲染问题 | 现象 | 状态 |