This commit is contained in:
SoulliesOfficial
2026-06-05 06:47:24 -04:00
parent 3995beeb29
commit 0fb72f5bba
94 changed files with 650754 additions and 4839 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1197,7 +1197,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
serializationData:
SerializedFormat: 0
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
@@ -2074,7 +2074,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
serializationData:
SerializedFormat: 0
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
@@ -5734,14 +5734,20 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
serializationData:
SerializedFormat: 0
SerializedBytes: 2d010e000000700072006f006a0065006300740049006e0066006f005f0042004d002d010b00000073006f006e00670049006e0066006f005f0042004d00
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
SerializationNodes:
- Name: projectInfo_BM
Entry: 6
Data:
- Name: songInfo_BM
Entry: 6
Data:
isLoadedProject: 0
loadedProjectName:
isRecovery: 0

View File

@@ -0,0 +1,23 @@
using System;
namespace Ichni.RhythmGame.Beatmap
{
public abstract class BaseElement_BM
{
public Guid attachedElementGuid;
/// <summary>
/// 从存档类中生成游戏物体
/// </summary>
public abstract void ExecuteBM();
/// <summary>
/// 在AfterInitialize中被调用用于处理GameElement的"需要引用"的物体在此物体后面生成的情况。
/// 注意如果使用此函数需要在ExecuteBM中设置 matchedElement.matchedBM = this;
/// </summary>
public virtual void AfterExecute()
{
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ab4e12be376b08f418bd2d7a7d78aec6

View File

@@ -297,7 +297,7 @@ namespace Ichni.RhythmGame
[Button("Rebuild")]
public void Rebuild()
{
trackPathSubmodule?.path.Rebuild(true);
trackPathSubmodule?.path.RebuildImmediate();
trackRendererSubmodule?.meshGenerator.Rebuild();
}
#endregion

View File

@@ -32,8 +32,8 @@ namespace Ichni.RhythmGame
LogWindow.Log("AnimationBase must be attached to a GameElement with TransformSubmodule", Color.red);
}
// 向 AnimationManager 注册,通过中废集权 Update 驱动,替代 MonoBehaviour.Update()
AnimationManager.instance.RegisterAnimation(this);
// 向 ElementUpdateScheduler 注册 Phase.Animation
CoreServices.UpdateScheduler.Register(UpdatePhase.Animation, this);
}
public virtual void OnDirtyRefresh(Dictionary<string, bool> flags)
@@ -43,7 +43,7 @@ namespace Ichni.RhythmGame
public override void OnDelete()
{
AnimationManager.instance.UnregisterAnimation(this);
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Animation, this);
}
#endregion
@@ -54,7 +54,16 @@ namespace Ichni.RhythmGame
/// <param name="songTime">歌曲时间</param>
protected abstract void UpdateAnimation(float songTime);
// Update() 已移除,改由 AnimationManager.ManualTick 集中驱动
/// <summary>
/// IScheduledElement 实现:在 Phase.Animation 阶段检查时间范围并执行动画更新。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (phase == UpdatePhase.Animation && timeDurationSubmodule.CheckTimeInDuration(songTime))
{
UpdateAnimation(songTime);
}
}
public virtual Vector3 getValue(float time)
{
@@ -75,10 +84,14 @@ namespace Ichni.RhythmGame
timeDurationSubmodule.endTime += offset;
}
/// <summary>
/// 一次性调用入口,供 BeatmapContainer.AfterLoadSet() 等非调度器路径使用。
/// 不再用于逐帧驱动。
/// </summary>
public virtual void InvokeUpdate()
{
UpdateAnimation(EditorManager.instance.songInformation.songTime);
UpdateAnimation(CoreServices.TimeProvider.SongTime);
}
#endregion
}
}
}

View File

@@ -32,6 +32,11 @@ namespace Ichni.RhythmGame
look.targetTransformSubmodule = (animatedObject as IHaveTransformSubmodule).transformSubmodule;
look.targetTransformSubmodule.lookAt = look;
look.gameCamera = EditorManager.instance.cameraManager.gameCamera;
// 注册到 Phase 7 (Effect) — TickLate 中在 Track/TrackFollower/Note 全部更新完毕后执行旋转覆盖
// 避免因轨道位置尚未就绪导致的旋转抖动
CoreServices.UpdateScheduler.Register(UpdatePhase.Effect, look);
return look;
}
@@ -39,12 +44,29 @@ namespace Ichni.RhythmGame
{
timeDurationSubmodule = new TimeDurationSubmodule(this);
}
public override void OnDelete()
{
base.OnDelete(); // AnimationBase 注销 Phase 1 (Animation)
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Effect, this);
}
#endregion
#region [] Animation Update
void LateUpdate()
/// <summary>
/// 调度器多阶段处理:
/// Phase 1 (Animation) → 由 base.ScheduledUpdate 调用 UpdateAnimation更新 enabling 状态
/// Phase 7 (Effect) → 在所有 Track/TrackFollower/Note 位置更新完毕后执行旋转覆盖
/// 替代原先的 LateUpdate()。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (enabling.value) (animatedObject as IHaveTransformSubmodule)?.UpdateLookAt(this);
base.ScheduledUpdate(phase, songTime); // AnimationBase 处理 Phase 1
if (phase == UpdatePhase.Effect)
{
if (enabling.value) (animatedObject as IHaveTransformSubmodule)?.UpdateLookAt(this);
}
}
protected override void UpdateAnimation(float songTime)

View File

@@ -56,27 +56,4 @@ namespace Ichni.RhythmGame
{
public void TriggerInteraction();
}
namespace Beatmap
{
public abstract class BaseElement_BM
{
public Guid attachedElementGuid;
/// <summary>
/// 从存档类中生成游戏物体
/// </summary>
public abstract void ExecuteBM();
/// <summary>
/// 在AfterInitialize中被调用用于处理GameElement的“需要引用”的物体在此物体后面生成的情况。
/// 注意如果使用此函数需要在ExecuteBM中设置 matchedElement.matchedBM = this;
/// </summary>
public virtual void AfterExecute()
{
}
}
}
}
}

View File

@@ -6,6 +6,13 @@ namespace Ichni.RhythmGame
public static class CoreServices
{
public static ISongTimeProvider TimeProvider { get; set; }
/// <summary>
/// 集中式元素更新调度器。
/// 所有 GameElement 子类应通过此属性访问调度器进行 Register / Unregister
/// 而非直接引用 EditorManager 或 GameManager。
/// </summary>
public static ElementUpdateScheduler UpdateScheduler { get; set; }
}
/// <summary>

View File

