Files
ichni_Creator_Studio/Assets/Scripts/Editor Tools/FastNoteTracker/FaseNoteTracker.cs

634 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
foreach (var preview in NotePreviews.Values)
{
if (preview != null) preview.gameObject.SetActive(showNotePreview);
}
Refresh();
}
}
public bool showNotePreview
{
get => _showNotePreview;
set
{
_showNotePreview = value;
foreach (var preview in NotePreviews.Values)
{
if (preview != null) preview.gameObject.SetActive(showNotePreview);
}
Refresh();
}
}
private bool _showNotePreview = false;
BaseElement_BM IBeChangeInExport.MatchingExportElement { get => null; set { } }
// NotePreview和TextHint的容器
private Transform _previewRoot;
private Transform PreviewRoot
{
get
{
if (_previewRoot == null)
{
var obj = GameObject.Find("NotePreviewRoot");
if (obj == null)
{
obj = new GameObject("NotePreviewRoot");
obj.transform.SetParent(this.transform, false);
}
_previewRoot = obj.transform;
}
return _previewRoot;
}
}
private class NotePreviewData : MonoBehaviour
{
public float noteTime;
public NoteBase noteBase1;
public LineRenderer lineRenderer;
public void Initialize(float time, NoteBase noteBase)
{
noteTime = time;
noteBase1 = noteBase;
lineRenderer = this.gameObject.AddComponent<LineRenderer>();
lineRenderer.useWorldSpace = false;
lineRenderer.positionCount = 2;
lineRenderer.startWidth = 1f;
lineRenderer.endWidth = 0f;
lineRenderer.material = EditorManager.instance.basePrefabs.defaultTrackMaterial;
Color color = new Color(1f, 0.5f, 0f, 0.8f);
DOTween.ToAlpha(() => color, c => color = c, 1f, 1f).SetEase(Ease.InOutQuad).OnUpdate(() =>
{
lineRenderer.startColor = lineRenderer.endColor = color;
});
}
public void Refresh(SplineComputer spline, TrackTimeSubmoduleMovable trackTime, float horizonWidth)
{
float trackPercent = Mathf.Clamp01(trackTime.GetTrackPercentRaw(noteTime));
SplineSample sample = spline.Evaluate(trackPercent);
Vector3 sideOffset = sample.rotation * (noteBase1.noteVisual != null ? noteBase1.noteVisual.transformSubmodule.originalPosition : Vector3.zero);
Vector3 worldPos = sample.position;
Vector3 localPos = this.transform.parent.InverseTransformPoint(worldPos);
Vector3 localOffset = this.transform.parent.InverseTransformVector(Vector3.up);
lineRenderer.SetPosition(0, localPos + sideOffset);
lineRenderer.SetPosition(1, localPos + localOffset + sideOffset);
}
void OnDestroy() { }
}
// NotePreviews字典改为以时间为key
private Dictionary<float, NotePreviewData> NotePreviews = new Dictionary<float, NotePreviewData>();
public List<LineRenderer> BeatLines = new List<LineRenderer>();
public List<float> Beats = new List<float>();
public override void Refresh()
{
base.Refresh();
UpdateBeatLine();
RefreshNotePreviews();
}
// 刷新NotePreviews按时间生成预览
private void RefreshNotePreviews()
{
if (TrackedTrack == null) return;
if (!_isEnabled) return;
if (!showNotePreview) return;
var notes = TrackedTrack.childElementList.OfType<NoteBase>().ToList();
var times = notes.Select(n => n.exactJudgeTime).ToList();
// 移除已不存在的note时间
var toRemove = NotePreviews.Keys.Except(times).ToList();
foreach (var t in toRemove)
{
if (NotePreviews[t] != null) Destroy(NotePreviews[t].gameObject);
NotePreviews.Remove(t);
}
// 添加新note时间
foreach (var n in notes)
{
float time = n.exactJudgeTime;
if (!NotePreviews.ContainsKey(time))
{
var obj = new GameObject($"NotePreview_{n.elementName}_{time}");
obj.transform.SetParent(PreviewRoot, false);
var preview = obj.AddComponent<NotePreviewData>();
preview.Initialize(time, n);
NotePreviews[time] = preview;
}
}
// 刷新所有预览
SplineComputer spline = TrackedTrack.trackPathSubmodule.path;
var trackTime = TrackedTrack.trackTimeSubmodule as TrackTimeSubmoduleMovable;
foreach (var preview in NotePreviews.Values)
{
if (preview != null)
{
Observable.NextFrame().Subscribe(_ =>
{
preview.Refresh(spline, trackTime, horizonWidth);
});
}
}
}
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();
RefreshNotePreviews();
}
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;
GameObject hintObj = new GameObject("NoteHint_" + content);
hintObj.transform.SetParent(PreviewRoot, false); // 统一放到PreviewRoot下
// 2. 添加 TextMeshPro 组件 (注意是 TextMeshPro不是 TextMeshProUGUI)
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);
// 位置同步协程
StartCoroutine(SyncHintPosition(hintObj.transform, noteBase));
text.DOFade(0, 1f).OnComplete(() => Destroy(hintObj));
// 让文字始终面向相机 (Billboard 效果)
hintObj.transform.LookAt(hintObj.transform.position + Camera.main.transform.rotation * Vector3.forward,
EditorManager.instance.cameraManager.currentCamera.transform.rotation * Vector3.up);
}
private IEnumerator SyncHintPosition(Transform hint, NoteBase noteBase)
{
float t = 0f;
Vector3 offset = Vector3.up;
while (t < 1f && noteBase != null && noteBase.noteVisual != null)
{
hint.position = noteBase.noteVisual.transform.position + offset;
t += Time.deltaTime;
yield return null;
}
}
}
// 后面的 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));
inspector.GenerateToggle(this, sub, "Show Note Preview", nameof(showNotePreview));
}
public static FastNoteTracker GenerateElement(string elementName, Guid id, List<string> tags,
bool isFirstGenerated, GameElement parentElement,
bool isEnabled = true, bool showNotePreview = false, float horizonWidth = 5f, int beatDiver = 1)
{
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);
fastNoteTracker.IsEnabled = isEnabled;
fastNoteTracker.showNotePreview = showNotePreview;
fastNoteTracker.horizonWidth = horizonWidth;
fastNoteTracker.BeatDiver = beatDiver;
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 AfterInitialize()
{
Observable.NextFrame().Subscribe(_ => Refresh());
}
public override void SaveBM()
{
matchedBM = new FastNoteTracker_BM(
elementName,
elementGuid,
tags,
parentElement.elementGuid,
IsEnabled,
showNotePreview,
horizonWidth,
BeatDiver
);
}
}
}
namespace Ichni.RhythmGame.Beatmap
{
public class FastNoteTracker_BM : GameElement_BM
{
public bool IsEnabled { get; set; }
public bool showNotePreview { get; set; }
public float horizonWidth { get; set; }
public int beatDiver { get; set; }
public FastNoteTracker_BM(string elementName, Guid id, List<string> tags, Guid attachedElementGuid, bool IsEnabled, bool showNotePreview, float horizonWidth, int beatDiver)
{
this.elementName = elementName;
this.elementGuid = id;
this.tags = tags;
this.attachedElementGuid = attachedElementGuid;
this.IsEnabled = IsEnabled;
this.showNotePreview = showNotePreview;
this.horizonWidth = horizonWidth;
this.beatDiver = beatDiver;
}
public override GameElement DuplicateBM(GameElement attached)
{
return FastNoteTracker.GenerateElement(elementName, Guid.NewGuid(), tags, false, attached);
}
public override void ExecuteBM()
{
FastNoteTracker.GenerateElement(elementName, elementGuid, tags, false, GetElement(attachedElementGuid));
}
}
}