Files
aibis-dream/Assets/Scripts/FixSystemNew/BodyModule/Cable.cs
T

475 lines
19 KiB
C#

using System;
using UnityEngine;
namespace AibisDream.FixSystem
{
[RequireComponent(typeof(LineRenderer))]
public class Cable : MonoBehaviour
{
private Transform _start;
private Transform _end;
private LineRenderer _lineRenderer;
private LineRenderer _stiffLineRenderer; // 用于渲染刚性部分
private Vector3[] _points;
// 渲染平滑:物理点数少,直接连线转折生硬,渲染前做切角细分
private Vector3[] _smoothBufferA;
private Vector3[] _smoothBufferB;
// 配置兜底后的生效值(旧场景/预制体上新字段是 0 时取默认)
private float _headStiffLength;
private float _headRenderBlend;
private int _tailStiffCount;
private float _tailStiffLength;
private float _tailRenderBlend;
private int _smoothIterations;
private CablePanel _cablePanel;
private RendererSortingState _defaultLineSorting;
private RendererSortingState _defaultStiffLineSorting;
[SerializeField] private CableConfig config = CableConfig.GeneDefaultConfig();
public void ShowCable()
{
_lineRenderer.enabled = true;
_stiffLineRenderer.enabled = true;
}
public void HideCable()
{
_lineRenderer.enabled = false;
_stiffLineRenderer.enabled = false;
}
private void Awake()
{
ResolveConfig();
_lineRenderer = GetComponent<LineRenderer>();
// 每次 Chaikin 细分点数翻倍(保留首尾 + 每段两个切分点)
int flexCount = config.resolution - config.stiffCount;
int smoothedCount = flexCount << _smoothIterations;
_lineRenderer.positionCount = smoothedCount;
_smoothBufferA = new Vector3[smoothedCount];
_smoothBufferB = new Vector3[smoothedCount];
// 创建第二个LineRenderer用于刚性部分
GameObject stiffObj = new GameObject("StiffLineRenderer");
stiffObj.transform.SetParent(transform);
_stiffLineRenderer = stiffObj.AddComponent<LineRenderer>();
_stiffLineRenderer.positionCount = config.stiffCount;
}
/// <summary>
/// 计算生效配置:新加的字段在旧序列化数据里是 0,取兜底默认;并做范围保护。
/// </summary>
private void ResolveConfig()
{
_headStiffLength = config.headStiffLength <= 0f ? 0.055f : config.headStiffLength;
_headRenderBlend = config.headRenderBlend <= 0f ? 0.55f : Mathf.Clamp01(config.headRenderBlend);
_tailStiffCount = config.tailStiffCount < 2 ? 2 : config.tailStiffCount;
// 至少给物理留 2 个自由点
_tailStiffCount = Mathf.Min(_tailStiffCount, config.resolution - config.stiffCount - 2);
_tailStiffCount = Mathf.Max(_tailStiffCount, 2);
_tailStiffLength = config.tailStiffLength <= 0f ? 0.024f : config.tailStiffLength;
_tailRenderBlend = config.tailRenderBlend <= 0f ? 0.45f : Mathf.Clamp01(config.tailRenderBlend);
_smoothIterations = config.smoothIterations <= 0 ? 2 : Mathf.Min(config.smoothIterations, 4);
}
private void Start()
{
InitComponentRefs();
InitPoints();
}
private void InitComponentRefs()
{
_cablePanel = transform.GetComponentInParent<CablePanel>();
// 复制第一个LineRenderer的属性到第二个
_stiffLineRenderer.startWidth = _lineRenderer.startWidth;
_stiffLineRenderer.endWidth = _lineRenderer.startWidth;
_stiffLineRenderer.material = _lineRenderer.material;
_stiffLineRenderer.startColor = _lineRenderer.startColor;
_stiffLineRenderer.endColor = _lineRenderer.startColor;
_stiffLineRenderer.numCornerVertices = _lineRenderer.numCornerVertices;
_stiffLineRenderer.alignment = _lineRenderer.alignment;
_stiffLineRenderer.textureMode = _lineRenderer.textureMode;
_stiffLineRenderer.sortingLayerName =
_cablePanel.CableReelRef.GetComponent<SpriteRenderer>().sortingLayerName;
_stiffLineRenderer.sortingOrder = _cablePanel.CableReelRef.GetComponent<SpriteRenderer>().sortingOrder - 1;
_defaultLineSorting = RendererSortingState.From(_lineRenderer);
_defaultStiffLineSorting = RendererSortingState.From(_stiffLineRenderer);
// 两端端帽分开处理:
// 刚性段贴着出线口,用方口端帽(0)保证切面与出线口齐平不凸出;
// 柔性段末端在插头/插孔处,用圆头端帽包进贴图,拖拽转向时不露缝。
_stiffLineRenderer.numCapVertices = 0;
if (_lineRenderer.numCapVertices < 1)
{
_lineRenderer.numCapVertices = 6;
}
// 连线起止点(末端用 PlugRoot,避免连到插头精灵中心)
_start = _cablePanel.CableRootPos;
_end = _cablePanel.PlugRef.PlugRoot;
}
private void InitPoints()
{
_points = new Vector3[config.resolution];
for (int i = 0; i < config.resolution; i++)
{
float t = i / (float)(config.resolution - 1);
_points[i] = Vector3.Lerp(_start.position, _end.position, t);
}
}
private void FixedUpdate()
{
DrawLine();
}
void DrawLine()
{
var points = UpdateRope();
Vector3 zOffset = Vector3.forward * -.3f;
// 刚性段走向(出线口 -> 接缝点)与接缝方向(接缝点 -> 下一个柔性点)
Vector3 stiffDir = (points[config.stiffCount - 1] - points[0]).normalized;
Vector3 seamOffset = points[config.stiffCount + 1] - points[config.stiffCount];
Vector3 seamDir = seamOffset.sqrMagnitude > 1e-6f ? seamOffset.normalized : stiffDir;
float halfWidth = _lineRenderer.startWidth * 0.5f;
// 渲染刚性部分
for (int i = 0; i < config.stiffCount; i++)
{
_stiffLineRenderer.SetPosition(i, points[i] + zOffset);
}
// 刚性段末点沿接缝方向多画半个线宽,垫在柔性段圆头下方,避免接缝两侧出现缺口。
// 仅在接缝方向大致顺延刚性段时外延;线松弛折返时跳过,
// 否则条带在折角处被斜切,会闪现细长尖刺。
if (Vector3.Dot(stiffDir, seamDir) > 0.2f)
{
_stiffLineRenderer.SetPosition(config.stiffCount - 1,
points[config.stiffCount - 1] + seamDir * halfWidth + zOffset);
}
// 拷贝柔性段准备渲染
int flexCount = config.resolution - config.stiffCount;
for (int i = 0; i < flexCount; i++)
{
_smoothBufferA[i] = points[i + config.stiffCount] + zOffset;
}
// 柔性段首点内缩半个线宽:圆头端帽顶点正好落在接缝处,
// 不会越过接缝、盖到出线口贴图上(柔性段排序在出线口贴图之上)。
// 内缩量不超过首段长度的一半,防止首段退化成零长度产生扭折。
float insetLimit = Mathf.Min(halfWidth, seamOffset.magnitude * 0.5f);
_smoothBufferA[0] += seamDir * insetLimit;
if (TryGetHeadDir(out Vector3 headRenderDir))
{
BlendHeadRenderControls(flexCount, headRenderDir);
}
if (TryGetTailDir(out Vector3 tailRenderDir))
{
BlendTailRenderControls(flexCount, tailRenderDir);
}
// Chaikin 切角细分:物理点直接连线在转折处(尤其插头端刚性段与曲线的交界)
// 过于生硬,细分把折角切圆;首尾端点保留,两端的进出方向不变。
Vector3[] src = _smoothBufferA;
Vector3[] dst = _smoothBufferB;
int count = flexCount;
for (int it = 0; it < _smoothIterations; it++)
{
count = Chaikin(src, count, dst);
(src, dst) = (dst, src);
}
for (int i = 0; i < count; i++)
{
_lineRenderer.SetPosition(i, src[i]);
}
}
/// <summary>
/// 一次 Chaikin 切角细分:保留首尾点,每段取 1/4、3/4 两个切分点,输出点数为 2n。
/// </summary>
private static int Chaikin(Vector3[] src, int count, Vector3[] dst)
{
int idx = 0;
dst[idx++] = src[0];
for (int i = 0; i < count - 1; i++)
{
dst[idx++] = Vector3.Lerp(src[i], src[i + 1], 0.25f);
dst[idx++] = Vector3.Lerp(src[i], src[i + 1], 0.75f);
}
dst[idx++] = src[count - 1];
return idx;
}
private void BlendHeadRenderControls(int flexCount, Vector3 headDir)
{
if (flexCount < 2) return;
Vector3 start = _smoothBufferA[0];
Vector3 next = _smoothBufferA[1];
float blendLength = Mathf.Min(_headStiffLength, Vector3.Distance(start, next));
Vector3 tangentTarget = start + headDir * blendLength;
_smoothBufferA[1] = Vector3.Lerp(next, tangentTarget, _headRenderBlend);
}
private void BlendTailRenderControls(int flexCount, Vector3 tailDir)
{
int endIndex = flexCount - 1;
int prevIndex = endIndex - 1;
if (prevIndex < 0) return;
Vector3 end = _smoothBufferA[endIndex];
Vector3 previous = _smoothBufferA[prevIndex];
Vector3 incoming = previous - end;
if (incoming.sqrMagnitude < 1e-6f) return;
float angleWeight = Mathf.InverseLerp(-0.1f, 0.7f, Vector3.Dot(incoming.normalized, tailDir));
float blendLength = Mathf.Min(_tailStiffLength, Vector3.Distance(end, previous));
Vector3 tangentTarget = end + tailDir * blendLength;
_smoothBufferA[prevIndex] = Vector3.Lerp(previous, tangentTarget, _tailRenderBlend * angleWeight);
}
Vector3[] UpdateRope()
{
float t = Mathf.InverseLerp(config.dstMin, config.dstMax, (_start.position - _end.position).magnitude);
float F = Mathf.Lerp(config.forceMin, config.forceMax, t);
// 计算出线方向(从转盘出线口到起始点)
Vector3 dir = (_start.position - _cablePanel.CableReelRef.transform.position).normalized;
// 设置刚性部分
for (int i = 0; i < config.stiffCount; i++)
{
float t_stiff = (float)i / (config.stiffCount - 1);
_points[i] = _start.position + dir * (_headStiffLength * t_stiff);
}
// 设置末端点
_points[^1] = _end.position;
// 插头端刚性段:让线先顺着插头轴向(插头中心 -> 线缆接点)伸出一小段再弯曲,
// 否则线会以任意角度横着搭在插头尾部,看起来没有"插进去"。
// 末端挂在 Socket 上时接点没有可靠的轴向,保持原有纯物理表现。
bool pinTail = TryGetTailDir(out Vector3 tailDir);
int tailPinned = 1;
if (pinTail)
{
tailPinned = _tailStiffCount;
for (int j = 0; j < tailPinned; j++)
{
float tTail = (float)j / (tailPinned - 1);
_points[_points.Length - 1 - j] = _end.position + tailDir * (_tailStiffLength * tTail);
}
}
int simEndExclusive = _points.Length - tailPinned;
// 只对非刚性部分进行物理模拟
for (int ik = 0; ik < config.k; ik++)
{
// 确保非刚性部分的第一个点与刚性部分的最后一个点重合
_points[config.stiffCount] = _points[config.stiffCount - 1];
for (int i = config.stiffCount + 1; i < simEndExclusive; i++)
{
Vector3 offsetPrev = _points[i - 1] - _points[i];
Vector3 offsetNext = _points[i + 1] - _points[i];
Vector3 velocity = offsetPrev.normalized * (offsetPrev.magnitude * F) +
offsetNext.normalized * (offsetNext.magnitude * F);
_points[i] += velocity * Time.deltaTime / config.k;
}
for (int i = config.stiffCount + 1; i < simEndExclusive; i++)
{
_points[i] += Vector3.down * (9.8f * Time.deltaTime) / config.k;
}
}
if (pinTail)
{
RelaxTailApproach(tailPinned, tailDir);
}
return _points;
}
private bool TryGetHeadDir(out Vector3 headDir)
{
headDir = Vector3.zero;
if (_start == null || _cablePanel == null || _cablePanel.CableReelRef == null) return false;
Vector3 offset = _start.position - _cablePanel.CableReelRef.transform.position;
if (offset.sqrMagnitude < 1e-6f) return false;
headDir = offset.normalized;
return true;
}
private void RelaxTailApproach(int tailPinned, Vector3 tailDir)
{
int firstTailIndex = _points.Length - tailPinned;
int approachIndex = firstTailIndex - 1;
if (approachIndex <= config.stiffCount) return;
Vector3 target = _points[firstTailIndex] + tailDir * _tailStiffLength;
_points[approachIndex] = Vector3.Lerp(_points[approachIndex], target, 0.22f);
}
/// <summary>
/// 插头轴向:从插头中心指向线缆接点(PlugRoot)、并继续向外的方向。
/// 仅当末端连接点是 PlugRoot 时有效;连接到 Socket 时返回 false。
/// </summary>
private bool TryGetTailDir(out Vector3 tailDir)
{
tailDir = Vector3.zero;
if (_end == null || _end != _cablePanel.PlugRef.PlugRoot || _end.parent == null) return false;
Vector3 offset = _end.position - _end.parent.position;
if (offset.sqrMagnitude < 1e-6f) return false;
tailDir = offset.normalized;
return true;
}
/// <summary>
/// 为线缆设置末端点
/// </summary>
/// <param name="endPos">末端点</param>
public void SetEndPos(Transform endPos)
{
_end = endPos;
}
public void SetSorting(string sortingLayerName, int sortingOrder)
{
ApplySorting(_lineRenderer, sortingLayerName, sortingOrder);
ApplySorting(_stiffLineRenderer, sortingLayerName, sortingOrder);
}
public void RestoreDefaultSorting()
{
_defaultLineSorting.ApplyTo(_lineRenderer);
_defaultStiffLineSorting.ApplyTo(_stiffLineRenderer);
}
public Vector3 GetDirection()
{
return _points[^1] - _points[^2];
}
private static void ApplySorting(LineRenderer lineRenderer, string sortingLayerName, int sortingOrder)
{
if (lineRenderer == null) return;
lineRenderer.sortingLayerName = sortingLayerName;
lineRenderer.sortingOrder = sortingOrder;
}
private readonly struct RendererSortingState
{
private readonly string _sortingLayerName;
private readonly int _sortingOrder;
private RendererSortingState(string sortingLayerName, int sortingOrder)
{
_sortingLayerName = sortingLayerName;
_sortingOrder = sortingOrder;
}
public static RendererSortingState From(LineRenderer lineRenderer)
{
return new RendererSortingState(lineRenderer.sortingLayerName, lineRenderer.sortingOrder);
}
public void ApplyTo(LineRenderer lineRenderer)
{
if (lineRenderer == null) return;
lineRenderer.sortingLayerName = _sortingLayerName;
lineRenderer.sortingOrder = _sortingOrder;
}
}
}
[Serializable]
public struct CableConfig
{
[Tooltip("物理采样点总数。越大曲线越顺滑,插头端/出线口端刚性点也从这里扣除")]
public int resolution;
[Tooltip("两端距离小于该值时张力取 forceMin(线最松弛)")]
public float dstMin;
[Tooltip("两端距离大于该值时张力取 forceMax(线完全绷直)。想让线平时带点垂坠感就调大它")]
public float dstMax;
[Tooltip("松弛时的张力。越小越软、下垂越明显")]
public float forceMin;
[Tooltip("绷紧时的张力。越大线越像直棍,调小会保留一点弧度")]
public float forceMax;
[Tooltip("每帧物理迭代次数,影响收敛稳定性,一般不用动")]
public int k;
[Tooltip("出线口端刚性点数:线离开出线口时保持笔直的采样点数量")]
public int stiffCount;
[Tooltip("出线口端刚性段长度(世界单位)")]
public float stiffLength;
[Tooltip("Outlet-side rigid section length. Zero uses the tuned default.")]
public float headStiffLength;
[Tooltip("Render-only blend for the first soft control point after the outlet-side rigid section.")]
public float headRenderBlend;
[Tooltip("插头端刚性点数(含末端点,最小 2)。0 = 用默认值 3")]
public int tailStiffCount;
[Tooltip("插头端刚性段长度:线沿插头轴向笔直伸出多远再开始弯。0 = 用 stiffLength 的 2 倍")]
public float tailStiffLength;
[Tooltip("Render-only blend for the first soft control point after the plug-side rigid section.")]
public float tailRenderBlend;
[Tooltip("渲染切角细分次数(1~4):越大转折越圆。0 = 用默认值 2")]
public int smoothIterations;
public static CableConfig GeneDefaultConfig()
{
return new CableConfig
{
resolution = 10,
dstMin = 0.1f,
dstMax = 1.0f,
forceMin = 0.1f,
forceMax = 150f,
k = 10,
stiffCount = 3,
stiffLength = 0.1f,
headStiffLength = 0f,
headRenderBlend = 0f,
tailStiffCount = 0,
tailStiffLength = 0f,
tailRenderBlend = 0f,
smoothIterations = 0
};
}
}
}