Files
ichni_Official/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackPathSubmodule.cs

342 lines
13 KiB
C#
Raw Normal View History

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);
}
}
}
}