线测试代码
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` 约束链的实际长度保持一致。
|
||||
- 不允许因平滑处理导致视觉上的鱼线长度明显长于物理长度。
|
||||
- 渲染层应服务于物理结果表现,不能脱离节点链和约束长度单独扩张。
|
||||
- 任何平滑、插值、样条、圆角化处理,都不得使任一局部段长或整条视觉长度超过约束链长度。
|
||||
- 若无法低成本严格保证长度不超限,则默认不启用该类视觉平滑。
|
||||
|
||||
## 虚拟节点规则
|
||||
- 第一段为动态段,即起点到第一个逻辑节点之间的线段。
|
||||
@@ -57,5 +65,23 @@
|
||||
- 当按固定步长切分第一段时,不能整除的余量应落在起点到第一个虚拟节点之间。
|
||||
- 例如基础步长为 0.1 米、第一段总长度为 1.12 米时,离起点最近的第一个虚拟节点距离应为 0.02 米,后续节点再按 0.1 米递进。
|
||||
|
||||
## 虚拟节点默认策略
|
||||
- 虚拟节点默认使用纯数据结构,不创建运行时 `GameObject`。
|
||||
- 只有在明确需要场景交互、挂载组件、独立调试对象时,才将虚拟节点提升为实体对象。
|
||||
- 长线场景下应优先避免大量 `Transform` 或 `GameObject` 带来的层级与性能开销。
|
||||
- 第一段动态虚拟节点在增减时,应尽量保持已有节点身份与历史状态稳定,避免链路重排导致明显抽动。
|
||||
|
||||
## 水面与地面处理
|
||||
- 水面或地面表现优先采用轻量规则修正,如高度约束、投影贴附。
|
||||
- 不默认引入额外碰撞采样器、完整碰撞链或新增物理节点。
|
||||
- 应支持排除头尾若干节点不参与表面修正,以保留杆梢入水、浮漂黑漂等表现自由度。
|
||||
- 若仅为钓鱼表现需求,优先使用固定水面高度或简单表面规则,不优先构建完整环境碰撞解。
|
||||
|
||||
## 调试规则
|
||||
- 调试默认优先使用 Scene Gizmos、必要日志和可开关的调试绘制。
|
||||
- 不默认添加 GUI 面板、屏幕按钮、大段调试文字,除非用户明确要求。
|
||||
- 虚拟节点即使为纯数据,也应支持 Gizmos 可视化,便于在 Scene 中观察。
|
||||
- 调试功能应尽量不影响运行时结构与性能,不为调试方便引入长期保留的重对象方案。
|
||||
|
||||
## 后续补充
|
||||
- 具体功能边界、数据结构、更新方式、测试用例在后续阶段继续补充。
|
||||
|
||||
@@ -468,8 +468,24 @@ MonoBehaviour:
|
||||
solverIterations: 8
|
||||
damping: 0.98
|
||||
gravityScale: 1
|
||||
pinFollowStrength: 50
|
||||
writeBackVirtualNodeTransforms: 1
|
||||
simulationStep: 0.0166667
|
||||
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
|
||||
debugSampleColor: {r: 1, g: 0.2, b: 0.2, a: 1}
|
||||
debugSampleRadius: 0.015
|
||||
@@ -533,7 +549,7 @@ LineRenderer:
|
||||
m_Curve:
|
||||
- serializedVersion: 3
|
||||
time: 0
|
||||
value: 0.01
|
||||
value: 0.002
|
||||
inSlope: 0
|
||||
outSlope: 0
|
||||
tangentMode: 0
|
||||
|
||||
@@ -44,6 +44,17 @@ namespace F2RopeLine2.FishingLine
|
||||
[SerializeField] private int stableFramesBeforeSleep = 4;
|
||||
[Min(0f)]
|
||||
[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")]
|
||||
[SerializeField] private bool drawDebugSamples;
|
||||
@@ -52,6 +63,7 @@ namespace F2RopeLine2.FishingLine
|
||||
[SerializeField] private float debugSampleRadius = 0.015f;
|
||||
|
||||
private readonly List<Vector3> positions = new();
|
||||
private readonly List<Vector3> renderPositions = new();
|
||||
private readonly List<Vector3> previousPositions = new();
|
||||
private readonly List<long> sampledPointKeys = new();
|
||||
private readonly List<Vector3> lastPinnedPointPositions = new();
|
||||
@@ -68,9 +80,10 @@ namespace F2RopeLine2.FishingLine
|
||||
get
|
||||
{
|
||||
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;
|
||||
@@ -235,6 +248,19 @@ namespace F2RopeLine2.FishingLine
|
||||
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++)
|
||||
{
|
||||
PinLogicalPoints(points);
|
||||
@@ -244,10 +270,6 @@ namespace F2RopeLine2.FishingLine
|
||||
SatisfyDistanceConstraint(segmentIndex, restLengths[segmentIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
ApplyWaterSurfaceConstraint(stepDelta);
|
||||
PinLogicalPoints(points);
|
||||
ApplySleep();
|
||||
}
|
||||
|
||||
private void PinLogicalPoints(IReadOnlyList<FishingLineSolver.ChainPoint> points)
|
||||
@@ -306,6 +328,76 @@ namespace F2RopeLine2.FishingLine
|
||||
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)
|
||||
{
|
||||
if (!constrainToWaterSurface || positions.Count == 0)
|
||||
@@ -473,13 +565,115 @@ namespace F2RopeLine2.FishingLine
|
||||
|
||||
private void ApplyToRenderer()
|
||||
{
|
||||
lineRenderer.positionCount = positions.Count;
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
BuildRenderPositions();
|
||||
|
||||
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()
|
||||
{
|
||||
if (!drawDebugSamples || positions.Count == 0)
|
||||
|
||||
@@ -157,16 +157,36 @@ namespace F2RopeLine2.FishingLine
|
||||
RebuildRuntimeChain();
|
||||
}
|
||||
|
||||
public void SetFirstSegmentLength(float length)
|
||||
public void SetLenght(float length, int segmentIndex = 0)
|
||||
{
|
||||
var clamped = Mathf.Max(0f, length);
|
||||
if (Mathf.Approximately(clamped, firstSegmentLength))
|
||||
var currentLength = GetSegmentLength(segmentIndex);
|
||||
if (Mathf.Approximately(clamped, currentLength))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
firstSegmentLength = clamped;
|
||||
UpdateJointLimit(1, firstSegmentLength);
|
||||
if (segmentIndex <= 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace F2RopeLine2.FishingLine
|
||||
}
|
||||
|
||||
targetFirstSegmentLength = Mathf.Clamp(initialFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
||||
solver.SetFirstSegmentLength(targetFirstSegmentLength);
|
||||
solver.SetLenght(targetFirstSegmentLength);
|
||||
solver.BuildLine();
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace F2RopeLine2.FishingLine
|
||||
{
|
||||
targetFirstSegmentLength += input * lineAdjustSpeed * Time.deltaTime;
|
||||
targetFirstSegmentLength = Mathf.Clamp(targetFirstSegmentLength, minFirstSegmentLength, maxFirstSegmentLength);
|
||||
solver.SetFirstSegmentLength(targetFirstSegmentLength);
|
||||
solver.SetLenght(targetFirstSegmentLength);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user