@@ -0,0 +1,22 @@
namespace Ichni.RhythmGame
{
/// <summary>
/// 所有参与集中更新调度的元素需实现的接口。
/// 同一元素可注册到多个 <see cref="UpdatePhase"/>
/// 通过 <paramref name="phase"/> 参数区分当前所处阶段并执行对应逻辑。
/// </summary>
public interface IScheduledElement
{
/// <summary>
/// 由 <see cref="ElementUpdateScheduler"/> 在对应阶段调用。
/// </summary>
/// <param name="phase">当前执行的更新阶段</param>
/// <param name="songTime">当前音频播放时间(秒)</param>
void ScheduledUpdate(UpdatePhase phase, float songTime);
/// <summary>
/// 元素是否处于活跃状态。调度器跳过非活跃元素以节省开销。
/// </summary>
bool IsScheduledActive { get; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fdf08f5bfdc7b9f4b87f697c199eb1cf

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Linq;
using Ichni.Editor;
using Ichni.RhythmGame.Beatmap;
using UniRx;
using UnityEngine;
using UnityEngine.Events;
@@ -27,11 +26,15 @@ namespace Ichni.RhythmGame
{
gameElementList = new List<GameElement>();
lowPriorityActions = new List<UnityAction>();
Observable.EveryUpdate().Subscribe(_ => ExecuteLowPriorityActions());
// 低优先级动作由 ElementUpdateScheduler Phase 8 Misc 尾部统一调用,
// 不再使用 UniRx Observable.EveryUpdate 订阅。
}
#endregion
#region [] Low Priority Action Dispatch
/// <summary>
/// 执行并清空低优先级动作队列。由 ElementUpdateScheduler.TickLate() Phase 8 调用。
/// </summary>
public void ExecuteLowPriorityActions()
{
if (lowPriorityActions.Count > 0)
@@ -83,4 +86,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -0,0 +1,38 @@
namespace Ichni.RhythmGame
{
/// <summary>
/// 集中式更新调度器的阶段定义。
/// 每帧按数值升序执行,保证严格的依赖顺序:
/// 动画先于变换应用 → 变换先于 Spline 重建 → 轨道先于音符。
/// 数值留有间隔,便于未来插入新阶段。
/// </summary>
public enum UpdatePhase
{
/// <summary>判定元素激活/隐藏状态</summary>
TimeDuration = 0,
/// <summary>更新动画值,设置脏标记</summary>
Animation = 10,
/// <summary>执行 DirtyRefresh + Transform + Color</summary>
Apply = 20,
/// <summary>手动重建 Dreamteck SplineComputer同时执行 LookAt 等 Transform 后处理覆盖</summary>
SplineRebuild = 30,
/// <summary>更新轨道时间、裁剪区间</summary>
TrackCore = 40,
/// <summary>更新轨道跟踪器CrossTrackPoint / ObjectTracker 等)</summary>
TrackFollower = 50,
/// <summary>音符可见性、轨道位置、判定、特效</summary>
Note = 60,
/// <summary>ParticleEmitter / TimeEffectsCollection 等特效</summary>
Effect = 70,
/// <summary>SkyboxSubsetter / LowPriorityActions 等杂项</summary>
Misc = 80
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6b79a50d212bc9a42b53f7f576f15ae7

View File

@@ -1,18 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Dodger : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}

View File

@@ -9,7 +9,7 @@ using Ichni.Editor;
namespace Ichni.RhythmGame
{
public abstract partial class GameElement : SerializedMonoBehaviour, IBaseElement, IComparable<GameElement>
public abstract partial class GameElement : SerializedMonoBehaviour, IBaseElement, IComparable<GameElement>, IScheduledElement
{
#region [] Essential & Tracking Info
//物体名
@@ -141,6 +141,19 @@ namespace Ichni.RhythmGame
return HierarchyPriority.CompareTo(other.HierarchyPriority);
}
#endregion
#region [] IScheduledElement Implementation
/// <summary>
/// 由 ElementUpdateScheduler 在对应阶段调用。
/// 子类按需重写以实现特定阶段的更新逻辑。
/// </summary>
public virtual void ScheduledUpdate(UpdatePhase phase, float songTime) { }
/// <summary>
/// 元素是否处于活跃状态。调度器跳过非活跃元素以节省开销。
/// </summary>
public virtual bool IsScheduledActive => isActiveAndEnabled;
#endregion
}
#region [] Editor Interaction & Interfaces Overrides

View File

@@ -66,6 +66,17 @@ namespace Ichni.RhythmGame
transformSubmodule = new TransformSubmodule(this);
colorSubmodule = new ColorSubmodule(this, Color.white, true, Color.white, 0);
}
public override void AfterInitialize()
{
base.AfterInitialize();
CoreServices.UpdateScheduler.Register(UpdatePhase.Effect, this);
}
public override void OnDelete()
{
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Effect, this);
}
#endregion
#region [] Control & Refresh
@@ -84,18 +95,21 @@ namespace Ichni.RhythmGame
(this as IHaveParticles).SetParticleSettings(prewarm, simulationSpace, density, lifeTime, speed, radius, isAutoOrient, particleRotation);
}
private void Update()
/// <summary>
/// IScheduledElement 实现:在 Phase.Effect 阶段控制粒子播放/暂停/停止。
/// 替代原 MonoBehaviour.Update(),由 ElementUpdateScheduler 集中驱动。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
float songTime = EditorManager.instance.songInformation.songTime;
if (phase != UpdatePhase.Effect) return;
if (playTime > songTime || stopTime < songTime)
{
if (particle.isPlaying || particle.isPaused)
{
particle.Stop();
if (songTime < playTime) { particle.Clear(); }
}
}
else
{
@@ -107,7 +121,6 @@ namespace Ichni.RhythmGame
{
particle.Play();
}
}
}
public override void Refresh()

View File

@@ -16,6 +16,12 @@ namespace Ichni.RhythmGame
public float time; //触发效果的时间
#endregion
#region [] Cached Effect Lists
private List<EffectBase> _priorEffects;
private List<EffectBase> _defaultEffects;
private List<EffectBase> _lateEffects;
#endregion
#region [] Generation & Initialization
public static TimeEffectsCollection GenerateElement(string name, Guid guid, List<string> tags,
bool isFirstGenerated, GameElement parentElement, float time)
@@ -33,15 +39,54 @@ namespace Ichni.RhythmGame
transformSubmodule = new TransformSubmodule(this);
effectSubmodule = new EffectSubmodule(this);
}
public override void AfterInitialize()
{
base.AfterInitialize();
CacheEffectLists();
CoreServices.UpdateScheduler.Register(UpdatePhase.Effect, this);
}
public override void OnDelete()
{
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Effect, this);
}
/// <summary>
/// 缓存 effectCollection 中的 Prior/Default/Late 列表引用,
/// 避免 ScheduledUpdate 中每帧执行 Dictionary string key 查找和 lambda 委托分配。
/// </summary>
private void CacheEffectLists()
{
if (effectSubmodule?.effectCollection == null) return;
effectSubmodule.effectCollection.TryGetValue("Prior", out _priorEffects);
effectSubmodule.effectCollection.TryGetValue("Default", out _defaultEffects);
effectSubmodule.effectCollection.TryGetValue("Late", out _lateEffects);
}
#endregion
#region [] Control & Refresh
private void Update()
/// <summary>
/// IScheduledElement 实现:在 Phase.Effect 阶段更新时间特效集合。
/// 替代原 MonoBehaviour.Update(),由 ElementUpdateScheduler 集中驱动。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
effectSubmodule.effectCollection["Prior"].ForEach(effect => effect.UpdateEffect(time));
effectSubmodule.effectCollection["Default"].ForEach(effect => effect.UpdateEffect(time));
effectSubmodule.effectCollection["Late"].ForEach(effect => effect.UpdateEffect(time));
if (phase != UpdatePhase.Effect) return;
UpdateEffectList(_priorEffects, time);
UpdateEffectList(_defaultEffects, time);
UpdateEffectList(_lateEffects, time);
}
private static void UpdateEffectList(List<EffectBase> effects, float effectTime)
{
if (effects == null) return;
for (int i = 0; i < effects.Count; i++)
{
effects[i].UpdateEffect(effectTime);
}
}
#endregion
}
}
}

View File

@@ -61,6 +61,17 @@ namespace Ichni.RhythmGame
skyboxBlender.makeFirstMaterialSkybox = true;
skyboxBlender.InspectorAndAwakeChanges();
}
public override void AfterInitialize()
{
base.AfterInitialize();
CoreServices.UpdateScheduler.Register(UpdatePhase.Misc, this);
}
public override void OnDelete()
{
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Misc, this);
}
#endregion
#region [] Control & Refresh
@@ -92,11 +103,16 @@ namespace Ichni.RhythmGame
skyboxBlender.InspectorAndAwakeChanges();
}
private void Update()
/// <summary>
/// IScheduledElement 实现:在 Phase.Misc 阶段处理天空盒切换与混合。
/// 替代原 MonoBehaviour.Update(),由 ElementUpdateScheduler 集中驱动。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (phase != UpdatePhase.Misc) return;
if (skyBoxThemeBundleList.Count > 1)
{
float songTime = EditorManager.instance.songInformation.songTime;
float delay = EditorManager.instance.songInformation.delay;
float finalTime = EditorManager.instance.songInformation.songLength;
@@ -117,4 +133,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -30,6 +30,7 @@ namespace Ichni.RhythmGame
{
flick.track = track;
flick.trackPositioner = flick.gameObject.GetComponent<SplinePositioner>() ?? flick.gameObject.AddComponent<SplinePositioner>();
flick.trackPositioner.autoUpdate = false; // 由调度器 Phase 6 手动刷新采样
flick.trackPositioner.spline = track.trackPathSubmodule.path;
flick.isOnTrack = true;
flick.UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);

View File

@@ -41,8 +41,8 @@ namespace Ichni.RhythmGame
{
hold.track = track;
hold.trackPositioner = hold.gameObject.GetComponent<SplinePositioner>() ?? hold.gameObject.AddComponent<SplinePositioner>();
hold.trackPositioner.autoUpdate = false; // 由调度器 Phase 6 手动刷新采样
hold.trackPositioner.spline = track.trackPathSubmodule.path;
hold.trackPositioner.updateMethod = SplineUser.UpdateMethod.LateUpdate;
hold.isOnTrack = true;
hold.UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);
Observable.NextFrame().Subscribe(_ =>
@@ -177,6 +177,15 @@ namespace Ichni.RhythmGame
holdingTime = songTime - exactJudgeTime;
}
}
// 轨道位置更新:必须在状态机之后执行,确保 holdingTime 等字段已更新。
// 在旧代码中ManualUpdate(Update) 先更新状态 → LateUpdate 再更新位置。
// 现在两者都在 LateUpdate Phase 6 的同一个 ManualUpdate 中,因此将位置更新移至状态机之后。
if (isOnTrack)
{
UpdateNoteInTrack(songTime);
}
if (noteJudgeSubmodule != null && !EditorManager.instance.cameraManager.isSceneCameraActive)
{
foreach (NoteJudgeUnit unit in noteJudgeSubmodule.judgeUnitList.Where(unit => unit.isShowingJudge))
@@ -241,14 +250,6 @@ namespace Ichni.RhythmGame
}
}
}
private void LateUpdate()
{
if (isOnTrack)
{
UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);
}
}
#endregion
}
}

