2026-01-07 23:48:28 +08:00
|
|
|
|
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;
|
2026-01-18 13:11:38 +08:00
|
|
|
|
using UniRx;
|
|
|
|
|
|
using Unity.VisualScripting;
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-07 23:48:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// [修复] 统一命名空间引用
|
|
|
|
|
|
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;
|
2026-02-11 01:26:10 +08:00
|
|
|
|
foreach (var preview in NotePreviews.Values)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (preview != null) preview.gameObject.SetActive(showNotePreview);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-07 23:48:28 +08:00
|
|
|
|
Refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 01:26:10 +08:00
|
|
|
|
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;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
BaseElement_BM IBeChangeInExport.MatchingExportElement { get => null; set { } }
|
|
|
|
|
|
|
2026-02-11 01:26:10 +08:00
|
|
|
|
// 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>();
|
2026-01-07 23:48:28 +08:00
|
|
|
|
public List<LineRenderer> BeatLines = new List<LineRenderer>();
|
|
|
|
|
|
public List<float> Beats = new List<float>();
|
|
|
|
|
|
public override void Refresh()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Refresh();
|
|
|
|
|
|
UpdateBeatLine();
|
2026-02-11 01:26:10 +08:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>();
|
|
|
|
|
|
|
2026-01-18 13:11:38 +08:00
|
|
|
|
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 13:11:38 +08:00
|
|
|
|
Color color = (Mathf.Abs(b - Mathf.Round(b)) <= 0.01f) ? Color.green : Color.cyan;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
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()
|
|
|
|
|
|
{
|
2026-01-18 13:11:38 +08:00
|
|
|
|
if (ForceRefresh)
|
|
|
|
|
|
{
|
|
|
|
|
|
AdjustBeatLine();
|
2026-02-11 01:26:10 +08:00
|
|
|
|
RefreshNotePreviews();
|
2026-01-18 13:11:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (InputListener.instance.isPointerOverUI) return;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
CastRay();
|
|
|
|
|
|
if (IsEnabled && selectedLine != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
DetectNote();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2026-02-11 01:26:10 +08:00
|
|
|
|
|
2026-01-07 23:48:28 +08:00
|
|
|
|
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"));
|
|
|
|
|
|
|
2026-01-18 13:11:38 +08:00
|
|
|
|
LineRenderer hoveredLine = hit.collider.GetComponent<LineRenderer>();
|
|
|
|
|
|
if (BeatLines.Contains(hoveredLine))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Mouse.current.leftButton.wasPressedThisFrame)
|
2026-01-07 23:48:28 +08:00
|
|
|
|
{
|
2026-01-18 13:11:38 +08:00
|
|
|
|
Debug.Log($"Clicked on line area: {hoveredLine.gameObject.name} at {hit.point}");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selectedLine != hoveredLine)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (selectedLine != null)
|
2026-01-07 23:48:28 +08:00
|
|
|
|
{
|
2026-01-18 13:11:38 +08:00
|
|
|
|
selectedLine.startWidth = 0.05f;
|
|
|
|
|
|
selectedLine.endWidth = 0.05f;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
2026-01-18 13:11:38 +08:00
|
|
|
|
selectedLine = hoveredLine;
|
|
|
|
|
|
selectedLine.startWidth = 0.15f;
|
|
|
|
|
|
selectedLine.endWidth = 0.15f;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-18 13:11:38 +08:00
|
|
|
|
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
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)]
|
|
|
|
|
|
);
|
2026-01-18 13:11:38 +08:00
|
|
|
|
NoteBase a = null;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
switch (NoteCode)
|
|
|
|
|
|
{
|
|
|
|
|
|
|
2026-01-18 13:11:38 +08:00
|
|
|
|
case 0:
|
|
|
|
|
|
a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
|
2026-01-07 23:48:28 +08:00
|
|
|
|
a.noteVisual.transformSubmodule.Refresh();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 3:
|
2026-01-18 13:11:38 +08:00
|
|
|
|
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));
|
|
|
|
|
|
});
|
2026-01-07 23:48:28 +08:00
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 1:
|
2026-01-18 13:11:38 +08:00
|
|
|
|
a = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time);
|
|
|
|
|
|
a.noteVisual.transformSubmodule.Refresh();
|
2026-01-07 23:48:28 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
2026-01-18 13:11:38 +08:00
|
|
|
|
a = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, TrackedTrack, time, new List<Vector2>());
|
|
|
|
|
|
a.noteVisual.transformSubmodule.Refresh();
|
2026-01-07 23:48:28 +08:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-01-18 13:11:38 +08:00
|
|
|
|
Observable.NextFrame().Subscribe(_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
CreateTextHint(a);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-07 23:48:28 +08:00
|
|
|
|
|
|
|
|
|
|
}
|
2026-01-18 13:11:38 +08:00
|
|
|
|
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)
|
2026-01-07 23:48:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 1. 创建一个新的 GameObject
|
2026-01-18 13:11:38 +08:00
|
|
|
|
string content = noteBase.elementName;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
GameObject hintObj = new GameObject("NoteHint_" + content);
|
2026-02-11 01:26:10 +08:00
|
|
|
|
hintObj.transform.SetParent(PreviewRoot, false); // 统一放到PreviewRoot下
|
2026-01-07 23:48:28 +08:00
|
|
|
|
// 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 秒后执行
|
2026-01-18 13:11:38 +08:00
|
|
|
|
hintObj.transform.DOScale(Vector3.one * 1.2f, 1f).SetEase(Ease.OutBack).From(Vector3.zero);
|
2026-02-11 01:26:10 +08:00
|
|
|
|
// 位置同步协程
|
|
|
|
|
|
StartCoroutine(SyncHintPosition(hintObj.transform, noteBase));
|
2026-01-07 23:48:28 +08:00
|
|
|
|
text.DOFade(0, 1f).OnComplete(() => Destroy(hintObj));
|
2026-02-11 01:26:10 +08:00
|
|
|
|
// 让文字始终面向相机 (Billboard 效果)
|
2026-01-07 23:48:28 +08:00
|
|
|
|
hintObj.transform.LookAt(hintObj.transform.position + Camera.main.transform.rotation * Vector3.forward,
|
|
|
|
|
|
EditorManager.instance.cameraManager.currentCamera.transform.rotation * Vector3.up);
|
|
|
|
|
|
}
|
2026-02-11 01:26:10 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 后面的 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));
|
2026-02-11 01:26:10 +08:00
|
|
|
|
inspector.GenerateToggle(this, sub, "Show Note Preview", nameof(showNotePreview));
|
|
|
|
|
|
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static FastNoteTracker GenerateElement(string elementName, Guid id, List<string> tags,
|
2026-02-13 17:40:50 +08:00
|
|
|
|
bool isFirstGenerated, GameElement parentElement,
|
|
|
|
|
|
bool isEnabled = true, bool showNotePreview = false, float horizonWidth = 5f, int beatDiver = 1)
|
2026-01-07 23:48:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
2026-02-13 17:40:50 +08:00
|
|
|
|
fastNoteTracker.IsEnabled = isEnabled;
|
|
|
|
|
|
fastNoteTracker.showNotePreview = showNotePreview;
|
|
|
|
|
|
fastNoteTracker.horizonWidth = horizonWidth;
|
|
|
|
|
|
fastNoteTracker.BeatDiver = beatDiver;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-02-13 17:40:50 +08:00
|
|
|
|
public override void AfterInitialize()
|
|
|
|
|
|
{
|
|
|
|
|
|
Observable.NextFrame().Subscribe(_ => Refresh());
|
|
|
|
|
|
}
|
2026-01-07 23:48:28 +08:00
|
|
|
|
|
|
|
|
|
|
public override void SaveBM()
|
|
|
|
|
|
{
|
2026-02-13 17:40:50 +08:00
|
|
|
|
matchedBM = new FastNoteTracker_BM(
|
|
|
|
|
|
elementName,
|
|
|
|
|
|
elementGuid,
|
|
|
|
|
|
tags,
|
|
|
|
|
|
parentElement.elementGuid,
|
|
|
|
|
|
IsEnabled,
|
|
|
|
|
|
showNotePreview,
|
|
|
|
|
|
horizonWidth,
|
|
|
|
|
|
BeatDiver
|
|
|
|
|
|
);
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.RhythmGame.Beatmap
|
|
|
|
|
|
{
|
|
|
|
|
|
public class FastNoteTracker_BM : GameElement_BM
|
|
|
|
|
|
{
|
2026-02-13 17:40:50 +08:00
|
|
|
|
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)
|
2026-01-07 23:48:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
this.elementName = elementName;
|
|
|
|
|
|
this.elementGuid = id;
|
|
|
|
|
|
this.tags = tags;
|
|
|
|
|
|
this.attachedElementGuid = attachedElementGuid;
|
2026-02-13 17:40:50 +08:00
|
|
|
|
this.IsEnabled = IsEnabled;
|
|
|
|
|
|
this.showNotePreview = showNotePreview;
|
|
|
|
|
|
this.horizonWidth = horizonWidth;
|
|
|
|
|
|
this.beatDiver = beatDiver;
|
2026-01-07 23:48:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|