大修
This commit is contained in:
51
Assets/Scripts/Manager/AnimationManager.cs
Normal file
51
Assets/Scripts/Manager/AnimationManager.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Manager/AnimationManager.cs.meta
Normal file
2
Assets/Scripts/Manager/AnimationManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f068825cf8c22b4fa573d55654c2474
|
||||
@@ -1,24 +1,33 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Dreamteck.Splines.Primitives;
|
||||
using Ichni.Editor;
|
||||
using Ichni.RhythmGame;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using Ichni.RhythmGame.ThemeBundles.Basic;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni
|
||||
{
|
||||
public class EditorManager : GameElement
|
||||
/// <summary>
|
||||
/// 编辑器全局管理器。
|
||||
/// 继承自 Singleton<EditorManager>,只持有编辑器基础设施引用:
|
||||
/// 子管理器、音频播放器、UI、相机、设置、首选项等。
|
||||
/// 游戏/谱面数据由 ProjectContainer 持有;
|
||||
/// 此处的属性均为转发属性,保持所有外部调用点零改动。
|
||||
/// </summary>
|
||||
public class EditorManager : Singleton<EditorManager>
|
||||
{
|
||||
public static EditorManager instance;
|
||||
#region [单例别名] Singleton Alias
|
||||
/// <summary>小写别名,兼容现有调用点</summary>
|
||||
public new static EditorManager instance => Instance;
|
||||
#endregion
|
||||
|
||||
#region [加载状态] Load State
|
||||
public bool isLoaded;
|
||||
#endregion
|
||||
|
||||
#region [编辑器基础设施管理器] Editor Infrastructure Managers
|
||||
public ProjectManager projectManager;
|
||||
public AudioManager audioManager;
|
||||
public MusicPlayer musicPlayer;
|
||||
@@ -29,39 +38,96 @@ namespace Ichni
|
||||
public SimpleGridController gridController;
|
||||
public CameraManager cameraManager;
|
||||
public NoteManager noteManager;
|
||||
public Ichni.Editor.PostProcessingManager postProcessingManager;
|
||||
public TrackManager trackManager;
|
||||
public AnimationManager animationManager;
|
||||
public Canvas judgeHintCanvas;
|
||||
public Canvas inspectorCanvas;
|
||||
public Timeline timeline;
|
||||
|
||||
public ProjectInformation projectInformation;
|
||||
public SongInformation songInformation;
|
||||
public BeatmapContainer beatmapContainer;
|
||||
public CommandScripts commandScripts;
|
||||
|
||||
public PanelDrawer panelDrawer;
|
||||
public NoteBase.NoteJudgeType currentJudgeType;
|
||||
public bool useClickSelect;
|
||||
public bool useNotePrefab;
|
||||
public bool ExpandWhileClick;
|
||||
public bool useQuickMove;
|
||||
#endregion
|
||||
|
||||
#region [编辑器首选项转发属性] Editor Preferences Forwarding Properties
|
||||
// 实际字段在 ProjectContainer 中定义,此处为转发属性以保持所有调用点不变
|
||||
public NoteBase.NoteJudgeType currentJudgeType
|
||||
{
|
||||
get => ProjectContainer.instance.currentJudgeType;
|
||||
set => ProjectContainer.instance.currentJudgeType = value;
|
||||
}
|
||||
public bool useClickSelect
|
||||
{
|
||||
get => ProjectContainer.instance.useClickSelect;
|
||||
set => ProjectContainer.instance.useClickSelect = value;
|
||||
}
|
||||
public bool useNotePrefab
|
||||
{
|
||||
get => ProjectContainer.instance.useNotePrefab;
|
||||
set => ProjectContainer.instance.useNotePrefab = value;
|
||||
}
|
||||
public bool ExpandWhileClick
|
||||
{
|
||||
get => ProjectContainer.instance.ExpandWhileClick;
|
||||
set => ProjectContainer.instance.ExpandWhileClick = value;
|
||||
}
|
||||
public bool useQuickMove
|
||||
{
|
||||
get => ProjectContainer.instance.useQuickMove;
|
||||
set => ProjectContainer.instance.useQuickMove = value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [预制体资产集合] Prefab Asset Collections
|
||||
public BasePrefabsCollection basePrefabs;
|
||||
public Dictionary<string, CustomPrefabsCollection> customPrefabs;
|
||||
|
||||
|
||||
public NoteAudioCollection noteAudioCollection;
|
||||
#endregion
|
||||
|
||||
[Title("Runtime Global Elements")]
|
||||
public VariablesContainer variablesContainer;
|
||||
public BackgroundSetter backgroundSetter;
|
||||
|
||||
private void Awake()
|
||||
#region [ProjectContainer 转发属性] ProjectContainer Forwarding Properties
|
||||
// 以下属性保持与原 EditorManager 完全相同的访问路径,
|
||||
// 内部转发至 ProjectContainer,无需修改任何调用点。
|
||||
public ProjectInformation projectInformation
|
||||
{
|
||||
instance = this;
|
||||
get => ProjectContainer.instance.projectInformation;
|
||||
set => ProjectContainer.instance.projectInformation = value;
|
||||
}
|
||||
public SongInformation songInformation
|
||||
{
|
||||
get => ProjectContainer.instance.songInformation;
|
||||
set => ProjectContainer.instance.songInformation = value;
|
||||
}
|
||||
public BeatmapContainer beatmapContainer
|
||||
{
|
||||
get => ProjectContainer.instance.beatmapContainer;
|
||||
set => ProjectContainer.instance.beatmapContainer = value;
|
||||
}
|
||||
public CommandScripts commandScripts
|
||||
{
|
||||
get => ProjectContainer.instance.commandScripts;
|
||||
set => ProjectContainer.instance.commandScripts = value;
|
||||
}
|
||||
public VariablesContainer variablesContainer
|
||||
{
|
||||
get => ProjectContainer.instance.variablesContainer;
|
||||
set => ProjectContainer.instance.variablesContainer = value;
|
||||
}
|
||||
public BackgroundSetter backgroundSetter
|
||||
{
|
||||
get => ProjectContainer.instance.backgroundSetter;
|
||||
set => ProjectContainer.instance.backgroundSetter = value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [生命周期] Lifecycle
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake(); // Singleton<T>.Initialize(false)
|
||||
isLoaded = false;
|
||||
projectManager = new ProjectManager();
|
||||
operationManager = new OperationManager();
|
||||
|
||||
// 注册时间提供者:让编辑器的所有时间相关逻辑通过 CoreServices.TimeProvider 访问,
|
||||
// 不再直接依赖 EditorManager.instance.musicPlayer
|
||||
CoreServices.TimeProvider = musicPlayer;
|
||||
|
||||
if (!ES3.FileExists(Application.streamingAssetsPath + "/EditorSettings.es3"))
|
||||
{
|
||||
editorSettings = new EditorSettings(300, 3, 100, 100, 60);
|
||||
@@ -78,31 +144,78 @@ namespace Ichni
|
||||
private void Start()
|
||||
{
|
||||
StartCoroutine(StartFrameRate());
|
||||
Debug.Log("EditorManager Start: Initializing UI and Loading Project...");
|
||||
// ProjectContainer 自身作为根节点注册到层级视图
|
||||
ProjectContainer.instance.elementName = "EditorManager";
|
||||
ProjectContainer.instance.elementGuid = Guid.Empty;
|
||||
uiManager.hierarchy.GenerateTab(ProjectContainer.instance, null);
|
||||
ProjectContainer.instance.connectedTab.deleteButton.gameObject.SetActive(false);
|
||||
|
||||
|
||||
this.elementName = "EditorManager";
|
||||
this.elementGuid = Guid.Empty;
|
||||
uiManager.hierarchy.GenerateTab(this, null);
|
||||
this.connectedTab.deleteButton.gameObject.SetActive(false);
|
||||
if (InformationTransistor.instance.isLoadedProject)
|
||||
{
|
||||
LoadProject(InformationTransistor.instance.loadedProjectName);
|
||||
Debug.Log("Loaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
projectManager.GenerateEmptyProject(InformationTransistor.instance.projectInfo_BM, InformationTransistor.instance.songInfo_BM);
|
||||
projectManager.GenerateEmptyProject(
|
||||
InformationTransistor.instance.projectInfo_BM,
|
||||
InformationTransistor.instance.songInfo_BM);
|
||||
projectManager.saveManager.Save();
|
||||
musicPlayer.audioSource.clip = songInformation.song;
|
||||
Debug.Log("Generated");
|
||||
}
|
||||
|
||||
StartCoroutine(beatmapContainer.AfterLoadSet());
|
||||
isLoaded = true;
|
||||
songInformation.songTime = musicPlayer.audioSource.time - songInformation.offset;
|
||||
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isLoaded) return;
|
||||
|
||||
projectManager.autoSaveManager.UpdateAutoSave();
|
||||
|
||||
// 统一调度: Animation → Submodules → Track → Note
|
||||
float songTime = CoreServices.TimeProvider.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.gameObject.activeSelf)
|
||||
{
|
||||
if (element is IHaveTransformSubmodule transformHost)
|
||||
{
|
||||
transformHost.UpdateTransform();
|
||||
}
|
||||
if (element is IHaveColorSubmodule colorHost)
|
||||
{
|
||||
colorHost.UpdateColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trackManager.ManualTick(songTime);
|
||||
noteManager.ManualTick(songTime);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [FPS 监控] FPS Monitor
|
||||
public float CurrentFrameRate;
|
||||
public TMP_Text FPStext;
|
||||
public TMP_Text UIText;
|
||||
|
||||
private IEnumerator StartFrameRate()
|
||||
{
|
||||
int frameCount = 0;
|
||||
@@ -118,13 +231,9 @@ namespace Ichni
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isLoaded) projectManager.autoSaveManager.UpdateAutoSave();
|
||||
|
||||
}
|
||||
|
||||
#region [项目加载] Project Loading
|
||||
public void LoadProject(string projectName)
|
||||
{
|
||||
if (!InformationTransistor.instance.isRecovery)
|
||||
@@ -144,107 +253,34 @@ namespace Ichni
|
||||
gameElement.Refresh();
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
IHaveInspection inspector = uiManager.inspector;
|
||||
|
||||
var container = inspector.GenerateContainer("Editor Manager");
|
||||
|
||||
var inGameSettings = container.GenerateSubcontainer(3);
|
||||
var judgeTypeDropdown = inspector.GenerateDropdown(this, inGameSettings, "Judge Type",
|
||||
typeof(NoteBase.NoteJudgeType), nameof(currentJudgeType)).AddListenerFunction(() =>
|
||||
{
|
||||
foreach (GameElement gameElement in beatmapContainer.gameElementList)
|
||||
{
|
||||
if (gameElement is NoteVisualBase noteVisual)
|
||||
{
|
||||
noteVisual.Recover();
|
||||
}
|
||||
}
|
||||
});
|
||||
var useNotePrefabToggle =
|
||||
inspector.GenerateToggle(this, inGameSettings, "Use Note Prefab", nameof(useNotePrefab));
|
||||
var useClickSelectToggle =
|
||||
inspector.GenerateToggle(this, inGameSettings, "Use Click Select", nameof(useClickSelect));
|
||||
var ExpandWhileClickToggle =
|
||||
inspector.GenerateToggle(this, inGameSettings, "Expand Tab While Click", nameof(ExpandWhileClick));
|
||||
|
||||
var useQuickMoveToggle =
|
||||
inspector.GenerateToggle(this, inGameSettings, "Use Quick Move", nameof(useQuickMove));
|
||||
|
||||
var generation = container.GenerateSubcontainer(3);
|
||||
var generateFolderButton =
|
||||
inspector.GenerateButton(this, generation, "Generate Folder",
|
||||
() => ElementFolder.GenerateElement("Folder", Guid.NewGuid(),
|
||||
new List<string>(), true, null));
|
||||
var generateBackgroundSetterButton =
|
||||
inspector.GenerateButton(this, generation, "Generate Background Setter",
|
||||
() => BackgroundSetter.GenerateElement("Background Setter", Guid.NewGuid(),
|
||||
new List<string>(), true, null, false,
|
||||
"basic", "Skybox", "Background"));
|
||||
var generateVariablesContainerButton =
|
||||
inspector.GenerateButton(this, generation, "Generate Variables Container",
|
||||
() => VariablesContainer.GenerateElement("Variables Container", Guid.NewGuid(),
|
||||
new List<string>(), true, null, new Dictionary<string, int>()));
|
||||
|
||||
projectInformation.SetUpInspector();
|
||||
songInformation.SetUpInspector();
|
||||
cameraManager.SetUpInspector();
|
||||
var oo = inspector.GenerateContainer("Grid");
|
||||
var p = oo.GenerateSubcontainer(3);
|
||||
var po = inspector.GenerateToggle(this, p, "Enable Grid");
|
||||
po.AddListenerFunction(() =>
|
||||
{
|
||||
gridController.gameObject.SetActive(po.toggle.isOn);
|
||||
});
|
||||
var o = inspector.GenerateInputField(p, "Grid Size", (gridController.baseGridSize * 10).ToString());
|
||||
o.AddListenerFunction(() =>
|
||||
{
|
||||
gridController.baseGridSize = float.Parse(o.inputField.text) / 10;
|
||||
});
|
||||
var c = inspector.GenerateToggle(this, p, "Show Coordinates", nameof(gridController) + "." + nameof(gridController.showCoordinates));
|
||||
}
|
||||
|
||||
|
||||
#region [退出处理] Application Quit
|
||||
private bool isQuit = false;
|
||||
|
||||
[Obsolete]
|
||||
public void OnApplicationQuit()
|
||||
{
|
||||
if (isQuit) return;
|
||||
|
||||
Application.CancelQuit();//退出拦截
|
||||
Application.CancelQuit(); // 退出拦截
|
||||
GeneralSecondaryWindow QuitWindow =
|
||||
Instantiate(EditorManager.instance.basePrefabs.generalSecondaryWindow,
|
||||
EditorManager.instance.uiManager.mainPage.mainCanvas.GetComponent<RectTransform>()).GetComponent<GeneralSecondaryWindow>();
|
||||
Instantiate(instance.basePrefabs.generalSecondaryWindow,
|
||||
instance.uiManager.mainPage.mainCanvas.GetComponent<RectTransform>())
|
||||
.GetComponent<GeneralSecondaryWindow>();
|
||||
|
||||
QuitWindow.Initialize("Do You Want To Save?");
|
||||
|
||||
|
||||
var container = QuitWindow.GenerateContainer("Save confirm");
|
||||
var beatmapToolsSettings = container.GenerateSubcontainer(3);
|
||||
var yesButton = QuitWindow.GenerateButton(beatmapToolsSettings, "Yes", () =>
|
||||
{
|
||||
SaveAndQuit();
|
||||
|
||||
});
|
||||
var noButton = QuitWindow.GenerateButton(beatmapToolsSettings, "No", () =>
|
||||
QuitWindow.GenerateButton(beatmapToolsSettings, "Yes", () => SaveAndQuit());
|
||||
QuitWindow.GenerateButton(beatmapToolsSettings, "No", () =>
|
||||
{
|
||||
isQuit = true;
|
||||
Application.Quit();
|
||||
});
|
||||
|
||||
// if (isQuit)
|
||||
// {
|
||||
// Application.CancelQuit();//退出拦截
|
||||
// MessageCtrl.Instance.OpenConfirmView("关闭界面将终止,确认关闭?", "", () =>
|
||||
// {
|
||||
// isQuit = false;
|
||||
// Application.Quit();
|
||||
// });
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
async void SaveAndQuit()
|
||||
{
|
||||
isQuit = true;
|
||||
@@ -252,5 +288,6 @@ namespace Ichni
|
||||
await projectManager.saveManager.SaveAllCoroutine();
|
||||
Application.Quit();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
executionOrder: -10
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.RhythmGame;
|
||||
using Sirenix.Utilities;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
@@ -12,26 +13,25 @@ using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public partial class InputListener : MonoBehaviour
|
||||
public partial class InputListener : Singleton<InputListener>
|
||||
{
|
||||
public static InputListener instance;
|
||||
/// <summary>小写别名,兼容现有调用点</summary>
|
||||
public new static InputListener instance => Instance;
|
||||
|
||||
public bool isPointerOverUI;
|
||||
public TMP_Text hoveredUIText;
|
||||
//public GameObject hoveredUI;
|
||||
public EventSystem eventSystem;
|
||||
public List<GraphicRaycaster> graphicRaycasters;
|
||||
private List<SelectionConnector> lastHitConnectors = new List<SelectionConnector>();
|
||||
private int currentSelectIndex = 0;
|
||||
|
||||
|
||||
private Vector2 lastMousePosition;
|
||||
private int frameCount = 0;
|
||||
private const int uiCheckFrameInterval = 5; // 每隔多少帧强制检查一次UI
|
||||
private const int uiCheckFrameInterval = 5;
|
||||
|
||||
|
||||
private void Awake()
|
||||
protected override void Awake()
|
||||
{
|
||||
instance = this;
|
||||
base.Awake(); // Singleton<T>.Initialize(false)
|
||||
}
|
||||
private void Start()
|
||||
{
|
||||
@@ -153,7 +153,7 @@ namespace Ichni.Editor
|
||||
{
|
||||
if (Keyboard.current.spaceKey.wasPressedThisFrame) // 回车键 播放或暂停音乐
|
||||
{
|
||||
if (!EditorManager.instance.musicPlayer.isPlaying)
|
||||
if (!CoreServices.TimeProvider.IsPlaying)
|
||||
{
|
||||
EditorManager.instance.musicPlayer.PlayMusic();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.RhythmGame;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public class MusicPlayer : MonoBehaviour
|
||||
/// <summary>
|
||||
/// 编辑器的音乐播放控制器。
|
||||
/// 实现 ISongTimeProvider,作为编辑器的标准时间来源,
|
||||
/// 在 EditorManager.Awake 中注册到 CoreServices.TimeProvider。
|
||||
/// </summary>
|
||||
public class MusicPlayer : MonoBehaviour, ISongTimeProvider
|
||||
{
|
||||
#region [ISongTimeProvider 实现] ISongTimeProvider Implementation
|
||||
/// <summary>当前播放进度(秒),已扣除 offset,供 CoreServices.TimeProvider 使用</summary>
|
||||
public float SongTime => EditorManager.instance.songInformation.songTime;
|
||||
|
||||
/// <summary>当前是否正在播放</summary>
|
||||
public bool IsPlaying => isPlaying;
|
||||
#endregion
|
||||
|
||||
#region [运行时缓存数据] Property Caches
|
||||
public bool isDebugging;
|
||||
public bool isPlaying;
|
||||
public AudioSource audioSource;
|
||||
private float DspTime => (float)AudioSettings.dspTime;
|
||||
#endregion
|
||||
|
||||
#region [生命周期] Lifecycle
|
||||
private void Update()
|
||||
{
|
||||
if (isDebugging)
|
||||
@@ -23,16 +38,18 @@ namespace Ichni.Editor
|
||||
|
||||
if (isPlaying)
|
||||
{
|
||||
EditorManager.instance.songInformation.songTime = EditorManager.instance.musicPlayer.audioSource.time - EditorManager.instance.songInformation.offset;
|
||||
EditorManager.instance.songInformation.songTime =
|
||||
audioSource.time - EditorManager.instance.songInformation.offset;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [播放控制] Playback Control
|
||||
public void PlayMusic()
|
||||
{
|
||||
|
||||
isPlaying = !isPlaying;
|
||||
|
||||
audioSource.time = EditorManager.instance.songInformation.songTime + EditorManager.instance.songInformation.offset;
|
||||
audioSource.time = EditorManager.instance.songInformation.songTime +
|
||||
EditorManager.instance.songInformation.offset;
|
||||
if (isPlaying)
|
||||
{
|
||||
Trail.FreezeAllTrails(!isPlaying);
|
||||
@@ -40,6 +57,7 @@ namespace Ichni.Editor
|
||||
}
|
||||
else PauseMusic();
|
||||
}
|
||||
|
||||
public IEnumerator PlayBackMusic()
|
||||
{
|
||||
float startt = audioSource.time - EditorManager.instance.songInformation.offset;
|
||||
@@ -47,12 +65,15 @@ namespace Ichni.Editor
|
||||
yield return new WaitUntil(() => Keyboard.current.rightAltKey.wasReleasedThisFrame);
|
||||
audioSource.time = startt + EditorManager.instance.songInformation.offset;
|
||||
PauseMusic();
|
||||
|
||||
}
|
||||
|
||||
public void PauseMusic()
|
||||
{
|
||||
if (isPlaying) EditorManager.instance.songInformation.songTime = audioSource.time - EditorManager.instance.songInformation.offset;
|
||||
|
||||
if (isPlaying)
|
||||
{
|
||||
EditorManager.instance.songInformation.songTime =
|
||||
audioSource.time - EditorManager.instance.songInformation.offset;
|
||||
}
|
||||
isPlaying = false;
|
||||
audioSource.Pause();
|
||||
Trail.FreezeAllTrails(!isPlaying);
|
||||
@@ -63,7 +84,7 @@ namespace Ichni.Editor
|
||||
isPlaying = false;
|
||||
EditorManager.instance.songInformation.songTime = 0;
|
||||
audioSource.Stop();
|
||||
//EditorManager.instance.uiManager.timeline.timePointerModule.SetRange(0);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,159 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UniRx;
|
||||
using Ichni.RhythmGame;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.RhythmGame
|
||||
{
|
||||
public class NoteManager : MonoBehaviour
|
||||
/// <summary>
|
||||
/// 编辑器 NoteManager:集中管理场上所有 Note 的激活/隐藏与逐帧更新。
|
||||
///
|
||||
/// 与游戏本体的关键区别——时间可逆性:
|
||||
/// 游戏本体时间单向流动,_nextNoteIndex 只递增。
|
||||
/// 编辑器时间可随意跳转 / 倒退(拖动 Slider、按数字键),
|
||||
/// 因此本管理器采用"每帧重新扫描激活窗口"的策略:
|
||||
/// - 时间区间内的 Note → 激活并更新
|
||||
/// - 时间区间外的 Note → 隐藏
|
||||
/// 保证无论时间如何跳转,Note 的可见性和状态始终与 songTime 一致。
|
||||
/// </summary>
|
||||
public class NoteManager : Singleton<NoteManager>
|
||||
{
|
||||
public static NoteManager instance;
|
||||
public List<(NoteBase note, float activationTime, float finishTime)> pendingNotes = new List<(NoteBase, float, float)>();
|
||||
private List<(NoteBase note1, bool isActive, float activationTime)> ProcessingNotes = new List<(NoteBase, bool, float)>();
|
||||
#region [单例别名] Singleton Alias
|
||||
public new static NoteManager instance => Instance;
|
||||
#endregion
|
||||
|
||||
#region [数据结构] Note Record
|
||||
/// <summary>Note 条目:存储 Note 本身及其激活/消失的时间阈值</summary>
|
||||
private struct NoteRecord
|
||||
{
|
||||
public NoteBase note;
|
||||
public float activationTime; // Note 应当进入可见状态的时间点
|
||||
public float finishTime; // Note 应当退出可见状态的时间点
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 所有已注册的 Note,按 activationTime 升序排列。
|
||||
/// 排序后可用二分查找高效定位当前窗口。
|
||||
/// </summary>
|
||||
private List<NoteRecord> _pendingNotes = new List<NoteRecord>(128);
|
||||
|
||||
/// <summary>当前帧内激活的 Note(缓存,减少 GC)</summary>
|
||||
private List<NoteBase> _currentlyActive = new List<NoteBase>(64);
|
||||
|
||||
private bool _isDirty = false; // 注册/更新后需要重排序
|
||||
#endregion
|
||||
|
||||
#region [注册与管理] Registration
|
||||
/// <summary>注册一个新 Note(isNewOne = true)</summary>
|
||||
public void RegisterNote(NoteBase note, float activationTime, float finishTime)
|
||||
{
|
||||
pendingNotes.Add((note, activationTime, finishTime));
|
||||
AllNotesRegistered();
|
||||
print($"Registered note {note.elementName} with activation time {activationTime} and finish time {finishTime}");
|
||||
}
|
||||
public void Awake()
|
||||
{
|
||||
instance = this;
|
||||
_pendingNotes.Add(new NoteRecord
|
||||
{
|
||||
note = note,
|
||||
activationTime = activationTime,
|
||||
finishTime = finishTime
|
||||
});
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>更新已注册 Note 的时间信息(参数改变后重新计算窗口)</summary>
|
||||
public void ChangeNoteInfo(NoteBase note, float activationTime, float finishTime)
|
||||
{
|
||||
int idx = pendingNotes.FindIndex(i => i.note == note);
|
||||
int idx = _pendingNotes.FindIndex(r => r.note == note);
|
||||
if (idx != -1)
|
||||
{
|
||||
var one = pendingNotes[idx];
|
||||
one.finishTime = finishTime;
|
||||
one.activationTime = activationTime;
|
||||
pendingNotes[idx] = one;
|
||||
AllNotesRegistered();
|
||||
_pendingNotes[idx] = new NoteRecord
|
||||
{
|
||||
note = note,
|
||||
activationTime = activationTime,
|
||||
finishTime = finishTime
|
||||
};
|
||||
_isDirty = true;
|
||||
}
|
||||
}
|
||||
// 在所有物体注册完毕后,对列表进行一次排序
|
||||
|
||||
/// <summary>手动触发排序(若需要在注册大批 Note 后一次性完成)</summary>
|
||||
public void AllNotesRegistered()
|
||||
{
|
||||
pendingNotes.Sort((a, b) => a.activationTime.CompareTo(b.activationTime));
|
||||
SortIfDirty();
|
||||
}
|
||||
|
||||
void Update()
|
||||
private void SortIfDirty()
|
||||
{
|
||||
float currentTime = EditorManager.instance.songInformation.songTime;
|
||||
|
||||
foreach (var item in ProcessingNotes)
|
||||
{
|
||||
var (note, isActive, activationTime) = item;
|
||||
if (!isActive) note.Update();
|
||||
note.gameObject.SetActive(isActive);
|
||||
if (isActive)
|
||||
{
|
||||
note.Update();
|
||||
if (currentTime < activationTime)
|
||||
{
|
||||
note.noteVisual?.Recover();
|
||||
|
||||
}
|
||||
else if (note is Hold hold && currentTime >= hold.holdEndTime)
|
||||
{
|
||||
hold.SetFinishEffects();
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessingNotes.Clear();
|
||||
|
||||
// 一次性移除所有 null 项
|
||||
pendingNotes.RemoveAll(item => item.note == null);
|
||||
|
||||
foreach (var item in pendingNotes)
|
||||
{
|
||||
var (note, activationTime, finishTime) = item;
|
||||
bool shouldBeActive = currentTime >= activationTime && currentTime <= finishTime;
|
||||
bool isActive = note.gameObject.activeSelf;
|
||||
|
||||
if (shouldBeActive && !isActive)
|
||||
{
|
||||
ProcessingNotes.Add((note, true, activationTime));
|
||||
|
||||
}
|
||||
else if (!shouldBeActive && isActive)
|
||||
{
|
||||
ProcessingNotes.Add((note, false, activationTime));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!_isDirty) return;
|
||||
// 移除已销毁的 Note
|
||||
_pendingNotes.RemoveAll(r => r.note == null);
|
||||
_pendingNotes.Sort((a, b) => a.activationTime.CompareTo(b.activationTime));
|
||||
_isDirty = false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [中央集权主循环] Manager-Driven Tick
|
||||
/// <summary>
|
||||
/// 由 EditorManager.Update 统一调度。
|
||||
///
|
||||
/// <para>
|
||||
/// 编辑器时间可逆策略:每帧通过二分查找定位当前 songTime 覆盖的激活窗口,
|
||||
/// 对窗口内的 Note 激活并调用 Update(),对窗口外的 Note 隐藏。
|
||||
/// 无需维护 _nextNoteIndex 指针,天然支持时间任意跳转与倒退。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void ManualTick(float songTime)
|
||||
{
|
||||
SortIfDirty();
|
||||
|
||||
// 1. 清空上一帧的激活记录
|
||||
_currentlyActive.Clear();
|
||||
|
||||
// 2. 二分查找第一个 activationTime <= songTime 的 Note 起始位置
|
||||
int count = _pendingNotes.Count;
|
||||
if (count == 0) return;
|
||||
|
||||
int left = 0, right = count - 1, firstInWindow = count;
|
||||
while (left <= right)
|
||||
{
|
||||
int mid = (left + right) >> 1;
|
||||
if (_pendingNotes[mid].activationTime <= songTime)
|
||||
{
|
||||
firstInWindow = mid;
|
||||
left = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
// firstInWindow 此时指向最后一个 activationTime <= songTime 的 Note,
|
||||
// 从 0 向左扫描(也就是从头扫描到 firstInWindow)都可能在窗口内
|
||||
// 实际需要:activationTime <= songTime && finishTime >= songTime
|
||||
|
||||
// 3. 遍历所有 Note,判断是否在当前窗口内
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var record = _pendingNotes[i];
|
||||
if (record.note == null) continue;
|
||||
|
||||
bool shouldBeActive = record.activationTime <= songTime && record.finishTime >= songTime;
|
||||
bool isActive = record.note.gameObject.activeSelf;
|
||||
|
||||
if (shouldBeActive)
|
||||
{
|
||||
if (!isActive) record.note.gameObject.SetActive(true);
|
||||
_currentlyActive.Add(record.note);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isActive) record.note.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 集中驱动当前帧内所有激活 Note 的逐帧更新
|
||||
for (int i = _currentlyActive.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_currentlyActive[i].ManualUpdate(songTime);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
namespace Ichni.Editor
|
||||
namespace Ichni
|
||||
{
|
||||
public class PostProcessingManager : MonoBehaviour
|
||||
public class PostProcessingManager : Singleton<PostProcessingManager>
|
||||
{
|
||||
public Volume globalVolume;
|
||||
public PostProcessingController nbController;
|
||||
public PixelateFeature pixelateFeature;
|
||||
public static Volume GlobalVolume => instance._globalVolume;
|
||||
|
||||
void Awake()
|
||||
[ShowInInspector]
|
||||
private Volume _globalVolume;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
FindAndCacheFeatureWithReflection();
|
||||
SetFeatureActive(false);
|
||||
SetPixelateStrength(Screen.width, Screen.height);
|
||||
base.Awake();
|
||||
//FindAndCacheFeatureWithReflection();
|
||||
}
|
||||
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
//FindAndCacheFeatureWithReflection();
|
||||
}
|
||||
|
||||
private void FindAndCacheFeatureWithReflection()
|
||||
{
|
||||
var pipelineAsset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
||||
@@ -32,7 +39,8 @@ namespace Ichni.Editor
|
||||
}
|
||||
|
||||
// 2. 使用反射来获取内部的 m_RendererDataList 字段
|
||||
FieldInfo rendererDataListField = typeof(UniversalRenderPipelineAsset).GetField("m_RendererDataList", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
FieldInfo rendererDataListField =
|
||||
typeof(UniversalRenderPipelineAsset).GetField("m_RendererDataList", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (rendererDataListField == null)
|
||||
{
|
||||
Debug.LogError("在 UniversalRenderPipelineAsset 中无法通过反射找到 'm_RendererDataList' 字段。API可能已在你的URP版本中更改。");
|
||||
@@ -45,47 +53,6 @@ namespace Ichni.Editor
|
||||
Debug.LogError("获取渲染器数据列表失败。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 遍历获取到的列表来查找我们的Feature
|
||||
foreach (var rendererData in rendererDataList)
|
||||
{
|
||||
if (rendererData == null) continue;
|
||||
|
||||
var feature = rendererData.rendererFeatures.OfType<PixelateFeature>().FirstOrDefault();
|
||||
if (feature != null)
|
||||
{
|
||||
pixelateFeature = feature;
|
||||
Debug.Log("成功找到并缓存 pixelateFeature (通过反射)!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pixelateFeature == null)
|
||||
{
|
||||
Debug.LogError("在所有 RendererData 中都未找到 pixelateFeature。");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFeatureActive(bool enable)
|
||||
{
|
||||
if (pixelateFeature != null)
|
||||
{
|
||||
pixelateFeature.SetActive(enable);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPixelateStrength(float strengthX, float strengthY)
|
||||
{
|
||||
if (pixelateFeature != null)
|
||||
{
|
||||
pixelateFeature.settings.pixelateStrengthX = strengthX;
|
||||
pixelateFeature.settings.pixelateStrengthY = strengthY;
|
||||
pixelateFeature.pixelatePass.UpdateConfig(strengthX, strengthY);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Pixelate feature is not initialized.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Assets/Scripts/Manager/ProjectContainer.cs
Normal file
115
Assets/Scripts/Manager/ProjectContainer.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Editor;
|
||||
using Ichni.RhythmGame;
|
||||
using Ichni.RhythmGame.Beatmap;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni
|
||||
{
|
||||
/// <summary>
|
||||
/// 谱面数据容器。
|
||||
/// 继承自 GameElement,作为编辑器中"游戏世界"的根节点,
|
||||
/// 持有 projectInformation / songInformation / beatmapContainer / commandScripts
|
||||
/// 等所有与谱面/游戏内容直接相关的数据。
|
||||
/// 由 EditorManager 在场景中持有引用,并通过 EditorManager 的转发属性对外暴露,
|
||||
/// 以实现零调用点改动的架构迁移。
|
||||
/// </summary>
|
||||
public class ProjectContainer : GameElement
|
||||
{
|
||||
#region [单例] Singleton
|
||||
private static ProjectContainer _instance;
|
||||
public static ProjectContainer instance =>
|
||||
_instance != null ? _instance : _instance = FindFirstObjectByType<ProjectContainer>();
|
||||
#endregion
|
||||
|
||||
#region [谱面数据] Project Data
|
||||
public ProjectInformation projectInformation;
|
||||
public SongInformation songInformation;
|
||||
public BeatmapContainer beatmapContainer;
|
||||
public CommandScripts commandScripts;
|
||||
#endregion
|
||||
|
||||
#region [游戏全局元素] Global Game Elements
|
||||
[Title("Runtime Global Elements")]
|
||||
public VariablesContainer variablesContainer;
|
||||
public BackgroundSetter backgroundSetter;
|
||||
#endregion
|
||||
|
||||
#region [编辑器首选项] Editor Preferences
|
||||
// 这些首选项字段放在 ProjectContainer 中,以便 SetUpInspector 将 this 作为 IBaseElement owner 传给 GenerateToggle
|
||||
public NoteBase.NoteJudgeType currentJudgeType;
|
||||
public bool useClickSelect;
|
||||
public bool useNotePrefab;
|
||||
public bool ExpandWhileClick;
|
||||
public bool useQuickMove;
|
||||
#endregion
|
||||
|
||||
#region [生成与初始化] Generation & Initialization
|
||||
private void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region [编辑器界面] Inspector
|
||||
public override void SetUpInspector()
|
||||
{
|
||||
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
|
||||
|
||||
var container = inspector.GenerateContainer("Editor Manager");
|
||||
|
||||
var inGameSettings = container.GenerateSubcontainer(3);
|
||||
inspector.GenerateDropdown(this, inGameSettings, "Judge Type",
|
||||
typeof(NoteBase.NoteJudgeType), nameof(currentJudgeType))
|
||||
.AddListenerFunction(() =>
|
||||
{
|
||||
foreach (GameElement gameElement in beatmapContainer.gameElementList)
|
||||
{
|
||||
if (gameElement is NoteVisualBase noteVisual)
|
||||
{
|
||||
noteVisual.Recover();
|
||||
}
|
||||
}
|
||||
});
|
||||
inspector.GenerateToggle(this, inGameSettings, "Use Note Prefab", nameof(useNotePrefab));
|
||||
inspector.GenerateToggle(this, inGameSettings, "Use Click Select", nameof(useClickSelect));
|
||||
inspector.GenerateToggle(this, inGameSettings, "Expand Tab While Click", nameof(ExpandWhileClick));
|
||||
inspector.GenerateToggle(this, inGameSettings, "Use Quick Move", nameof(useQuickMove));
|
||||
|
||||
var generation = container.GenerateSubcontainer(3);
|
||||
inspector.GenerateButton(this, generation, "Generate Folder",
|
||||
() => ElementFolder.GenerateElement("Folder", Guid.NewGuid(), new List<string>(), true, null));
|
||||
inspector.GenerateButton(this, generation, "Generate Background Setter",
|
||||
() => BackgroundSetter.GenerateElement("Background Setter", Guid.NewGuid(),
|
||||
new List<string>(), true, null, false, "basic", "Skybox", "Background"));
|
||||
inspector.GenerateButton(this, generation, "Generate Variables Container",
|
||||
() => VariablesContainer.GenerateElement("Variables Container", Guid.NewGuid(),
|
||||
new List<string>(), true, null, new Dictionary<string, int>()));
|
||||
|
||||
projectInformation.SetUpInspector();
|
||||
songInformation.SetUpInspector();
|
||||
EditorManager.instance.cameraManager.SetUpInspector();
|
||||
|
||||
var oo = inspector.GenerateContainer("Grid");
|
||||
var p = oo.GenerateSubcontainer(3);
|
||||
var po = inspector.GenerateToggle(this, p, "Enable Grid");
|
||||
po.AddListenerFunction(() =>
|
||||
{
|
||||
EditorManager.instance.gridController.gameObject.SetActive(po.toggle.isOn);
|
||||
});
|
||||
var o = inspector.GenerateInputField(p, "Grid Size",
|
||||
(EditorManager.instance.gridController.baseGridSize * 10).ToString());
|
||||
o.AddListenerFunction(() =>
|
||||
{
|
||||
EditorManager.instance.gridController.baseGridSize = float.Parse(o.inputField.text) / 10;
|
||||
});
|
||||
inspector.GenerateToggle(this, p, "Show Coordinates",
|
||||
nameof(EditorManager.instance.gridController) + "." + nameof(EditorManager.instance.gridController.showCoordinates));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Manager/ProjectContainer.cs.meta
Normal file
2
Assets/Scripts/Manager/ProjectContainer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4dfe4d6abafce9449218211cb48dadb
|
||||
@@ -2,6 +2,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ichni.StartMenu;
|
||||
using SLSUtilities.General;
|
||||
using TMPro;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
@@ -10,9 +11,10 @@ using UnityEngine.Serialization;
|
||||
|
||||
namespace Ichni
|
||||
{
|
||||
public class ThemeBundleManager : MonoBehaviour
|
||||
public class ThemeBundleManager : Singleton<ThemeBundleManager>
|
||||
{
|
||||
public static ThemeBundleManager instance;
|
||||
/// <summary>小写别名,兼容现有调用点</summary>
|
||||
public new static ThemeBundleManager instance => Instance;
|
||||
|
||||
public List<ThemeBundleAbstract> themeBundleAbstractList;
|
||||
public List<string> selectedThemeBundleList;
|
||||
@@ -22,23 +24,13 @@ namespace Ichni
|
||||
|
||||
public TMP_Text LogWindow;
|
||||
|
||||
private void Awake()
|
||||
protected override void Awake()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else if (instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
Initialize(true); // DontDestroyOnLoad 持久化单例
|
||||
|
||||
loadedThemeBundleList = new List<ThemeBundle>();
|
||||
AssetBundle.UnloadAllAssetBundles(true);
|
||||
LoadAllThemeBundlesAbstract();
|
||||
//LoadThemeBundle("basic");
|
||||
//LoadThemeBundle("departure_to_multiverse");
|
||||
}
|
||||
|
||||
public bool TryGetThemeBundle(string themeBundleName, out ThemeBundle themeBundle)
|
||||
|
||||
96
Assets/Scripts/Manager/TrackManager.cs
Normal file
96
Assets/Scripts/Manager/TrackManager.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
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);
|
||||
// 注意:TrackHeadPoint / TrackPercentPoint 在编辑器中无对应的逐帧更新逻辑,暂不加入
|
||||
#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);
|
||||
#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);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Manager/TrackManager.cs.meta
Normal file
2
Assets/Scripts/Manager/TrackManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3435f487f7c1c654e93adba65c7de915
|
||||
Reference in New Issue
Block a user