2025-06-03 02:42:28 -04:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Dreamteck.Splines;
|
|
|
|
|
|
using Ichni.RhythmGame.Beatmap;
|
2026-02-27 08:21:00 -05:00
|
|
|
|
using Sirenix.OdinInspector;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
using Unity.VisualScripting;
|
|
|
|
|
|
using UnityEngine;
|
2026-02-27 08:21:00 -05:00
|
|
|
|
using UnityEngine.Splines;
|
|
|
|
|
|
using Spline = Dreamteck.Splines.Spline;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
|
|
|
|
|
|
namespace Ichni.RhythmGame
|
|
|
|
|
|
{
|
2026-02-27 08:21:00 -05:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Unity.Mathematics;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
// 引入官方的 Splines 命名空间
|
|
|
|
|
|
using UnitySpline = UnityEngine.Splines.Spline;
|
|
|
|
|
|
using UnitySplineContainer = UnityEngine.Splines.SplineContainer;
|
|
|
|
|
|
|
2025-06-03 02:42:28 -04:00
|
|
|
|
public partial class TrackPathSubmodule : TrackSubmodule
|
|
|
|
|
|
{
|
2026-02-27 08:21:00 -05:00
|
|
|
|
public Dreamteck.Splines.SplineComputer path;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
public List<PathNode> pathNodeList;
|
|
|
|
|
|
|
|
|
|
|
|
public Track.TrackSpaceType trackSpaceType;
|
|
|
|
|
|
public Track.TrackSamplingType trackSamplingType;
|
|
|
|
|
|
public bool isClosed;
|
|
|
|
|
|
|
2026-01-21 00:31:23 -05:00
|
|
|
|
public bool refreshedThisFrame = false;
|
|
|
|
|
|
|
2025-06-03 02:42:28 -04:00
|
|
|
|
public bool isShowingDisplay;
|
|
|
|
|
|
|
|
|
|
|
|
public TrackPathSubmodule(Track track, Track.TrackSpaceType trackSpaceType,
|
|
|
|
|
|
Track.TrackSamplingType trackSamplingType, bool isClosed, bool isShowingDisplay) : base(track)
|
|
|
|
|
|
{
|
|
|
|
|
|
this.path = track.AddComponent<SplineComputer>();
|
2026-02-27 08:21:00 -05:00
|
|
|
|
|
2025-06-03 02:42:28 -04:00
|
|
|
|
this.pathNodeList = new List<PathNode>();
|
|
|
|
|
|
this.trackSpaceType = trackSpaceType;
|
|
|
|
|
|
this.trackSamplingType = trackSamplingType;
|
|
|
|
|
|
this.isClosed = isClosed;
|
|
|
|
|
|
|
2026-02-27 08:21:00 -05:00
|
|
|
|
this.path.sampleRate = 8;
|
2025-06-03 02:42:28 -04:00
|
|
|
|
this.path.updateMode = SplineComputer.UpdateMode.LateUpdate;
|
|
|
|
|
|
SetUpSplineComputer(this.trackSpaceType, this.trackSamplingType);
|
|
|
|
|
|
//闭合路径在PathNode生成时执行,在初始化的情况下,PathNode数量为0,不会执行闭合操作
|
|
|
|
|
|
|
|
|
|
|
|
this.isShowingDisplay = isShowingDisplay;
|
|
|
|
|
|
|
|
|
|
|
|
if (!HaveSameSubmodule)
|
|
|
|
|
|
{
|
|
|
|
|
|
this.track.trackPathSubmodule = this;
|
|
|
|
|
|
}
|
2026-02-27 08:21:00 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.container = track.AddComponent<UnitySplineContainer>();
|
|
|
|
|
|
|
2025-06-03 02:42:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
{
|
2026-01-21 00:31:23 -05:00
|
|
|
|
if(refreshedThisFrame) return;
|
|
|
|
|
|
refreshedThisFrame = true;
|
|
|
|
|
|
|
2025-06-03 02:42:28 -04:00
|
|
|
|
SetTrackSpaceType((int)trackSpaceType);
|
|
|
|
|
|
SetUpSplineComputer(trackSpaceType, trackSamplingType);
|
|
|
|
|
|
foreach (var pathNode in pathNodeList)
|
|
|
|
|
|
{
|
|
|
|
|
|
SetPathNode(pathNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
ClosePath();
|
2026-01-21 00:31:23 -05:00
|
|
|
|
path.RebuildImmediate(true, true);
|
2025-06-03 02:42:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public partial class TrackPathSubmodule
|
|
|
|
|
|
{
|
|
|
|
|
|
public override void SaveBM()
|
|
|
|
|
|
{
|
|
|
|
|
|
matchedBM = new TrackPathSubmodule_BM(attachedGameElement, this);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 08:21:00 -05:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-03 02:42:28 -04:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|