线测试代码
This commit is contained in:
26
AGENTS.md
26
AGENTS.md
@@ -16,6 +16,12 @@
|
|||||||
- 设计结构确认后,再进入具体实现与代码补全。
|
- 设计结构确认后,再进入具体实现与代码补全。
|
||||||
- 如果需求变更会影响节点结构、连接关系、渲染流程或交互挂接方式,应重新对齐设计后再修改代码。
|
- 如果需求变更会影响节点结构、连接关系、渲染流程或交互挂接方式,应重新对齐设计后再修改代码。
|
||||||
|
|
||||||
|
## 实现默认策略
|
||||||
|
- 首次设计确认时,默认只输出 3 到 6 个核心脚本或模块,不主动展开完整分层架构,除非用户明确要求。
|
||||||
|
- 未经确认,不引入 Builder、Runtime、Config、Interaction Framework 等扩展层。
|
||||||
|
- 测试阶段优先使用最少脚本、最少运行时对象、最少中间抽象的实现。
|
||||||
|
- 新增方案优先选择容易验证、容易回退、容易观察结果的写法。
|
||||||
|
|
||||||
## 当前需求
|
## 当前需求
|
||||||
- 鱼线由多节点组成,节点分为逻辑节点和虚拟节点。
|
- 鱼线由多节点组成,节点分为逻辑节点和虚拟节点。
|
||||||
- 逻辑节点用于表达真实业务结构,数量和类型会随钓鱼场景变化。
|
- 逻辑节点用于表达真实业务结构,数量和类型会随钓鱼场景变化。
|
||||||
@@ -48,6 +54,8 @@
|
|||||||
- `Verlet` 渲染出的鱼线长度应与 `Joint` 约束链的实际长度保持一致。
|
- `Verlet` 渲染出的鱼线长度应与 `Joint` 约束链的实际长度保持一致。
|
||||||
- 不允许因平滑处理导致视觉上的鱼线长度明显长于物理长度。
|
- 不允许因平滑处理导致视觉上的鱼线长度明显长于物理长度。
|
||||||
- 渲染层应服务于物理结果表现,不能脱离节点链和约束长度单独扩张。
|
- 渲染层应服务于物理结果表现,不能脱离节点链和约束长度单独扩张。
|
||||||
|
- 任何平滑、插值、样条、圆角化处理,都不得使任一局部段长或整条视觉长度超过约束链长度。
|
||||||
|
- 若无法低成本严格保证长度不超限,则默认不启用该类视觉平滑。
|
||||||
|
|
||||||
## 虚拟节点规则
|
## 虚拟节点规则
|
||||||
- 第一段为动态段,即起点到第一个逻辑节点之间的线段。
|
- 第一段为动态段,即起点到第一个逻辑节点之间的线段。
|
||||||
@@ -57,5 +65,23 @@
|
|||||||
- 当按固定步长切分第一段时,不能整除的余量应落在起点到第一个虚拟节点之间。
|
- 当按固定步长切分第一段时,不能整除的余量应落在起点到第一个虚拟节点之间。
|
||||||
- 例如基础步长为 0.1 米、第一段总长度为 1.12 米时,离起点最近的第一个虚拟节点距离应为 0.02 米,后续节点再按 0.1 米递进。
|
- 例如基础步长为 0.1 米、第一段总长度为 1.12 米时,离起点最近的第一个虚拟节点距离应为 0.02 米,后续节点再按 0.1 米递进。
|
||||||
|
|
||||||
|
## 虚拟节点默认策略
|
||||||
|
- 虚拟节点默认使用纯数据结构,不创建运行时 `GameObject`。
|
||||||
|
- 只有在明确需要场景交互、挂载组件、独立调试对象时,才将虚拟节点提升为实体对象。
|
||||||
|
- 长线场景下应优先避免大量 `Transform` 或 `GameObject` 带来的层级与性能开销。
|
||||||
|
- 第一段动态虚拟节点在增减时,应尽量保持已有节点身份与历史状态稳定,避免链路重排导致明显抽动。
|
||||||
|
|
||||||
|
## 水面与地面处理
|
||||||
|
- 水面或地面表现优先采用轻量规则修正,如高度约束、投影贴附。
|
||||||
|
- 不默认引入额外碰撞采样器、完整碰撞链或新增物理节点。
|
||||||
|
- 应支持排除头尾若干节点不参与表面修正,以保留杆梢入水、浮漂黑漂等表现自由度。
|
||||||
|
- 若仅为钓鱼表现需求,优先使用固定水面高度或简单表面规则,不优先构建完整环境碰撞解。
|
||||||
|
|
||||||
|
## 调试规则
|
||||||
|
- 调试默认优先使用 Scene Gizmos、必要日志和可开关的调试绘制。
|
||||||
|
- 不默认添加 GUI 面板、屏幕按钮、大段调试文字,除非用户明确要求。
|
||||||
|
- 虚拟节点即使为纯数据,也应支持 Gizmos 可视化,便于在 Scene 中观察。
|
||||||
|
- 调试功能应尽量不影响运行时结构与性能,不为调试方便引入长期保留的重对象方案。
|
||||||
|
|
||||||
## 后续补充
|
## 后续补充
|
||||||
- 具体功能边界、数据结构、更新方式、测试用例在后续阶段继续补充。
|
- 具体功能边界、数据结构、更新方式、测试用例在后续阶段继续补充。
|
||||||
|
|||||||
@@ -468,8 +468,24 @@ MonoBehaviour:
|
|||||||
solverIterations: 8
|
solverIterations: 8
|
||||||
damping: 0.98
|
damping: 0.98
|
||||||
gravityScale: 1
|
gravityScale: 1
|
||||||
pinFollowStrength: 50
|
simulationStep: 0.0166667
|
||||||
writeBackVirtualNodeTransforms: 1
|
maxDeltaTime: 0.0333333
|
||||||
|
constrainToWaterSurface: 1
|
||||||
|
waterSurfaceTransform: {fileID: 0}
|
||||||
|
waterSurfaceHeight: 0.01
|
||||||
|
ignoreHeadNodeCount: 1
|
||||||
|
ignoreTailNodeCount: 1
|
||||||
|
waterSurfaceFollowSpeed: 12
|
||||||
|
maxSubStepsPerFrame: 2
|
||||||
|
sleepVelocityThreshold: 0.001
|
||||||
|
sleepDistanceThreshold: 0.002
|
||||||
|
stableFramesBeforeSleep: 4
|
||||||
|
wakeDistanceThreshold: 0.001
|
||||||
|
tautSegmentThreshold: 0.002
|
||||||
|
smoothCorners: 1
|
||||||
|
minCornerAngle: 12
|
||||||
|
maxCornerSmoothDistance: 0.03
|
||||||
|
cornerSmoothSubdivisions: 3
|
||||||
drawDebugSamples: 0
|
drawDebugSamples: 0
|
||||||
debugSampleColor: {r: 1, g: 0.2, b: 0.2, a: 1}
|
debugSampleColor: {r: 1, g: 0.2, b: 0.2, a: 1}
|
||||||
debugSampleRadius: 0.015
|
debugSampleRadius: 0.015
|
||||||
@@ -533,7 +549,7 @@ LineRenderer:
|
|||||||
m_Curve:
|
m_Curve:
|
||||||
- serializedVersion: 3
|
- serializedVersion: 3
|
||||||
time: 0
|
time: 0
|
||||||
value: 0.01
|
value: 0.002
|
||||||
inSlope: 0
|
inSlope: 0
|
||||||
outSlope: 0
|
outSlope: 0
|
||||||
tangentMode: 0
|
tangentMode: 0
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ namespace F2RopeLine2.FishingLine
|
|||||||
[SerializeField] private int stableFramesBeforeSleep = 4;
|
[SerializeField] private int stableFramesBeforeSleep = 4;
|
||||||
[Min(0f)]
|
[Min(0f)]
|
||||||
[SerializeField] private float wakeDistanceThreshold = 0.001f;
|
[SerializeField] private float wakeDistanceThreshold = 0.001f;
|
||||||
|
[Min(0f)]
|
||||||
|
[SerializeField] private float tautSegmentThreshold = 0.002f;
|
||||||
|
|
||||||
|
[Header("Corner Smoothing")]
|
||||||
|
[SerializeField] private bool smoothCorners = true;
|
||||||
|
[Range(0f, 180f)]
|
||||||
|
[SerializeField] private float minCornerAngle = 12f;
|
||||||
|
[Min(0f)]
|
||||||
|
[SerializeField] private float maxCornerSmoothDistance = 0.03f;
|
||||||
|
[Min(1)]
|
||||||
|
[SerializeField] private int cornerSmoothSubdivisions = 3;
|
||||||
|
|
||||||
[Header("Debug")]
|
[Header("Debug")]
|
||||||
[SerializeField] private bool drawDebugSamples;
|
[SerializeField] private bool drawDebugSamples;
|
||||||
@@ -52,6 +63,7 @@ namespace F2RopeLine2.FishingLine
|
|||||||
[SerializeField] private float debugSampleRadius = 0.015f;
|
[SerializeField] private float debugSampleRadius = 0.015f;
|
||||||
|
|
||||||
private readonly List<Vector3> positions = new();
|
private readonly List<Vector3> positions = new();
|
||||||
|
private readonly List<Vector3> renderPositions = new();
|
||||||
private readonly List<Vector3> previousPositions = new();
|
private readonly List<Vector3> previousPositions = new();
|
||||||
private readonly List<long> sampledPointKeys = new();
|
private readonly List<long> sampledPointKeys = new();
|
||||||
private readonly List<Vector3> lastPinnedPointPositions = new();
|
private readonly List<Vector3> lastPinnedPointPositions = new();
|
||||||
@@ -68,9 +80,10 @@ namespace F2RopeLine2.FishingLine
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
var total = 0f;
|
var total = 0f;
|
||||||
for (var i = 0; i < positions.Count - 1; i++)
|
var source = renderPositions.Count > 0 ? renderPositions : positions;
|
||||||
|
for (var i = 0; i < source.Count - 1; i++)
|
||||||
{
|
{
|
||||||
total += Vector3.Distance(positions[i], positions[i + 1]);
|
total += Vector3.Distance(source[i], source[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return total;
|
return total;
|
||||||
@@ -235,6 +248,19 @@ namespace F2RopeLine2.FishingLine
|
|||||||
positions[i] = current + velocity + gravity;
|
positions[i] = current + velocity + gravity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SolveDistanceConstraints(points, restLengths);
|
||||||
|
|
||||||
|
ApplyWaterSurfaceConstraint(stepDelta);
|
||||||
|
SolveDistanceConstraints(points, restLengths);
|
||||||
|
StraightenTautLogicalSegments(points, restLengths);
|
||||||
|
PinLogicalPoints(points);
|
||||||
|
ApplySleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SolveDistanceConstraints(
|
||||||
|
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||||
|
IReadOnlyList<float> restLengths)
|
||||||
|
{
|
||||||
for (var iteration = 0; iteration < solverIterations; iteration++)
|
for (var iteration = 0; iteration < solverIterations; iteration++)
|
||||||
{
|
{
|
||||||
PinLogicalPoints(points);
|
PinLogicalPoints(points);
|
||||||
@@ -244,10 +270,6 @@ namespace F2RopeLine2.FishingLine
|
|||||||
SatisfyDistanceConstraint(segmentIndex, restLengths[segmentIndex]);
|
SatisfyDistanceConstraint(segmentIndex, restLengths[segmentIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyWaterSurfaceConstraint(stepDelta);
|
|
||||||
PinLogicalPoints(points);
|
|
||||||
ApplySleep();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PinLogicalPoints(IReadOnlyList<FishingLineSolver.ChainPoint> points)
|
private void PinLogicalPoints(IReadOnlyList<FishingLineSolver.ChainPoint> points)
|
||||||
@@ -306,6 +328,76 @@ namespace F2RopeLine2.FishingLine
|
|||||||
positions[segmentIndex + 1] -= correction;
|
positions[segmentIndex + 1] -= correction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void StraightenTautLogicalSegments(
|
||||||
|
IReadOnlyList<FishingLineSolver.ChainPoint> points,
|
||||||
|
IReadOnlyList<float> restLengths)
|
||||||
|
{
|
||||||
|
if (points.Count < 2 || restLengths.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentStartIndex = 0;
|
||||||
|
while (segmentStartIndex < points.Count - 1)
|
||||||
|
{
|
||||||
|
if (!points[segmentStartIndex].IsLogical)
|
||||||
|
{
|
||||||
|
segmentStartIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentEndIndex = segmentStartIndex + 1;
|
||||||
|
while (segmentEndIndex < points.Count && !points[segmentEndIndex].IsLogical)
|
||||||
|
{
|
||||||
|
segmentEndIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentEndIndex >= points.Count)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectLogicalSegmentIfTaut(segmentStartIndex, segmentEndIndex, restLengths);
|
||||||
|
segmentStartIndex = segmentEndIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProjectLogicalSegmentIfTaut(
|
||||||
|
int startIndex,
|
||||||
|
int endIndex,
|
||||||
|
IReadOnlyList<float> restLengths)
|
||||||
|
{
|
||||||
|
if (endIndex - startIndex <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentRestLength = 0f;
|
||||||
|
for (var i = startIndex; i < endIndex; i++)
|
||||||
|
{
|
||||||
|
segmentRestLength += restLengths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = positions[startIndex];
|
||||||
|
var end = positions[endIndex];
|
||||||
|
var delta = end - start;
|
||||||
|
var endpointDistance = delta.magnitude;
|
||||||
|
if (endpointDistance <= 0.0001f || endpointDistance + tautSegmentThreshold < segmentRestLength)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var direction = delta / endpointDistance;
|
||||||
|
var accumulatedDistance = 0f;
|
||||||
|
for (var pointIndex = startIndex + 1; pointIndex < endIndex; pointIndex++)
|
||||||
|
{
|
||||||
|
accumulatedDistance += restLengths[pointIndex - 1];
|
||||||
|
var projectedPosition = start + direction * accumulatedDistance;
|
||||||
|
positions[pointIndex] = projectedPosition;
|
||||||
|
previousPositions[pointIndex] = projectedPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyWaterSurfaceConstraint(float stepDelta)
|
private void ApplyWaterSurfaceConstraint(float stepDelta)
|
||||||
{
|
{
|
||||||
if (!constrainToWaterSurface || positions.Count == 0)
|
if (!constrainToWaterSurface || positions.Count == 0)
|
||||||
@@ -473,13 +565,115 @@ namespace F2RopeLine2.FishingLine
|
|||||||
|
|
||||||
private void ApplyToRenderer()
|
private void ApplyToRenderer()
|
||||||
{
|
{
|
||||||
lineRenderer.positionCount = positions.Count;
|
BuildRenderPositions();
|
||||||
for (var i = 0; i < positions.Count; i++)
|
|
||||||
|
lineRenderer.positionCount = renderPositions.Count;
|
||||||
|
for (var i = 0; i < renderPositions.Count; i++)
|
||||||
{
|
{
|
||||||
lineRenderer.SetPosition(i, positions[i]);
|
lineRenderer.SetPosition(i, renderPositions[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildRenderPositions()
|
||||||
|
{
|
||||||
|
renderPositions.Clear();
|
||||||
|
if (positions.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smoothCorners || positions.Count < 3)
|
||||||
|
{
|
||||||
|
renderPositions.AddRange(positions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPositions.Add(positions[0]);
|
||||||
|
|
||||||
|
for (var i = 1; i < positions.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var previous = positions[i - 1];
|
||||||
|
var current = positions[i];
|
||||||
|
var next = positions[i + 1];
|
||||||
|
|
||||||
|
if (!TryBuildSmoothedCorner(previous, current, next, out var entry, out var exit))
|
||||||
|
{
|
||||||
|
AddRenderPointIfDistinct(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddRenderPointIfDistinct(entry);
|
||||||
|
for (var subdivision = 1; subdivision <= cornerSmoothSubdivisions; subdivision++)
|
||||||
|
{
|
||||||
|
var t = subdivision / (cornerSmoothSubdivisions + 1f);
|
||||||
|
var pointOnCurve = EvaluateQuadraticBezier(entry, current, exit, t);
|
||||||
|
AddRenderPointIfDistinct(pointOnCurve);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddRenderPointIfDistinct(exit);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddRenderPointIfDistinct(positions[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryBuildSmoothedCorner(
|
||||||
|
Vector3 previous,
|
||||||
|
Vector3 current,
|
||||||
|
Vector3 next,
|
||||||
|
out Vector3 entry,
|
||||||
|
out Vector3 exit)
|
||||||
|
{
|
||||||
|
entry = current;
|
||||||
|
exit = current;
|
||||||
|
|
||||||
|
var incoming = current - previous;
|
||||||
|
var outgoing = next - current;
|
||||||
|
var incomingLength = incoming.magnitude;
|
||||||
|
var outgoingLength = outgoing.magnitude;
|
||||||
|
if (incomingLength <= 0.0001f || outgoingLength <= 0.0001f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cornerAngle = Vector3.Angle(incoming, outgoing);
|
||||||
|
if (cornerAngle < minCornerAngle)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trimDistance = Mathf.Min(
|
||||||
|
maxCornerSmoothDistance,
|
||||||
|
incomingLength * 0.5f,
|
||||||
|
outgoingLength * 0.5f);
|
||||||
|
|
||||||
|
if (trimDistance <= 0.0001f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = current - (incoming / incomingLength) * trimDistance;
|
||||||
|
exit = current + (outgoing / outgoingLength) * trimDistance;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRenderPointIfDistinct(Vector3 point)
|
||||||
|
{
|
||||||
|
if (renderPositions.Count > 0 && Vector3.Distance(renderPositions[^1], point) <= 0.0001f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPositions.Add(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3 EvaluateQuadraticBezier(Vector3 start, Vector3 control, Vector3 end, float t)
|
||||||
|
{
|
||||||
|
var oneMinusT = 1f - t;
|
||||||
|
return (oneMinusT * oneMinusT * start)
|
||||||
|
+ (2f * oneMinusT * t * control)
|
||||||
|
+ (t * t * end);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
private void OnDrawGizmosSelected()
|
||||||
{
|
{
|
||||||
if (!drawDebugSamples || positions.Count == 0)
|
if (!drawDebugSamples || positions.Count == 0)
|
||||||
|
|||||||
@@ -157,16 +157,36 @@ namespace F2RopeLine2.FishingLine
|
|||||||
RebuildRuntimeChain();
|
RebuildRuntimeChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetFirstSegmentLength(float length)
|
public void SetLenght(float length, int segmentIndex = 0)
|
||||||
{
|
{
|
||||||
var clamped = Mathf.Max(0f, length);
|
var clamped = Mathf.Max(0f, length);
|
||||||
if (Mathf.Approximately(clamped, firstSegmentLength))
|
var currentLength = GetSegmentLength(segmentIndex);
|
||||||
|
if (Mathf.Approximately(clamped, currentLength))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstSegmentLength = clamped;
|
if (segmentIndex <= 0)
|
||||||
UpdateJointLimit(1, firstSegmentLength);
|
{
|
||||||
|
firstSegmentLength = clamped;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (logicalNodes == null || segmentIndex >= logicalNodes.Length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceNode = logicalNodes[segmentIndex];
|
||||||
|
if (sourceNode == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceNode.SegmentLengthToNext = clamped;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateJointLimit(segmentIndex + 1, clamped);
|
||||||
chainDirty = true;
|
chainDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ namespace F2RopeLine2.FishingLine
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetFirstSegmentLength = Mathf.Clamp(initialFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
targetFirstSegmentLength = Mathf.Clamp(initialFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
||||||
solver.SetFirstSegmentLength(targetFirstSegmentLength);
|
solver.SetLenght(targetFirstSegmentLength);
|
||||||
solver.BuildLine();
|
solver.BuildLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ namespace F2RopeLine2.FishingLine
|
|||||||
{
|
{
|
||||||
targetFirstSegmentLength += input * lineAdjustSpeed * Time.deltaTime;
|
targetFirstSegmentLength += input * lineAdjustSpeed * Time.deltaTime;
|
||||||
targetFirstSegmentLength = Mathf.Clamp(targetFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
targetFirstSegmentLength = Mathf.Clamp(targetFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
||||||
solver.SetFirstSegmentLength(targetFirstSegmentLength);
|
solver.SetLenght(targetFirstSegmentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user