using System; using System.Collections; using System.Collections.Generic; using Dreamteck.Splines; using Ichni.RhythmGame.Beatmap; using Sirenix.OdinInspector; using Unity.VisualScripting; using UnityEngine; using UnityEngine.Splines; using Spline = Dreamteck.Splines.Spline; namespace Ichni.RhythmGame { using System.Collections.Generic; using Unity.Mathematics; using UnityEngine; // 引入官方的 Splines 命名空间 using UnitySpline = UnityEngine.Splines.Spline; using UnitySplineContainer = UnityEngine.Splines.SplineContainer; public partial class TrackPathSubmodule : TrackSubmodule { public Dreamteck.Splines.SplineComputer path; public List pathNodeList; public Track.TrackSpaceType trackSpaceType; public Track.TrackSamplingType trackSamplingType; public bool isClosed; public bool refreshedThisFrame = false; public bool isShowingDisplay; public TrackPathSubmodule(Track track, Track.TrackSpaceType trackSpaceType, Track.TrackSamplingType trackSamplingType, bool isClosed, bool isShowingDisplay) : base(track) { this.path = track.AddComponent(); this.pathNodeList = new List(); this.trackSpaceType = trackSpaceType; this.trackSamplingType = trackSamplingType; this.isClosed = isClosed; this.path.sampleRate = 8; this.path.updateMode = SplineComputer.UpdateMode.LateUpdate; SetUpSplineComputer(this.trackSpaceType, this.trackSamplingType); //闭合路径在PathNode生成时执行,在初始化的情况下,PathNode数量为0,不会执行闭合操作 this.isShowingDisplay = isShowingDisplay; if (!HaveSameSubmodule) { this.track.trackPathSubmodule = this; } this.container = track.AddComponent(); } } public partial class TrackPathSubmodule { private void SetUpSplineComputer(Track.TrackSpaceType trackSpaceType, Track.TrackSamplingType trackSamplingType) { path.type = (Spline.Type)trackSpaceType; path.sampleMode = (SplineComputer.SampleMode)(int)trackSamplingType; path.space = SplineComputer.Space.Local; } public void ClosePath() { if (isClosed) { path.Close(); } else { path.Break(); } } public void SetTrackSpaceType(int spaceType) { int SpaceType = spaceType; if (spaceType == 2) SpaceType++; trackSpaceType = (Track.TrackSpaceType)SpaceType; path.type = (Spline.Type)SpaceType; } public void SetPathNode(PathNode point) { path.SetPoint(point.index, point.node, SplineComputer.Space.Local); } public override void Refresh() { if(refreshedThisFrame) return; refreshedThisFrame = true; SetTrackSpaceType((int)trackSpaceType); SetUpSplineComputer(trackSpaceType, trackSamplingType); foreach (var pathNode in pathNodeList) { SetPathNode(pathNode); } ClosePath(); path.RebuildImmediate(true, true); } } public partial class TrackPathSubmodule { public override void SaveBM() { matchedBM = new TrackPathSubmodule_BM(attachedGameElement, this); } } public partial class TrackPathSubmodule { public UnitySplineContainer container; public bool isSplineDirty = false; // 这个标记可以在外部被设置为 true 来触发表现层更新 public void GenerateCatmullRomSpline() { UnitySpline spline = new UnitySpline(); int count = pathNodeList.Count; for (int i = 0; i < count; i++) { //使用Auto Knot模式,Unity会自动计算切线以实现Catmull-Rom样条的特性 BezierKnot knot = new BezierKnot(); knot.Position = pathNodeList[i].transformSubmodule.currentPosition; knot.Rotation = quaternion.identity; //初始旋转,后续可以根据需要 spline.Add(knot); } //必须设置为Auto Knot模式,让Unity自动计算切线以实现Catmull-Rom样条的特性 spline.SetTangentMode(TangentMode.AutoSmooth); container.Splines = new UnitySpline[] { spline }; } public void GenerateLinearSpline() { UnitySpline spline = new UnitySpline(); int count = pathNodeList.Count; for (int i = 0; i < count; i++) { BezierKnot knot = new BezierKnot(); knot.Position = pathNodeList[i].transformSubmodule.currentPosition; knot.Rotation = quaternion.identity; //初始旋转,后续可以根据需要 spline.Add(knot); } //必须设置为Linear模式,让Unity不计算切线,保持线性插值 spline.SetTangentMode(TangentMode.Linear); container.Splines = new UnitySpline[] { spline }; } [Button] public void GenerateBSpline() { //if(pathNodeList.Count < 3) return; UnitySpline spline = new UnitySpline(); int count = pathNodeList.Count; for (int i = 0; i < count; i++) { spline.Add(CalculateBSplineKnot(i)); } // 必须设置为 Broken 模式,因为我们已经手动用数学算出了完美平滑的 Tangent,不需要 Unity 再去插手 spline.SetTangentMode(TangentMode.Broken); container.Splines = new UnitySpline[] { spline }; } private BezierKnot CalculateBSplineKnot(int i) { UnitySpline spline = new UnitySpline(); int count = pathNodeList.Count; Vector3 pPrev, pCurr, pNext; pCurr = pathNodeList[i].transformSubmodule.currentPosition; // 1. 获取前后相邻点 (处理封闭与开放的拓扑逻辑) if (isClosed) { // 使用取模运算,让索引在首尾循环缠绕 (加 count 是为了防止负数) pPrev = pathNodeList[(i - 1 + count) % count].transformSubmodule.currentPosition; pNext = pathNodeList[(i + 1) % count].transformSubmodule.currentPosition; } else { // 开放状态下,首尾节点重复使用端点坐标 pPrev = pathNodeList[Mathf.Max(0, i - 1)].transformSubmodule.currentPosition; pNext = pathNodeList[Mathf.Min(count - 1, i + 1)].transformSubmodule.currentPosition; } BezierKnot knot = new BezierKnot(); // 2. 计算 B-Spline 等价的 Bezier 节点位置与切线 if (!isClosed && (i == 0 || i == count - 1)) { // 开放曲线的硬性端点锚定 knot.Position = pCurr; if (i == 0) { knot.TangentIn = float3.zero; knot.TangentOut = (pNext - pCurr) / 3f; } else { knot.TangentIn = (pPrev - pCurr) / 3f; knot.TangentOut = float3.zero; } } else { // 标准 1/6 B-Spline 平滑换算公式 (封闭曲线全程走这里) knot.Position = (pPrev + 4f * pCurr + pNext) / 6f; knot.TangentOut = (pNext - pPrev) / 6f; knot.TangentIn = (pPrev - pNext) / 6f; } knot.Rotation = Quaternion.Euler(pathNodeList[i].transformSubmodule.currentEulerAngles); // 直接使用节点的欧拉角作为旋转,保持与节点编辑器一致 return knot; } public void UpdateSplineFromPathNode(int index) { if (container == null || container.Splines.Count == 0) return; UnitySpline spline = new UnitySpline(); int count = pathNodeList.Count; if (index < 0 || index >= spline.Count) { Debug.LogError($"节点索引 {index} 越界!"); return; } if (trackSpaceType is Track.TrackSpaceType.Linear or Track.TrackSpaceType.CatmullRom) { BezierKnot knot = spline[index]; Vector3 newPosition = pathNodeList[index].transformSubmodule.currentPosition; // 1. 提取当前节点 (使用 spline[index] 读取可以保留该节点原本的切线数据) // 这一步对于 Catmull-Rom 非常重要,因为它的切线是由 Unity 自动维护的 // 转换到本地坐标系 (如果传入的是世界坐标) knot.Position = container.transform.InverseTransformPoint(newPosition); // 2. 更新旋转 (如果需要的话) Quaternion newRotation = Quaternion.Euler(pathNodeList[index].transformSubmodule.currentEulerAngles); knot.Rotation = newRotation; // 直接使用节点的欧拉角作为旋转,保持与节点编辑器一致 // 3. 将修改后的 Knot 写回 Spline // 此时 Unity 底层会自动触发它自己的 Dirty 标记 spline.SetKnot(index, knot); } else if (trackSpaceType == Track.TrackSpaceType.BSpline) { if (isClosed) { // 封闭曲线使用取模处理环绕越界 int i0 = (index - 1 + count) % count; int i1 = (index + 1) % count; spline.SetKnot(i0, CalculateBSplineKnot(i0)); spline.SetKnot(index, CalculateBSplineKnot(index)); spline.SetKnot(i1, CalculateBSplineKnot(i1)); } else { // 开放曲线需进行安全边界检查 if (index - 1 >= 0) spline.SetKnot(index - 1, CalculateBSplineKnot(index - 1)); spline.SetKnot(index, CalculateBSplineKnot(index)); if (index + 1 < count) spline.SetKnot(index + 1, CalculateBSplineKnot(index + 1)); } } // 4. 触发我们自己的表现层脏标记 isSplineDirty = true; } public void UpdatePoint(Transform pointTransform, float progress) { UnitySpline spline = container.Splines[0]; progress = isClosed ? Mathf.Repeat(progress, 1f) : Mathf.Clamp01(progress); float mathT = progress; if (trackSamplingType == Track.TrackSamplingType.DistanceDistributed) { float targetDistance = progress * spline.GetLength(); mathT = spline.ConvertIndexUnit(targetDistance, PathIndexUnit.Distance, PathIndexUnit.Normalized); } container.Evaluate(0, mathT, out float3 pos, out float3 tangent, out float3 upVector); pointTransform.position = pos; if (math.lengthsq(tangent) > 0) { Vector3 worldTangent = container.transform.TransformDirection(tangent); Vector3 worldUp = container.transform.TransformDirection(upVector); pointTransform.rotation = quaternion.LookRotationSafe(worldTangent, worldUp); } } } namespace Beatmap { public class TrackPathSubmodule_BM : Submodule_BM { public Track.TrackSpaceType trackSpaceType; public Track.TrackSamplingType trackSamplingType; public bool isClosed; public bool isShowingDisplay; public TrackPathSubmodule_BM() { } public TrackPathSubmodule_BM(GameElement attachedElement, TrackPathSubmodule trackPathSubmodule) : base( attachedElement) { this.trackSpaceType = trackPathSubmodule.trackSpaceType; this.trackSamplingType = trackPathSubmodule.trackSamplingType; this.isClosed = trackPathSubmodule.isClosed; this.isShowingDisplay = trackPathSubmodule.isShowingDisplay; } public override void ExecuteBM() { attachedElement = GameElement_BM.GetElement(attachedElementGuid); Track track = attachedElement as Track; track.trackPathSubmodule = new TrackPathSubmodule(track, trackSpaceType, trackSamplingType, isClosed, isShowingDisplay); } } } }