This commit is contained in:
SoulliesOfficial
2026-03-14 02:30:26 -04:00
parent cf86f0ee51
commit aee62cd637
2041 changed files with 246771 additions and 129128 deletions

View 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
}
}

View File

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

View File

@@ -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
}
}

View File

@@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
executionOrder: -10
icon: {instanceID: 0}
userData:
assetBundleName:

View File

@@ -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();
}

View File

@@ -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
}
}

View File

@@ -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>注册一个新 NoteisNewOne = 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
}
}

View File

@@ -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.");
}
}
}
}

View 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
}
}

View File

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

View File

@@ -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)

View 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
}
}

View File

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