这能叫例行更新吗

Signed-off-by: TRAfoer <lhf190@outlook.com>
This commit is contained in:
2026-02-09 23:10:55 +08:00
parent 77726bcb6c
commit a76f650998
40 changed files with 1323 additions and 866 deletions

View File

@@ -0,0 +1,473 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using Dreamteck;
using Dreamteck.Splines;
using Dreamteck.Splines.Primitives;
using ichni.RhythmGame;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using Ichni.RhythmGame.Beatmap;
using TMPro;
using UniRx;
using Unity.VisualScripting;
// [修复] 统一命名空间引用
using UnityEngine;
using UnityEngine.InputSystem;
namespace ichni.RhythmGame // [修复] 修正命名空间首字母大写符合C#规范
{
public partial class FastNoteTracker : GameElement, IBeChangeInExport
{
public Track TrackedTrack { get => parentElement as Track; }
private bool _isEnabled = false;
public float horizonWidth = 5f;
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
Refresh();
}
}
BaseElement_BM IBeChangeInExport.MatchingExportElement { get => null; set { } }
public List<LineRenderer> BeatLines = new List<LineRenderer>();
public List<float> Beats = new List<float>();
public override void Refresh()
{
base.Refresh();
UpdateBeatLine();
}
public int BeatDiver = 1; // [建议] 变量名修正为 BeatDiver (原本是 Beatdiver)
private LineRenderer selectedLine = null;
// 工具方法:计算线条的两个端点位置
private void CalculateLinePositions(SplineSample sample, out Vector3 pos0, out Vector3 pos1)
{
Vector3 sideOffset = sample.rotation * Vector3.left * horizonWidth;
pos0 = sample.position + sideOffset;
pos1 = sample.position - sideOffset;
}
// 工具方法:配置 LineRenderer 的基础属性
private void SetupLineRenderer(LineRenderer line, Vector3 pos0, Vector3 pos1, Color color, bool refreshWidth = true)
{
line.useWorldSpace = true;
line.positionCount = 2;
line.SetPosition(0, pos0);
line.SetPosition(1, pos1);
if (refreshWidth)
{
line.startWidth = 0.05f;
line.endWidth = 0.05f;
}
line.material = EditorManager.instance.basePrefabs.defaultTrailMaterial;
line.startColor = line.endColor = color;
}
private void RefreshMeshCollider(LineRenderer lineA, LineRenderer lineB)
{
MeshCollider col = lineA.GetComponent<MeshCollider>();
if (col == null) col = lineA.gameObject.AddComponent<MeshCollider>();
Vector3 a1 = lineA.GetPosition(0);
Vector3 a2 = lineA.GetPosition(1);
Vector3 b1 = lineB.GetPosition(0);
Vector3 b2 = lineB.GetPosition(1);
if (Vector3.Distance(a1, b1) < 0.001f)
{
if (col.sharedMesh != null) col.sharedMesh.Clear();
return;
}
// 转换为局部坐标
Vector3[] newVertices = new Vector3[] {
lineA.transform.InverseTransformPoint(a1),
lineA.transform.InverseTransformPoint(a2),
lineA.transform.InverseTransformPoint(b1),
lineA.transform.InverseTransformPoint(b2)
};
Mesh mesh = col.sharedMesh;
if (mesh == null)
{
mesh = new Mesh();
mesh.name = "BeatLineMesh";
}
else
{
// [关键优化]:如果新旧顶点差异极小,则不刷新 Mesh防止物理系统抖动
if (mesh.vertexCount == 4 && Vector3.Distance(mesh.vertices[0], newVertices[0]) < 0.0001f)
{
return;
}
mesh.Clear();
}
mesh.vertices = newVertices;
mesh.triangles = new int[] { 0, 1, 2, 2, 1, 3, 2, 1, 0, 3, 1, 2 };
mesh.RecalculateNormals();
mesh.RecalculateBounds();
col.sharedMesh = mesh;
// 确保 Tag 正确
if (!lineA.CompareTag("LineRenderer")) lineA.tag = "LineRenderer";
}
public void UpdateBeatLine()
{
// 1. 清理
foreach (var line in BeatLines) if (line != null) Destroy(line.gameObject);
BeatLines.Clear();
Beats.Clear();
if (!_isEnabled) return;
// 获取数据源
SplineComputer splineComputer = TrackedTrack.trackPathSubmodule.path;
var trackTime = TrackedTrack.trackTimeSubmodule as TrackTimeSubmoduleMovable;
float beatStart = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackStartTime);
float beatEnd = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime);
// 2. 批量生成
for (float b = beatStart - 1; b <= beatEnd + 1f / BeatDiver; b += 1f / BeatDiver)
{
float timeAtBeat = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(b);
float trackPercent = Mathf.Clamp01(trackTime.GetTrackPercentRaw(timeAtBeat));
CalculateLinePositions(splineComputer.Evaluate(trackPercent), out Vector3 p0, out Vector3 p1);
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
LineRenderer lr = obj.AddComponent<LineRenderer>();
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
SetupLineRenderer(lr, p0, p1, color);
float bi = (trackPercent >= 1f) ? EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime) : b;
Beats.Add(bi < 0 ? 0f : bi);
BeatLines.Add(lr);
}
// 3. 批量生成 Collider
for (int i = 0; i < BeatLines.Count - 1; i++) RefreshMeshCollider(BeatLines[i], BeatLines[i + 1]);
}
public void AdjustBeatLine()
{
if (!_isEnabled)
{
foreach (var line in BeatLines) if (line != null) line.gameObject.SetActive(false);
return;
}
SplineComputer splineComputer = TrackedTrack.trackPathSubmodule.path;
var trackTime = TrackedTrack.trackTimeSubmodule as TrackTimeSubmoduleMovable;
float beatStart = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackStartTime);
float beatEnd = EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime);
int index = 0;
// 重置 Beats 列表以匹配新的位置
Beats.Clear();
for (float b = beatStart - 1; b <= beatEnd + 1f / BeatDiver; b += 1f / BeatDiver)
{
float timeAtBeat = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(b);
float trackPercent = Mathf.Clamp01(trackTime.GetTrackPercentRaw(timeAtBeat));
// 如果超出范围,跳过更新(除非你想一直显示)
// 这里沿用你 UpdateBeatLine 的逻辑
CalculateLinePositions(splineComputer.Evaluate(trackPercent), out Vector3 p0, out Vector3 p1);
LineRenderer lr;
if (index < BeatLines.Count)
{
lr = BeatLines[index];
lr.gameObject.SetActive(true);
}
else
{
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
lr = obj.AddComponent<LineRenderer>();
BeatLines.Add(lr);
}
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
SetupLineRenderer(lr, p0, p1, color, false);
float bi = (trackPercent >= 1f) ? EditorManager.instance.songInformation.beatManager.GetBeatFromTime(trackTime.trackEndTime) : b;
Beats.Add(bi < 0 ? 0f : bi);
index++;
}
for (int i = index; i < BeatLines.Count; i++) BeatLines[i].gameObject.SetActive(false);
for (int i = 0; i < index - 1; i++) RefreshMeshCollider(BeatLines[i], BeatLines[i + 1]);
}
public bool ForceRefresh = false;
void Update()
{
if (ForceRefresh)
{
AdjustBeatLine();
}
if (InputListener.instance.isPointerOverUI) return;
CastRay();
if (IsEnabled && selectedLine != null)
{
DetectNote();
}
}
public void CastRay()
{
if (EditorManager.instance.cameraManager.currentCamera == null) return;
Ray ray = EditorManager.instance.cameraManager.currentCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
RaycastHit hit;
// Debug.DrawRay(ray.origin, ray.direction * 100f, Color.red);
if (Physics.RaycastAll(ray).FirstOrDefault(h => h.collider.CompareTag("LineRenderer")).collider != null)
{
hit = Physics.RaycastAll(ray).First(h => h.collider.CompareTag("LineRenderer"));
LineRenderer hoveredLine = hit.collider.GetComponent<LineRenderer>();
if (BeatLines.Contains(hoveredLine))
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
Debug.Log($"Clicked on line area: {hoveredLine.gameObject.name} at {hit.point}");
}
if (selectedLine != hoveredLine)
{
if (selectedLine != null)
{
selectedLine.startWidth = 0.05f;
selectedLine.endWidth = 0.05f;
}
selectedLine = hoveredLine;
selectedLine.startWidth = 0.15f;
selectedLine.endWidth = 0.15f;
}
}
}
else
{
if (selectedLine != null)
{
selectedLine.startWidth = 0.05f;
selectedLine.endWidth = 0.05f;
selectedLine = null;
}
}
}
public void InputDetected()
{
// if(Keyboard)
}
public void DetectNote()
{
if (Keyboard.current.digit1Key.wasPressedThisFrame)
AddNote(0);
else if (Keyboard.current.digit2Key.wasPressedThisFrame)
AddNote(1);
else if (Keyboard.current.digit3Key.wasPressedThisFrame)
AddNote(2);
else if (Keyboard.current.digit4Key.wasPressedThisFrame)
AddNote(3);
}
public void AddNote(int NoteCode)
{
if (!EditorManager.instance.useNotePrefab)
{
LogWindow.Log("Please enable \"Note Prefab\" in EditorManager", Color.red);
return;
}
if (selectedLine == null) return;
float time = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(
Beats[BeatLines.IndexOf(selectedLine)]
);
NoteBase a = null;
switch (NoteCode)
{
case 0:
a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
a.noteVisual.transformSubmodule.Refresh();
break;
case 3:
a = Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, time + 0.5f);
a.noteVisual.transformSubmodule.Refresh();
Observable.NextFrame().Subscribe(_ =>
{
StartCoroutine(DraggingHold((Hold)a));
});
break;
case 1:
a = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
a.noteVisual.transformSubmodule.Refresh();
break;
case 2:
a = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, new List<Vector2>());
a.noteVisual.transformSubmodule.Refresh();
break;
default:
break;
}
Observable.NextFrame().Subscribe(_ =>
{
CreateTextHint(a);
});
}
private IEnumerator DraggingHold(Hold hold)
{
GameObject obj = Instantiate(EditorManager.instance.basePrefabs.emptyObject, this.transform);
LineRenderer lr = obj.AddComponent<LineRenderer>();
lr.startWidth = lr.endWidth = 0.05f;
lr.positionCount = 2;
lr.material = EditorManager.instance.basePrefabs.defaultTrailMaterial;
lr.startColor = lr.endColor = Color.yellow;
lr.SetPosition(0, hold.noteVisual.transform.position);
lr.SetPosition(1, hold.noteVisual.transform.position);
while (Keyboard.current.digit4Key.isPressed && selectedLine != null)
{
yield return null;
try
{
float time = EditorManager.instance.songInformation.beatManager.GetTimeFromBeat(
Beats[BeatLines.IndexOf(selectedLine)]
);
hold.holdEndTime = time < hold.exactJudgeTime ? hold.exactJudgeTime + 0.1f : time;
hold.noteVisual.transformSubmodule.Refresh();
lr.SetPosition(1, (selectedLine.GetPosition(0) + selectedLine.GetPosition(1)) / 2);
}
catch (Exception e)
{
Debug.LogWarning(e);
break;
}
}
yield return null;
Destroy(lr.gameObject);
CreateTextHint(hold);
}
private void CreateTextHint(NoteBase noteBase)
{
// 1. 创建一个新的 GameObject
string content = noteBase.elementName;
Vector3 worldPos = noteBase.noteVisual.transform.position;
GameObject hintObj = new GameObject("NoteHint_" + content);
hintObj.transform.position = worldPos;
// 2. 添加 TextMeshPro 组件 (注意是 TextMeshPro不是 TextMeshProUGUI)
// 因为我们在 3D 空间生成,所以使用非 UI 版本
TextMeshPro text = hintObj.AddComponent<TextMeshPro>();
// 3. 配置文字属性
text.text = content;
text.fontSize = 6; // 根据你的缩放调整大小
text.alignment = TextAlignmentOptions.Center;
text.color = Color.yellow;
// 4. (可选) 设置渲染层级,确保它在轨道上方显示
// text.sortingOrder = 100;
// 5. 动画效果:向上移动 1 个单位,并同时淡出
// 1 秒后执行
hintObj.transform.DOScale(Vector3.one * 1.2f, 1f).SetEase(Ease.OutBack).From(Vector3.zero);
hintObj.transform.DOMoveY(worldPos.y + 1f, 1f);
text.DOFade(0, 1f).OnComplete(() => Destroy(hintObj));
// 7. 让文字始终面向相机 (Billboard 效果)
// 如果你的相机视角是固定的,可以直接设置旋转
hintObj.transform.LookAt(hintObj.transform.position + Camera.main.transform.rotation * Vector3.forward,
EditorManager.instance.cameraManager.currentCamera.transform.rotation * Vector3.up);
}
}
// 后面的 Inspector 和 Export 代码逻辑暂时不需要大改,只要注意变量名 Beatdiver -> BeatDiver
public partial class FastNoteTracker
{
public BaseElement_BM MatchingExportElement { get => null; set => throw new System.NotImplementedException(); }
public void SaveExportBM() { }
public override void SetUpInspector()
{
base.SetUpInspector();
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
var container = inspector.GenerateContainer("Fast Note Tracker");
var sub = container.GenerateSubcontainer(2);
inspector.GenerateToggle(this, sub, "Enabled", nameof(IsEnabled));
// 修正变量名
inspector.GenerateInputField(this, sub, "Beat Diver", nameof(BeatDiver));
inspector.GenerateToggle(this, sub, "Force refresh (cost++)", nameof(ForceRefresh));
inspector.GenerateInputField(this, sub, "Horizon Width", nameof(horizonWidth));
}
public static FastNoteTracker GenerateElement(string elementName, Guid id, List<string> tags,
bool isFirstGenerated, GameElement parentElement)
{
FastNoteTracker fastNoteTracker = Instantiate(EditorManager.instance.basePrefabs.emptyObject, parentElement.transform)
.AddComponent<FastNoteTracker>();
if (parentElement is not Track)
{
LogWindow.Log("FastNoteTracker must be a child of Track element.");
return null;
}
fastNoteTracker.Initialize(elementName, id, tags, isFirstGenerated, parentElement);
return fastNoteTracker;
}
public override void Initialize(string elementName, Guid id, List<string> tags, bool isFirstGenerated, GameElement parentElement)
{
base.Initialize(elementName, id, tags, isFirstGenerated, parentElement);
//parentElement.refreshAction += AdjustBeatLine;
}
public override void SaveBM()
{
matchedBM = new FastNoteTracker_BM(elementName, elementGuid, tags, parentElement.elementGuid);
}
}
}
namespace Ichni.RhythmGame.Beatmap
{
public class FastNoteTracker_BM : GameElement_BM
{
public FastNoteTracker_BM(string elementName, Guid id, List<string> tags, Guid attachedElementGuid)
{
this.elementName = elementName;
this.elementGuid = id;
this.tags = tags;
this.attachedElementGuid = attachedElementGuid;
}
public override GameElement DuplicateBM(GameElement attached)
{
return FastNoteTracker.GenerateElement(elementName, elementGuid, tags, false, attached);
}
public override void ExecuteBM()
{
FastNoteTracker.GenerateElement(elementName, elementGuid, tags, false, GetElement(attachedElementGuid));
}
}
}