View File

@@ -93,6 +93,7 @@ namespace Ichni.RhythmGame
public virtual void UpdateNoteInMovableTrack(float songTime)
{
TrackTimeSubmoduleMovable trackTimeSubmoduleMovable = track.trackTimeSubmodule as TrackTimeSubmoduleMovable;
trackPositioner.RebuildImmediate();
trackPositioner.SetPercent(trackTimeSubmoduleMovable.GetTrackPercent(exactJudgeTime));
}
@@ -105,6 +106,7 @@ namespace Ichni.RhythmGame
percent = Mathf.Clamp01(percent);
trackPositioner.RebuildImmediate();
trackPositioner.SetPercent(1 - percent);
}
@@ -119,10 +121,10 @@ namespace Ichni.RhythmGame
var editor = EditorManager.instance;
var cameraManager = editor.cameraManager;
// 轨道位置更新
if (isOnTrack && track.trackTimeSubmodule is TrackTimeSubmoduleStatic)
// 轨道位置更新Phase 6 在 LateUpdate 中执行SplineComputer 已完成采样)
if (isOnTrack)
{
UpdateNoteInStaticTrack(currentSongTime);
UpdateNoteInTrack(currentSongTime);
}
// 判定状态更新
@@ -245,10 +247,11 @@ namespace Ichni.RhythmGame
if (exactJudgeTime - beyondTime - 0.5f > -EditorManager.instance.songInformation.delay)
{
gameObject.SetActive(false);
var noteScheduler = CoreServices.UpdateScheduler.NoteScheduler;
if (isNewOne)
EditorManager.instance.noteManager.RegisterNote(this, exactJudgeTime - beyondTime - 0.1f, (this is Hold hold ? hold.holdEndTime : exactJudgeTime) + finishTime + 0.1f);
noteScheduler.RegisterNote(this, exactJudgeTime - beyondTime - 0.1f, (this is Hold hold ? hold.holdEndTime : exactJudgeTime) + finishTime + 0.1f);
else
NoteManager.instance.ChangeNoteInfo(this, exactJudgeTime - beyondTime - 0.1f, (this is Hold hold ? hold.holdEndTime : exactJudgeTime) + finishTime + 0.1f);
noteScheduler.ChangeNoteInfo(this, exactJudgeTime - beyondTime - 0.1f, (this is Hold hold ? hold.holdEndTime : exactJudgeTime) + finishTime + 0.1f);
}
}

View File

@@ -23,6 +23,7 @@ namespace Ichni.RhythmGame
{
stay.track = track;
stay.trackPositioner = stay.gameObject.GetComponent<SplinePositioner>() ?? stay.gameObject.AddComponent<SplinePositioner>();
stay.trackPositioner.autoUpdate = false; // 由调度器 Phase 6 手动刷新采样
stay.trackPositioner.spline = track.trackPathSubmodule.path;
stay.isOnTrack = true;
stay.UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);

View File

@@ -24,6 +24,7 @@ namespace Ichni.RhythmGame
{
tap.track = track;
tap.trackPositioner = tap.gameObject.GetComponent<SplinePositioner>() ?? tap.gameObject.AddComponent<SplinePositioner>();
tap.trackPositioner.autoUpdate = false; // 由调度器 Phase 6 手动刷新采样
tap.trackPositioner.spline = track.trackPathSubmodule.path;
tap.isOnTrack = true;
tap.UpdateNoteInTrack(EditorManager.instance.songInformation.songTime);

View File

@@ -48,15 +48,26 @@ namespace Ichni.RhythmGame
trackPathSubmodule.ClosePath();
}
// 向 TrackManager 注册,通过中废集权 Update 驱动,替代 MonoBehaviour.Update()
TrackManager.instance.RegisterTrack(this);
// 向 ElementUpdateScheduler 注册 Phase.TrackCore + SplineRebuild
var scheduler = CoreServices.UpdateScheduler;
scheduler.Register(UpdatePhase.TrackCore, this);
scheduler.RegisterTrackSpline(this);
Refresh();
}
#endregion
#region [] Update & Refresh
// Update() 已移除,改由 TrackManager.ManualTick 集中驱动
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackCore 阶段更新轨道时间子模块。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (phase == UpdatePhase.TrackCore && timeDurationSubmodule.CheckTimeInDuration(songTime))
{
(trackTimeSubmodule as TrackTimeSubmoduleMovable)?.UpdateTrackPart(songTime);
}
}
public override void Refresh()
{
@@ -69,7 +80,9 @@ namespace Ichni.RhythmGame
public override void OnDelete()
{
if (parentElement is ElementFolder folder) folder.trackList.Remove(this);
TrackManager.instance.UnregisterTrack(this);
var scheduler = CoreServices.UpdateScheduler;
scheduler.Unregister(UpdatePhase.TrackCore, this);
scheduler.UnregisterTrackSpline(this);
}
#endregion
@@ -88,4 +101,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -30,6 +30,7 @@ namespace Ichni.RhythmGame
.AddComponent<CrossTrackPoint>();
point.Initialize(elementName, id, tags, isFirstGenerated, elementFolder);
point.trackPositioner = point.gameObject.AddComponent<SplinePositioner>();
point.trackPositioner.autoUpdate = false; // 由调度器 Phase 5 手动刷新采样
point.nowAttachedTrackIndex = -1;
point.trackListFolder = elementFolder;
point.trackSwitch = trackSwitch;
@@ -51,13 +52,24 @@ namespace Ichni.RhythmGame
public override void AfterInitialize()
{
base.AfterInitialize();
TrackManager.instance.RegisterCrossPoint(this);
// 向 ElementUpdateScheduler 注册 Phase.TrackFollower
CoreServices.UpdateScheduler.Register(UpdatePhase.TrackFollower, this);
}
#endregion
#region [] Update & Refresh
// Update() 已移除,改由 TrackManager.ManualTick 集中驱动
public void ManualTick(float songTime)
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackFollower 阶段更新跨轨切分点。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (phase == UpdatePhase.TrackFollower)
{
ManualTick(songTime);
}
}
private void ManualTick(float songTime)
{
if (trackPercent.animations.Count > 0)
{
@@ -69,7 +81,7 @@ namespace Ichni.RhythmGame
public override void OnDelete()
{
TrackManager.instance.UnregisterCrossPoint(this);
CoreServices.UpdateScheduler.Unregister(UpdatePhase.TrackFollower, this);
}
public override void Refresh()
@@ -92,8 +104,9 @@ namespace Ichni.RhythmGame
trackPositioner.spline = trackListFolder.trackList[trackSwitch.value].trackPathSubmodule.path;
}
trackPositioner.RebuildImmediate();
trackPositioner.SetPercent(trackPercent.value);
}
#endregion
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Ichni.RhythmGame
public TimeDurationSubmodule timeDurationSubmodule { get; set; }
public bool motionApplyRotation;
public Vector3 motionEulerAngles;
private float SongTime => EditorManager.instance.songInformation.songTime;
private float SongTime => CoreServices.TimeProvider.SongTime;
#endregion
#region [] Generation & Initialization
@@ -28,6 +28,7 @@ namespace Ichni.RhythmGame
head.Initialize(elementName, id, tags, isFirstGenerated, track);
head.track = track;
head.trackPositioner = head.gameObject.AddComponent<SplinePositioner>();
head.trackPositioner.autoUpdate = false; // 由调度器 Phase 5 手动刷新采样
head.trackPositioner.spline = track.trackPathSubmodule.path;
head.trackTimeSubmoduleMovable = track.trackTimeSubmodule as TrackTimeSubmoduleMovable;
@@ -54,23 +55,28 @@ namespace Ichni.RhythmGame
public override void AfterInitialize()
{
base.AfterInitialize();
TrackManager.instance.RegisterHeadPoint(this);
// 向 ElementUpdateScheduler 注册 Phase.TrackFollower
CoreServices.UpdateScheduler.Register(UpdatePhase.TrackFollower, this);
}
public override void OnDelete()
{
base.OnDelete();
TrackManager.instance.UnregisterHeadPoint(this);
CoreServices.UpdateScheduler.Unregister(UpdatePhase.TrackFollower, this);
}
#region [] Update & Tracking
public void ManualTick(float songTime)
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackFollower 阶段更新轨道头节点位置。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (track.timeDurationSubmodule.CheckTimeInDuration(songTime))
if (phase == UpdatePhase.TrackFollower && track.timeDurationSubmodule.CheckTimeInDuration(songTime))
{
trackPositioner.RebuildImmediate();
trackPositioner.SetPercent(trackTimeSubmoduleMovable.headPercent);
}
}
#endregion
}
}
}

View File

@@ -32,6 +32,7 @@ namespace Ichni.RhythmGame
point.Initialize(elementName, id, tags, isFirstGenerated, track);
point.track = track;
point.trackPositioner = point.gameObject.AddComponent<SplinePositioner>();
point.trackPositioner.autoUpdate = false; // 由调度器 Phase 5 手动刷新采样
point.trackPositioner.spline = track.trackPathSubmodule.path;
point.trackPercent = trackPercent;
@@ -52,28 +53,35 @@ namespace Ichni.RhythmGame
public override void AfterInitialize()
{
base.AfterInitialize();
TrackManager.instance.RegisterPercentPoint(this);
// 向 ElementUpdateScheduler 注册 Phase.TrackFollower
CoreServices.UpdateScheduler.Register(UpdatePhase.TrackFollower, this);
}
public override void OnDelete()
{
base.OnDelete();
TrackManager.instance.UnregisterPercentPoint(this);
CoreServices.UpdateScheduler.Unregister(UpdatePhase.TrackFollower, this);
}
#region [] Update & Refresh
public void ManualTick(float songTime)
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackFollower 阶段更新轨道百分比位置。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (trackPercent.animations.Count > 0)
if (phase == UpdatePhase.TrackFollower)
{
trackPercent.UpdateFlexibleFloat(songTime);
if (trackPercent.returnType == FlexibleReturnType.MiddleExecuting)
if (trackPercent.animations.Count > 0)
{
float finalValue = trackPercent.value;
if (finalValue > 1 && finalValue > Mathf.Floor(finalValue)) finalValue -= Mathf.Floor(finalValue);
trackPercent.UpdateFlexibleFloat(songTime);
if (trackPercent.returnType == FlexibleReturnType.MiddleExecuting)
{
float finalValue = trackPercent.value;
if (finalValue > 1 && finalValue > Mathf.Floor(finalValue)) finalValue -= Mathf.Floor(finalValue);
trackPositioner.SetPercent(finalValue);
trackPositioner.RebuildImmediate();
trackPositioner.SetPercent(finalValue);
}
}
}
}
@@ -88,4 +96,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -1,18 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrackExtraModifier : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}

View File

@@ -35,7 +35,10 @@ namespace Ichni.RhythmGame
this.sampleRate = sampleRate;
this.path.sampleRate = sampleRate;
this.path.updateMode = SplineComputer.UpdateMode.LateUpdate;
// 保持默认 UpdateMode.Update由 SplineComputer 自行检测 transform.hasChanged
// 并在自身 Update 周期中按需重采样、通知订阅者(延迟)。
// 不可使用 UpdateMode.NoneRebuild(true) 在 None 模式下不触发重采样;
// 不可使用 RebuildImmediate同步重建所有订阅者含 SplineRenderer 网格生成)导致 ~50ms 帧耗。
SetUpSplineComputer(this.trackSpaceType, this.trackSamplingType);
this.isShowingDisplay = isShowingDisplay;
@@ -106,7 +109,7 @@ namespace Ichni.RhythmGame
SetUpSplineComputer(trackSpaceType, trackSamplingType);
ClosePath();
#endif
path.Rebuild(true);
path.RebuildImmediate();
}
#endregion
}

View File

@@ -59,11 +59,17 @@ namespace Ichni.RhythmGame
applyRotationOffset, rotationOffsetMin, rotationOffsetMax, customRotationRuleName,
applyScaleOffset, scaleOffsetMin, scaleOffsetMax, customScaleRuleName);
// 向 TrackManager 注册
TrackManager.instance.RegisterObjectTracker(objectTracker);
if (isFirstGenerated) objectTracker.AfterInitialize();
return objectTracker;
}
public override void AfterInitialize()
{
base.AfterInitialize();
// 向 ElementUpdateScheduler 注册 Phase.TrackFollower
CoreServices.UpdateScheduler.Register(UpdatePhase.TrackFollower, this);
}
public void SetSpawnSettings(int spawnCount,
Vector2 positionOffsetMin, Vector2 positionOffsetMax, string customPositionRuleName,
bool applyRotationOffset, Vector3 rotationOffsetMin, Vector3 rotationOffsetMax, string customRotationRuleName,
@@ -99,28 +105,33 @@ namespace Ichni.RhythmGame
#endregion
#region [] Update
// Update() 已移除,改由 TrackManager.ManualTick 集中驱动
public void ManualTick(float songTime)
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackFollower 阶段控制 objectController 的启用/禁用。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (playTime > songTime || stopTime < songTime)
if (phase == UpdatePhase.TrackFollower)
{
if (objectController.enabled)
objectController.enabled = false;
}
else
{
if (!objectController.enabled)
if (playTime > songTime || stopTime < songTime)
{
objectController.enabled = true;
objectController.Spawn();
if (objectController.enabled)
objectController.enabled = false;
}
else
{
if (!objectController.enabled)
{
objectController.enabled = true;
objectController.Spawn();
}
}
}
}
public override void OnDelete()
{
TrackManager.instance.UnregisterObjectTracker(this);
CoreServices.UpdateScheduler.Unregister(UpdatePhase.TrackFollower, this);
}
#endregion
}
}
}

View File

@@ -60,11 +60,17 @@ namespace Ichni.RhythmGame
particleTracker.particlesContainer.SetParticleMaterial(themeBundleName, materialName);
particleTracker.SetParticleSettings(prewarm, is3D, width, extendDirection, density, lifeTime, isAutoOrient, particleRotation);
// 向 TrackManager 注册
TrackManager.instance.RegisterParticleTracker(particleTracker);
if (isFirstGenerated) particleTracker.AfterInitialize();
return particleTracker;
}
public override void AfterInitialize()
{
base.AfterInitialize();
// 向 ElementUpdateScheduler 注册 Phase.TrackFollower
CoreServices.UpdateScheduler.Register(UpdatePhase.TrackFollower, this);
}
public override void SetDefaultSubmodules()
{
colorSubmodule = new ColorSubmodule(this, Color.white, true, Color.white, 0);
@@ -97,33 +103,38 @@ namespace Ichni.RhythmGame
#endregion
#region [] Update & Refresh
// Update() 已移除,改由 TrackManager.ManualTick 集中驱动
public void ManualTick(float songTime)
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackFollower 阶段控制粒子播放/暂停/停止。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (playTime > songTime || stopTime < songTime)
if (phase == UpdatePhase.TrackFollower)
{
if (particle.isPlaying || particle.isPaused)
if (playTime > songTime || stopTime < songTime)
{
particle.Stop();
if (songTime < playTime) { particle.Clear(); }
if (particle.isPlaying || particle.isPaused)
{
particle.Stop();
if (songTime < playTime) { particle.Clear(); }
}
}
}
else
{
if (!CoreServices.TimeProvider.IsPlaying)
else
{
particle.Pause();
}
else if (!particle.isPlaying)
{
particle.Play();
if (!CoreServices.TimeProvider.IsPlaying)
{
particle.Pause();
}
else if (!particle.isPlaying)
{
particle.Play();
}
}
}
}
public override void OnDelete()
{
TrackManager.instance.UnregisterParticleTracker(this);
CoreServices.UpdateScheduler.Unregister(UpdatePhase.TrackFollower, this);
}
public override void Refresh()
@@ -143,4 +154,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni
{
/// <summary>
/// 编辑器 AnimationManager集中管理场上所有 AnimationBase 实例的逐帧更新。
/// 替代 AnimationBase.Update() 中大量零散的 MonoBehaviour 帧回调,
/// 由 EditorManager.Update 统一驱动,减少 Update() 调用开销。
/// 倒序遍历防止在更新途中某个动画自行销毁导致越界。
/// </summary>
public class AnimationManager : Singleton<AnimationManager>
{
#region [] Singleton Alias
public new static AnimationManager instance => Instance;
#endregion
#region [] Active Animation List
private readonly List<AnimationBase> _activeAnimations = new List<AnimationBase>(200);
#endregion
#region [] Registration
public void RegisterAnimation(AnimationBase anim)
{
if (!_activeAnimations.Contains(anim)) _activeAnimations.Add(anim);
}
public void UnregisterAnimation(AnimationBase anim) => _activeAnimations.Remove(anim);
#endregion
#region [] Manager-Driven Tick
/// <summary>
/// 由 EditorManager.Update 统一调度。
/// 倒序遍历以防在更新途中某个动画自行销毁导致列表越界。
/// </summary>
public void ManualTick(float songTime)
{
for (int i = _activeAnimations.Count - 1; i >= 0; i--)
{
var anim = _activeAnimations[i];
if (!anim.isActiveAndEnabled) continue;
if (anim.timeDurationSubmodule.CheckTimeInDuration(songTime))
{
anim.InvokeUpdate();
}
}
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 5f068825cf8c22b4fa573d55654c2474

View File

@@ -16,6 +16,7 @@ namespace Ichni
/// 游戏/谱面数据由 ProjectContainer 持有;
/// 此处的属性均为转发属性,保持所有外部调用点零改动。
/// </summary>
[DefaultExecutionOrder(-100)]
public class EditorManager : Singleton<EditorManager>
{
#region [] Singleton Alias
@@ -37,13 +38,20 @@ namespace Ichni
public BackgroundController backgroundController;
public SimpleGridController gridController;
public CameraManager cameraManager;
public NoteManager noteManager;
public TrackManager trackManager;
public AnimationManager animationManager;
public Canvas judgeHintCanvas;
public Canvas inspectorCanvas;
public Timeline timeline;
public PanelDrawer panelDrawer;
/// <summary>集中式元素更新调度器,替代原先分散的帧更新逻辑</summary>
[NonSerialized]
public ElementUpdateScheduler updateScheduler;
/// <summary>
/// NoteManager 转发属性。NoteManager 现为纯 C# 类,由 updateScheduler 内部持有。
/// 保留此属性以兼容现有调用点。
/// </summary>
public NoteManager noteManager => updateScheduler?.NoteScheduler;
#endregion
#region [] Editor Preferences Forwarding Properties
@@ -124,6 +132,14 @@ namespace Ichni
// 不再直接依赖 EditorManager.instance.musicPlayer
CoreServices.TimeProvider = musicPlayer;
// 初始化集中式更新调度器:
// BeatmapContainer 在项目加载后才可用,使用 Func 延迟解析
// NoteManager 在调度器内部创建,不再依赖外部 Singleton
updateScheduler = new ElementUpdateScheduler(
() => beatmapContainer
);
CoreServices.UpdateScheduler = updateScheduler;
if (!ES3.FileExists(Application.streamingAssetsPath + "/EditorSettings.es3"))
{
editorSettings = new EditorSettings(300, 3, 100, 100, 60);
@@ -173,42 +189,14 @@ namespace Ichni
projectManager.autoSaveManager.UpdateAutoSave();
// 统一调度: Animation → Submodules → Track → Note
float songTime = CoreServices.TimeProvider.SongTime;
updateScheduler.TickEarly(songTime);
}
animationManager.ManualTick(songTime);
// 手动执行原本属于 UniRx 的每帧调度,消灭不可控的时序错乱
for (int i = 0; i < beatmapContainer.gameElementList.Count; i++)
{
var element = beatmapContainer.gameElementList[i];
if (element == null) continue;
if (element is IHaveTimeDurationSubmodule timeHost && !(element is NoteBase))
{
timeHost.timeDurationSubmodule?.UpdateTimeDuration(songTime);
}
if (element is IHaveDirtyMarkSubmodule dirtyHost)
{
dirtyHost.dirtyMarkSubmodule?.ExecuteDeferredRefresh();
}
if (element.gameObject.activeSelf)
{
if (element is IHaveTransformSubmodule transformHost)
{
transformHost.UpdateTransform();
}
if (element is IHaveColorSubmodule colorHost)
{
colorHost.UpdateColor();
}
}
}
trackManager.ManualTick(songTime);
noteManager.ManualTick(songTime);
private void LateUpdate()
{
if (!isLoaded) return;
updateScheduler.TickLate();
}
#endregion

View File

@@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni
{
/// <summary>
/// 集中式元素更新调度器。
/// 将所有 GameElement 的帧更新按 9 个阶段有序执行,
/// 替代原先分散在 EditorManager.Update()、各子管理器、MonoBehaviour.Update() 中的零散更新。
///
/// 调度器分为两个执行阶段,分别在 Update 和 LateUpdate 中调用:
///
/// TickEarly (Update, EditorManager [order -100])
/// Phase 0 TimeDuration — 判定元素激活/隐藏
/// Phase 1 Animation — 更新动画值,设置脏标记
/// Phase 2 Apply — 执行 DirtyRefresh + Transform + Color
///
/// --- Unity 执行其他 Update (包括 SplineComputer.Update [order 0]) ---
/// SplineComputer 检测 transform.hasChanged重新采样延迟通知订阅者。
///
/// TickLate (LateUpdate, EditorManager [order -100])
/// Phase 3 SplineRebuild — 保留占位SplineComputer 已在 Update 中自行处理)
/// Phase 4 TrackCore — 更新轨道时间、裁剪区间
/// Phase 5 TrackFollower — CrossTrackPoint / Tracker / HeadPoint / PercentPoint
/// Phase 6 Note — 音符可见性、轨道位置、判定、特效
/// Phase 7 Effect — ParticleEmitter / TimeEffectsCollection / LookAt 旋转覆盖
/// Phase 8 Misc — SkyboxSubsetter / LowPriorityActions
///
/// 此分离设计消除了 Dreamteck Spline 的一帧延迟:
/// Phase 2 (Update) 修改 Transform → SplineComputer.Update() 检测 hasChanged 并重采样
/// → Phase 5/6 (LateUpdate) 调用 RebuildImmediate() 获取最新采样后 SetPercent()。
///
/// 所有通过调度器管理的 SplinePositioner 应设置 autoUpdate = false
/// 由调度器在 Phase 5/6 中手动调用 RebuildImmediate() 刷新采样。
///
/// 设计要点:
/// - AnimationManager 和 TrackManager 已删除,其逻辑由各 GameElement 子类通过
/// IScheduledElement.ScheduledUpdate() 自行处理。
/// - NoteManager 因内部逻辑复杂(二分搜索、时间可逆),保留为纯 C# 类,
/// 由本调度器内部持有和驱动。
/// - Phase 0TimeDuration和 Phase 2Apply仍有 [Legacy] 内联循环,
/// 待后续将 TimeDurationSubmodule / TransformSubmodule / ColorSubmodule
/// 迁移到 IScheduledElement 后移除。
/// </summary>
public class ElementUpdateScheduler
{
#region [] Constants
/// <summary>阶段总数TimeDuration..Misc步长 10共 9 个)</summary>
private const int PhaseCount = 9;
/// <summary>阶段枚举值到数组索引的步长除数</summary>
private const int PhaseStep = 10;
private static readonly UpdatePhase[] AllPhases = (UpdatePhase[])Enum.GetValues(typeof(UpdatePhase));
/// <summary>TickEarly 执行的最大 Phase</summary>
private const UpdatePhase EarlyPhaseCutoff = UpdatePhase.Apply;
#endregion
#region [] Phase Element Lists
/// <summary>
/// 每个阶段的已注册 IScheduledElement 列表。
/// 使用数组替代 Dictionary&lt;UpdatePhase, ...&gt;
/// 索引通过 (int)phase / 10 直接映射,消除哈希查找开销。
/// </summary>
private readonly List<IScheduledElement>[] _phaseElements;
/// <summary>将 UpdatePhase 枚举值转换为数组索引。</summary>
private static int PhaseIndex(UpdatePhase phase) => (int)phase / PhaseStep;
#endregion
#region [ Spline ] Track Spline List
/// <summary>Phase 3 专用:需要手动重建 SplineComputer 的 Track 列表(保留供后续优化)</summary>
private readonly List<Track> _trackSplines = new List<Track>(50);
#endregion
#region [] Internal Managers
/// <summary>音符管理器由调度器内部持有Phase 6 调用</summary>
private readonly NoteManager _noteManager;
/// <summary>BeatmapContainer 延迟提供器(项目加载后才可用)</summary>
private readonly Func<BeatmapContainer> _beatmapContainerProvider;
/// <summary>公开 NoteManager 以供 NoteBase 等外部类注册/更新音符信息</summary>
public NoteManager NoteScheduler => _noteManager;
#endregion
#region [] Cached State
/// <summary>TickEarly 中缓存的 songTime供 TickLate 使用</summary>
private float _cachedSongTime;
/// <summary>TickEarly 中缓存的 BeatmapContainer 引用</summary>
private BeatmapContainer _cachedBeatmapContainer;
#endregion
#region [] Constructor
/// <summary>
/// 创建调度器实例。NoteManager 在内部创建,不再依赖外部 Singleton。
/// </summary>
/// <param name="beatmapContainerProvider">
/// BeatmapContainer 延迟提供器。因 BeatmapContainer 在项目加载后才可用,
/// 使用 Func 延迟解析,避免构造时空引用。
/// </param>
public ElementUpdateScheduler(Func<BeatmapContainer> beatmapContainerProvider)
{
_noteManager = new NoteManager();
_beatmapContainerProvider = beatmapContainerProvider;
_phaseElements = new List<IScheduledElement>[PhaseCount];
for (int i = 0; i < PhaseCount; i++)
{
_phaseElements[i] = new List<IScheduledElement>();
}
}
#endregion
#region [] Registration
/// <summary>将元素注册到指定的更新阶段。同一元素可注册到多个阶段。</summary>
public void Register(UpdatePhase phase, IScheduledElement element)
{
var list = _phaseElements[PhaseIndex(phase)];
if (!list.Contains(element))
{
list.Add(element);
}
}
/// <summary>将元素从指定阶段注销。</summary>
public void Unregister(UpdatePhase phase, IScheduledElement element)
{
_phaseElements[PhaseIndex(phase)].Remove(element);
}
/// <summary>注册 Track 的 SplineComputer 到 Phase 3SplineRebuild。</summary>
public void RegisterTrackSpline(Track track)
{
if (!_trackSplines.Contains(track))
{
_trackSplines.Add(track);
}
}
/// <summary>从 Phase 3 注销 Track 的 SplineComputer。</summary>
public void UnregisterTrackSpline(Track track)
{
_trackSplines.Remove(track);
}
#endregion
#region [ - ] Main Tick Early Phases (Update)
/// <summary>
/// 早期阶段调度,由 EditorManager.Update() 调用。
/// 执行 Phase 0TimeDuration→ Phase 1Animation→ Phase 2Apply
/// Phase 2 完成后 Transform 已更新SplineComputer 将在后续的 Update [order 0] 中检测变化。
/// </summary>
public void TickEarly(float songTime)
{
_cachedBeatmapContainer = _beatmapContainerProvider?.Invoke();
_cachedSongTime = songTime;
if (_cachedBeatmapContainer == null) return;
// ─── Phase 0: TimeDuration ────────────────────────────────────────
TickPhase(UpdatePhase.TimeDuration, songTime);
TickTimeDurationLegacy(_cachedBeatmapContainer, songTime);
// ─── Phase 1: Animation ───────────────────────────────────────────
TickPhase(UpdatePhase.Animation, songTime);
// ─── Phase 2: Apply ───────────────────────────────────────────────
TickPhase(UpdatePhase.Apply, songTime);
TickApplyLegacy(_cachedBeatmapContainer);
}
#endregion
#region [ - ] Main Tick Late Phases (LateUpdate)
/// <summary>
/// 晚期阶段调度,由 EditorManager.LateUpdate() 调用。
/// 此时所有 Update() 已完成SplineComputer 已检测 transform.hasChanged 并重新采样。
/// 各跟踪器通过 RebuildImmediate() 获取最新 Spline 采样后再 SetPercent()。
/// </summary>
public void TickLate()
{
if (_cachedBeatmapContainer == null) return;
float songTime = _cachedSongTime;
// ─── Phase 3: SplineRebuild ───────────────────────────────────────
// SplineComputer 使用默认 UpdateMode.Update已在 Update 中自行处理。
// 此阶段保留占位,不执行手动 Rebuild。
TickPhase(UpdatePhase.SplineRebuild, songTime);
// ─── Phase 4: TrackCore ───────────────────────────────────────────
TickPhase(UpdatePhase.TrackCore, songTime);
// ─── Phase 5: TrackFollower ───────────────────────────────────────
// 各跟踪器在 ScheduledUpdate 中先调用 RebuildImmediate() 再 SetPercent()
TickPhase(UpdatePhase.TrackFollower, songTime);
// ─── Phase 6: Note ────────────────────────────────────────────────
TickPhase(UpdatePhase.Note, songTime);
_noteManager.ManualTick(songTime);
// ─── Phase 7: Effect ──────────────────────────────────────────────
TickPhase(UpdatePhase.Effect, songTime);
// ─── Phase 8: Misc ────────────────────────────────────────────────
TickPhase(UpdatePhase.Misc, songTime);
_cachedBeatmapContainer.ExecuteLowPriorityActions();
}
#endregion
#region [] Phase Execution
/// <summary>
/// 执行指定阶段的所有已注册 IScheduledElement。
/// 倒序遍历防止更新途中元素自行销毁导致越界。
/// </summary>
private void TickPhase(UpdatePhase phase, float songTime)
{
var list = _phaseElements[PhaseIndex(phase)];
for (int i = list.Count - 1; i >= 0; i--)
{
var element = list[i];
if (element != null && element.IsScheduledActive)
{
element.ScheduledUpdate(phase, songTime);
}
}
}
#endregion
#region [Legacy]
/// <summary>
/// [Legacy] Phase 0 内联逻辑:遍历 gameElementList 执行 TimeDuration 检查。
/// 当所有 IHaveTimeDurationSubmodule 元素迁移到调度器后,此方法将被移除。
/// </summary>
private void TickTimeDurationLegacy(BeatmapContainer beatmapContainer, float songTime)
{
for (int i = 0; i < beatmapContainer.gameElementList.Count; i++)
{
var element = beatmapContainer.gameElementList[i];
if (element == null) continue;
if (element is IHaveTimeDurationSubmodule timeHost && !(element is NoteBase))
{
timeHost.timeDurationSubmodule?.UpdateTimeDuration(songTime);
}
}
}
/// <summary>
/// [Legacy] Phase 2 内联逻辑:遍历 gameElementList 执行 DirtyRefresh / Transform / Color。
/// 当所有相关子模块接口元素迁移到调度器后,此方法将被移除。
/// </summary>
private void TickApplyLegacy(BeatmapContainer beatmapContainer)
{
for (int i = 0; i < beatmapContainer.gameElementList.Count; i++)
{
var element = beatmapContainer.gameElementList[i];
if (element == null) continue;
if (element is IHaveDirtyMarkSubmodule dirtyHost)
{
dirtyHost.dirtyMarkSubmodule?.ExecuteDeferredRefresh();
}
if (element.gameObject.activeSelf)
{
if (element is IHaveTransformSubmodule transformHost)
{
transformHost.UpdateTransform();
}
if (element is IHaveColorSubmodule colorHost)
{
colorHost.UpdateColor();
}
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2086c4a4da4d364428cdff9ac604ad38

View File

@@ -1,10 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni.RhythmGame
namespace Ichni
{
/// <summary>
/// 编辑器 NoteManager集中管理场上所有 Note 的激活/隐藏与逐帧更新。
@@ -16,13 +14,12 @@ namespace Ichni.RhythmGame
/// - 时间区间内的 Note → 激活并更新
/// - 时间区间外的 Note → 隐藏
/// 保证无论时间如何跳转Note 的可见性和状态始终与 songTime 一致。
///
/// 此类为纯 C# 类,由 ElementUpdateScheduler 内部持有和驱动,
/// 不再继承 Singleton / MonoBehaviour。
/// </summary>
public class NoteManager : Singleton<NoteManager>
public class NoteManager
{
#region [] Singleton Alias
public new static NoteManager instance => Instance;
#endregion
#region [] Note Record
/// <summary>Note 条目:存储 Note 本身及其激活/消失的时间阈值</summary>
private struct NoteRecord
@@ -91,7 +88,7 @@ namespace Ichni.RhythmGame
#region [] Manager-Driven Tick
/// <summary>
/// 由 EditorManager.Update 统一调度
/// 由 ElementUpdateScheduler.Tick 在 Phase.Note 阶段调用
///
/// <para>
/// 编辑器时间可逆策略:每帧通过二分查找定位当前 songTime 覆盖的激活窗口,
@@ -124,9 +121,6 @@ namespace Ichni.RhythmGame
right = mid - 1;
}
}
// firstInWindow 此时指向最后一个 activationTime <= songTime 的 Note
// 从 0 向左扫描(也就是从头扫描到 firstInWindow都可能在窗口内
// 实际需要activationTime <= songTime && finishTime >= songTime
// 3. 遍历所有 Note判断是否在当前窗口内
for (int i = 0; i < count; i++)
@@ -156,4 +150,4 @@ namespace Ichni.RhythmGame
}
#endregion
}
}
}

View File

@@ -1,125 +0,0 @@
using System.Collections.Generic;
using Ichni.RhythmGame;
using SLSUtilities.General;
using UnityEngine;
namespace Ichni
{
/// <summary>
/// 编辑器 TrackManager集中管理场上所有轨道相关组件的逐帧更新。
/// 替代各组件自身持有的 Update() 调用,消除大量零散的 MonoBehaviour 帧回调开销。
/// 通过 ManualTick() 由 EditorManager 统一调度,确保时序可控。
/// </summary>
public class TrackManager : Singleton<TrackManager>
{
#region [] Singleton Alias
public new static TrackManager instance => Instance;
#endregion
#region [] Active Component Lists
private readonly List<Track> _activeTracks = new List<Track>(50);
private readonly List<CrossTrackPoint> _activeCrossPoints = new List<CrossTrackPoint>(50);
private readonly List<ObjectTracker> _activeObjectTrackers = new List<ObjectTracker>(50);
private readonly List<ParticleTracker> _activeParticleTrackers = new List<ParticleTracker>(50);
private readonly List<TrackHeadPoint> _activeHeadPoints = new List<TrackHeadPoint>(50);
private readonly List<TrackPercentPoint> _activePercentPoints = new List<TrackPercentPoint>(50);
#endregion
#region [] Registration
public void RegisterTrack(Track track)
{
if (!_activeTracks.Contains(track)) _activeTracks.Add(track);
}
public void UnregisterTrack(Track track) => _activeTracks.Remove(track);
public void RegisterCrossPoint(CrossTrackPoint point)
{
if (!_activeCrossPoints.Contains(point)) _activeCrossPoints.Add(point);
}
public void UnregisterCrossPoint(CrossTrackPoint point) => _activeCrossPoints.Remove(point);
public void RegisterObjectTracker(ObjectTracker tracker)
{
if (!_activeObjectTrackers.Contains(tracker)) _activeObjectTrackers.Add(tracker);
}
public void UnregisterObjectTracker(ObjectTracker tracker) => _activeObjectTrackers.Remove(tracker);
public void RegisterParticleTracker(ParticleTracker tracker)
{
if (!_activeParticleTrackers.Contains(tracker)) _activeParticleTrackers.Add(tracker);
}
public void UnregisterParticleTracker(ParticleTracker tracker) => _activeParticleTrackers.Remove(tracker);
public void RegisterHeadPoint(TrackHeadPoint point)
{
if (!_activeHeadPoints.Contains(point)) _activeHeadPoints.Add(point);
}
public void UnregisterHeadPoint(TrackHeadPoint point) => _activeHeadPoints.Remove(point);
public void RegisterPercentPoint(TrackPercentPoint point)
{
if (!_activePercentPoints.Contains(point)) _activePercentPoints.Add(point);
}
public void UnregisterPercentPoint(TrackPercentPoint point) => _activePercentPoints.Remove(point);
#endregion
#region [] Manager-Driven Tick
/// <summary>
/// 由 EditorManager.Update 统一调度。
/// </summary>
public void ManualTick(float songTime)
{
// 1. Track更新轨道时间子模块
for (int i = 0; i < _activeTracks.Count; i++)
{
var track = _activeTracks[i];
if (!track.isActiveAndEnabled) continue;
if (track.timeDurationSubmodule.CheckTimeInDuration(songTime))
{
(track.trackTimeSubmodule as TrackTimeSubmoduleMovable)?.UpdateTrackPart(songTime);
}
}
// 2. CrossTrackPoint更新跨轨切分点
for (int i = 0; i < _activeCrossPoints.Count; i++)
{
var point = _activeCrossPoints[i];
if (!point.isActiveAndEnabled) continue;
point.ManualTick(songTime);
}
// 3. ObjectTracker更新轨道物体跟踪器
for (int i = 0; i < _activeObjectTrackers.Count; i++)
{
var tracker = _activeObjectTrackers[i];
if (!tracker.isActiveAndEnabled) continue;
tracker.ManualTick(songTime);
}
// 4. ParticleTracker更新轨道粒子跟踪器
for (int i = 0; i < _activeParticleTrackers.Count; i++)
{
var tracker = _activeParticleTrackers[i];
if (!tracker.isActiveAndEnabled) continue;
tracker.ManualTick(songTime);
}
// 5. TrackHeadPoint更新轨道头节点
for (int i = 0; i < _activeHeadPoints.Count; i++)
{
var point = _activeHeadPoints[i];
if (!point.isActiveAndEnabled) continue;
point.ManualTick(songTime);
}
// 6. TrackPercentPoint更新轨道百分比节点
for (int i = 0; i < _activePercentPoints.Count; i++)
{
var point = _activePercentPoints[i];
if (!point.isActiveAndEnabled) continue;
point.ManualTick(songTime);
}
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3435f487f7c1c654e93adba65c7de915

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: fda5a36c77610481e976e5d7ceb764ab
guid: 1c8fad83bf015114ca0c02e8f42058f7
folderAsset: yes
DefaultImporter:
externalObjects: {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 28802a2f6f0ff2942bd2a248b9b68960
guid: cc21fb1bb36766044abf71b576d7d12c
DefaultImporter:
externalObjects: {}
userData:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6392436ca0d07464b98d158fd6cad5e2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 88f0993afef43b34eb23a79fbdd94e9b
guid: d0f3b53f3516db445b3bb38127c6a03f
DefaultImporter:
externalObjects: {}
userData:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 24c93fe194f1c4349b4ff6b0cd5aac1f
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d02ffa27542509e488e8d8bab70a3c16
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 22676d2e0ea9c194b84b33c59c48fac1
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8b64bd0799a2d9a4caade060fa09c19c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7bfe901bed086ee4187e2011c5f107da
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
ŰQ[pßńîÔˇż\Ą\-tçÎO•ţX ź¤D=zŢüLć'č+Úˇˇř‚ŁňônÓÉkV<6B>ŚŃy¨}©ép]_u€đŃŠ/ř@Ç6{ôt.üOŘDŠ—vyxł$Żé˘<C3A9>eAŻŘâ#v

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5ac25c78d1cee8744b81b36c6f236364
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c39126d70d6e28a49bce803a52238933
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
ŕ“€qĎr3¬z--}ŢLĆË c\<zű_Ô<5F>Ăž@hÉË6łżPźjD`Ţő|†vm|ť+Ë•Id

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 08acf28f1d0ab4841a2cc84237f640b1
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1890190e17022134e8de82377cd88405
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 274b0a26f8a8318458380defb69d4325
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
{
"CommandScripts" : {
"__type" : "Ichni.RhythmGame.Beatmap.CommandScripts_BM,Assembly-CSharp",
"value" : {
"commandList" : [
],
"attachedElementGuid" : {
"value" : "00000000-0000-0000-0000-000000000000"
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e277fe47b357c964a92c78254687f2d6
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
{
"ProjectInformation" : {
"__type" : "Ichni.RhythmGame.Beatmap.ProjectInformation_BM,Assembly-CSharp",
"value" : {
"projectName" : "Your_Shadow",
"creatorName" : "U_Mora",
"editorVersion" : "0.1.0",
"createTime" : "2026\/1\/6 22:14:38",
"lastSaveTime" : "2026\/5\/29 20:18:32",
"selectedThemeBundleList" : [
"basic","departure_to_multiverse"
],
"tagManager" : {
"tagMatchers" : [
]
},
"attachedElementGuid" : {
"value" : "00000000-0000-0000-0000-000000000000"
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3ce70c5a293e68145941a65c07a0e14d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"SongInformation" : {
"__type" : "Ichni.RhythmGame.Beatmap.SongInformation_BM,Assembly-CSharp",
"value" : {
"songName" : "Your Shadow.wav",
"bpm" : 180,
"delay" : 0,
"offset" : -0.6,
"attachedElementGuid" : {
"value" : "00000000-0000-0000-0000-000000000000"
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6d3f1337c61fbc944b94aad9c9eb0459
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4ccfa8b82ef12634c981472b34795c2c
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9498aebaaae72d24ca5096f71fef4552
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 31622a9f418403d4d97679cf2ef93047
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
{
"CommandScripts" : {
"__type" : "Ichni.RhythmGame.Beatmap.CommandScripts_BM,Assembly-CSharp",
"value" : {
"commandList" : [
],
"attachedElementGuid" : {
"value" : "00000000-0000-0000-0000-000000000000"
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 337405124eca95d4288947981462d243
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
{
"ProjectInformation" : {
"__type" : "Ichni.RhythmGame.Beatmap.ProjectInformation_BM,Assembly-CSharp",
"value" : {
"projectName" : "solitudes hd",
"creatorName" : "jiankn",
"editorVersion" : "0.1.0",
"createTime" : "2026\/5\/5 23:24:17",
"lastSaveTime" : "2026\/5\/30 23:47:44",
"selectedThemeBundleList" : [
"basic","departure_to_multiverse"
],
"tagManager" : {
"tagMatchers" : [
]
},
"attachedElementGuid" : {
"value" : "00000000-0000-0000-0000-000000000000"
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7ce8adba7efb90d47afa51881f4b1449
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 859ca5bcd1e63db4ba1502e71f916841
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"SongInformation" : {
"__type" : "Ichni.RhythmGame.Beatmap.SongInformation_BM,Assembly-CSharp",
"value" : {
"songName" : "Solitudes.mp3",
"bpm" : 200,
"delay" : 0,
"offset" : 0,
"attachedElementGuid" : {
"value" : "00000000-0000-0000-0000-000000000000"
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b2aa73ff74964b742bc7b15f66001357
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@@ -113,6 +113,21 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
{
base.AfterInitialize();
ApplyColorSubmodule();
// 一次性初始化星座(原 Update() 中的 hasInitializedSpawning 守卫逻辑)
if (!hasInitializedSpawning)
{
GenerateSingleConstellation();
hasInitializedSpawning = true;
}
// 注册到调度器 Phase 7 (Effect),每帧重建连线 Mesh
CoreServices.UpdateScheduler.Register(UpdatePhase.Effect, this);
}
public override void OnDelete()
{
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Effect, this);
}
public override void Refresh()
@@ -153,74 +168,14 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
}
}
void Update()
/// <summary>
/// 调度器 Phase 7 (Effect):每帧读取粒子位置,通过 Burst Job 计算连线并重建 Mesh。
/// 替代原先的 LateUpdate()。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (!hasInitializedSpawning)
{
GenerateSingleConstellation();
hasInitializedSpawning = true;
}
}
if (phase != UpdatePhase.Effect) return;
[Button("Refresh Constellation")]
public void GenerateSingleConstellation()
{
starParticleSystem.Stop();
starParticleSystem.Clear();
// --- 【新增】:通过代码接管并设置 Velocity over Lifetime 模块 ---
// 注意:如果轨道旋转不为零,则强制开启该模块
var vol = starParticleSystem.velocityOverLifetime;
if (orbitalVelocity != Vector3.zero)
{
vol.enabled = true;
vol.orbitalX = new ParticleSystem.MinMaxCurve(orbitalVelocity.x);
vol.orbitalY = new ParticleSystem.MinMaxCurve(orbitalVelocity.y);
vol.orbitalZ = new ParticleSystem.MinMaxCurve(orbitalVelocity.z);
}
else
{
vol.enabled = false;
}
// --- 【新增】:通过代码接管并设置 Rotation over Lifetime 模块 ---
var rol = starParticleSystem.rotationOverLifetime;
if (angularVelocity != 0f)
{
rol.enabled = true;
// 对于普通的 Billboard 粒子Z 轴旋转就是屏幕空间上的二维自转
rol.z = new ParticleSystem.MinMaxCurve(angularVelocity * Mathf.Deg2Rad); // 转换为弧度
}
else
{
rol.enabled = false;
}
ParticleSystem.EmitParams emitParams = new ParticleSystem.EmitParams();
for (int i = 0; i < maxParticles; i++)
{
float x = Random.Range(-spreadSize.x * 0.5f, spreadSize.x * 0.5f);
float y = Random.Range(-spreadSize.y * 0.5f, spreadSize.y * 0.5f);
float z = Random.Range(-spreadSize.z * 0.5f, spreadSize.z * 0.5f);
emitParams.position = new Vector3(x, y, z);
// startColor 使用 colorSubmodule 的当前 BaseColor单色
emitParams.startColor = colorSubmodule != null
? (Color32)colorSubmodule.currentBaseColor
: new Color32(0, 255, 255, 255);
// 为了让自身旋转可见,可以在生成时赋予一个随机的初始旋转角度
emitParams.rotation3D = new Vector3(0, 0, Random.Range(0f, 360f));
starParticleSystem.Emit(emitParams, 1);
}
starParticleSystem.Play();
}
void LateUpdate()
{
if (starParticleSystem == null || lineMeshFilter == null) return;
int aliveCount = starParticleSystem.GetParticles(particles);
@@ -287,6 +242,63 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
connectionCounts.Dispose();
adjacencyMatrix.Dispose();
}
[Button("Refresh Constellation")]
public void GenerateSingleConstellation()
{
starParticleSystem.Stop();
starParticleSystem.Clear();
// --- 【新增】:通过代码接管并设置 Velocity over Lifetime 模块 ---
// 注意:如果轨道旋转不为零,则强制开启该模块
var vol = starParticleSystem.velocityOverLifetime;
if (orbitalVelocity != Vector3.zero)
{
vol.enabled = true;
vol.orbitalX = new ParticleSystem.MinMaxCurve(orbitalVelocity.x);
vol.orbitalY = new ParticleSystem.MinMaxCurve(orbitalVelocity.y);
vol.orbitalZ = new ParticleSystem.MinMaxCurve(orbitalVelocity.z);
}
else
{
vol.enabled = false;
}
// --- 【新增】:通过代码接管并设置 Rotation over Lifetime 模块 ---
var rol = starParticleSystem.rotationOverLifetime;
if (angularVelocity != 0f)
{
rol.enabled = true;
// 对于普通的 Billboard 粒子Z 轴旋转就是屏幕空间上的二维自转
rol.z = new ParticleSystem.MinMaxCurve(angularVelocity * Mathf.Deg2Rad); // 转换为弧度
}
else
{
rol.enabled = false;
}
ParticleSystem.EmitParams emitParams = new ParticleSystem.EmitParams();
for (int i = 0; i < maxParticles; i++)
{
float x = Random.Range(-spreadSize.x * 0.5f, spreadSize.x * 0.5f);
float y = Random.Range(-spreadSize.y * 0.5f, spreadSize.y * 0.5f);
float z = Random.Range(-spreadSize.z * 0.5f, spreadSize.z * 0.5f);
emitParams.position = new Vector3(x, y, z);
// startColor 使用 colorSubmodule 的当前 BaseColor单色
emitParams.startColor = colorSubmodule != null
? (Color32)colorSubmodule.currentBaseColor
: new Color32(0, 255, 255, 255);
// 为了让自身旋转可见,可以在生成时赋予一个随机的初始旋转角度
emitParams.rotation3D = new Vector3(0, 0, Random.Range(0f, 360f));
starParticleSystem.Emit(emitParams, 1);
}
starParticleSystem.Play();
}
}
[BurstCompile(CompileSynchronously = true)]

View File

@@ -7,7 +7,6 @@ using Ichni.RhythmGame.Beatmap;
using Ichni.RhythmGame.ThemeBundles.Basic.Beatmap;
using Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse.Beatmap;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
{
@@ -77,6 +76,17 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
return dtmTrail;
}
public override void AfterInitialize()
{
base.AfterInitialize();
CoreServices.UpdateScheduler.Register(UpdatePhase.Effect, this);
}
public override void OnDelete()
{
CoreServices.UpdateScheduler.Unregister(UpdatePhase.Effect, this);
}
public override void SetDefaultSubmodules()
{
base.SetDefaultSubmodules();
@@ -230,9 +240,14 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
#endregion
#region [] Event Animation Logic
private void Update()
/// <summary>
/// 调度器 Phase 7 (Effect):更新 FlexibleBool/Float 动画,驱动 Head 显隐和 TrailRenderer 参数。
/// 替代原先的 Update()。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
float songTime = EditorManager.instance.songInformation.songTime;
if (phase != UpdatePhase.Effect) return;
enableTimes.UpdateFlexibleBool(songTime);
if (enableTimes.value && !isHeadEnabled)
{
@@ -246,24 +261,10 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
}
visibleTimeLength.UpdateFlexibleFloat(songTime);
if (visibleTimeLength.animations.Count > 0 && EditorManager.instance.musicPlayer.isPlaying && trailRenderer.time != visibleTimeLength.value)//为的是接口里头那个用来set的
if (visibleTimeLength.animations.Count > 0 && EditorManager.instance.musicPlayer.isPlaying && trailRenderer.time != visibleTimeLength.value)
{
// Debug.Log(trailRenderer == null);
trailRenderer.time = visibleTimeLength.value;
}
if (isHeadEnabled && headRotateSpeed.animations.Count > 0)
{
/*headRotateSpeed.UpdateFlexibleFloat(songTime);
var rotationBySpeedModule = headCircle.GetComponent<ParticleSystem>().rotationOverLifetime;
rotationBySpeedModule.z = headRotateSpeed.value;*/
}
if (Keyboard.current.tKey.wasPressedThisFrame)
{
// TriggerInteraction();
}
}
private Sequence enableHeadSequence;

View File

@@ -55,8 +55,10 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
this.hold.trackPositioner.autoUpdate = false;
headPoint.spline = hold.track.trackPathSubmodule.path;
headPoint.autoUpdate = false; // 由调度器 Phase 6 通过 RebuildImmediate 手动刷新
meshGenerator.spline = hold.track.trackPathSubmodule.path;
tailPoint.spline = hold.track.trackPathSubmodule.path;
tailPoint.autoUpdate = false; // 由调度器 Phase 6 通过 RebuildImmediate 手动刷新
TrackTimeSubmoduleMovable trackTimeSubmoduleMovable = hold.track.trackTimeSubmodule as TrackTimeSubmoduleMovable;
float startPercent = trackTimeSubmoduleMovable.GetTrackPercent(hold.exactJudgeTime);
@@ -161,9 +163,19 @@ namespace Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse
endPercent = trackTimeSubmoduleMovable.GetTrackPercent(hold.holdEndTime);
}
// 确保所有 SplineUser 使用当前帧的最新 Spline 采样数据。
// Phase 6 在 LateUpdate 中执行SplineComputer 已在 Update 中重采样完毕。
// autoUpdate=false 的 SplinePositioner 不会自行刷新,必须手动 RebuildImmediate。
hold.trackPositioner.RebuildImmediate();
hold.trackPositioner.SetPercent(startPercent);
meshGenerator.SetClipRange(startPercent, endPercent);
meshGenerator.RebuildImmediate(); // 立即重建网格,避免与 head/tail 位置错位一帧
headPoint.RebuildImmediate();
headPoint.SetPercent(startPercent);
tailPoint.RebuildImmediate();
tailPoint.SetPercent(endPercent);
}

View File

@@ -14,10 +14,11 @@
"bezi3d"
],
"unity": "2018.3",
"version": "0.86.4",
"version": "0.90.0",
"type": "library",
"hideInEditor": false,
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.unity.timeline": "1.6.0"
}
}

View File

@@ -5,7 +5,8 @@
"depth": 0,
"source": "embedded",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.unity.timeline": "1.6.0"
}
},
"com.unity.2d.animation": {