update 0.6.5

This commit is contained in:
2026-09-30 08:56:49 +08:00
parent d9f9b2a90f
commit 016aee57ee
30 changed files with 2471 additions and 7 deletions
@@ -5,6 +5,7 @@ using UnityEngine.InputSystem;
#endif
using XericLibrary.Runtime.Blueprint.Canvas;
using XericLibrary.Runtime.Blueprint.Element;
using XericLibrary.Runtime.SuperStyleSheet;
namespace XericLibrary.Runtime.Blueprint
{
@@ -33,7 +34,7 @@ namespace XericLibrary.Runtime.Blueprint
[Header("Input")]
[Tooltip("新输入系统 InputActionAsset。留空则仅支持鼠标基础操作(中键拖动平移、滚轮缩放)。")]
[SerializeField] private Object _inputActions;
public Object InputActions
{
get { return _inputActions; }
Binary file not shown.
@@ -0,0 +1,504 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 一条面片切出来的一个贝塞尔小块:几何、逐点宽度、端点颜色、uv 进度区间。
/// 位置都在 Graphic 的局部平面内,与渲染器同一坐标约定。
/// </summary>
internal struct RibbonPiece
{
public Vector2 c0;
public Vector2 c1;
public Vector2 c2;
public Vector2 c3;
public float w0;
public float w1;
public float w2;
public float w3;
public Color32 startColor;
public Color32 endColor;
public float uvYStart;
public float uvYEnd;
public int ribbonIndex;
/// <summary>本小块占它所在样条段的比例,用来折算细分数</summary>
public float segmentFraction;
}
/// <summary>面片上 uv0.y 代表什么进度</summary>
public enum SplineUvProgressSpace
{
/// <summary>每条面片各自 0→1:尾部 0、头部 1,效果跟着已填充部分伸缩</summary>
RibbonLocal,
/// <summary>钉在整条样条上:贴图位置不随进度变化</summary>
SplineGlobal,
}
/// <summary>
/// 样条面片切分器:把若干条 (头部,尾部) 面片切成逐段贝塞尔小块。
/// <code>
/// 纯函数:不碰 MonoBehaviour,也不碰 Debug.Log —— 后者在 Unity 之外会抛
/// "ECall could not be defined",那样就没法在离线门里直接跑出数来验证。
/// 几何切分用贝塞尔在 [t0,t1] 上的精确限制(Restrict);
/// t0=0,t1=1 时逐点还原原始四个控制点,所以整段不产生任何漂移。
/// </code>
/// </summary>
internal static class SplineRibbonBuilder
{
internal struct Request
{
/// <summary>各控制点锚点(局部坐标),长度 = pointCount</summary>
public Vector2[] anchors;
/// <summary>各点的出手柄(局部坐标世界式,即已含锚点)</summary>
public Vector2[] outHandles;
/// <summary>各点的入手柄(局部坐标世界式,即已含锚点)</summary>
public Vector2[] inHandles;
/// <summary>各点颜色</summary>
public Color[] pointColors;
/// <summary>各点宽度</summary>
public float[] pointWidths;
public int pointCount;
public int segmentCount;
public bool curveLoop;
/// <summary>偶数下标=头部、奇数下标=尾部,成对;取值 [0,1]</summary>
public float[] headTailPairs;
/// <summary>可空;与面片同序时逐条叠乘到样条颜色上</summary>
public Color32[] ribbonTints;
/// <summary>进度 → 参数 t(距离校正就落在这个回调里)</summary>
public Func<float, float> progressToParametric;
public SplineUvProgressSpace uvSpace;
/// <summary>每条面片至少切成几块;决定 uv0.y 与视觉距离对齐的精度</summary>
public int chunksPerRibbon;
}
/// <summary>
/// 切分所有面片。无效输入返回空表而不是抛异常。
/// </summary>
internal static List<RibbonPiece> Build(Request req)
{
var pieces = new List<RibbonPiece>();
if (req.anchors == null || req.outHandles == null || req.inHandles == null)
return pieces;
if (req.headTailPairs == null || req.progressToParametric == null)
return pieces;
if (req.pointCount < 2 || req.segmentCount < 1 || req.headTailPairs.Length < 2)
return pieces;
int chunks = Mathf.Max(1, req.chunksPerRibbon);
int ribbonCount = req.headTailPairs.Length / 2;
for (int r = 0; r < ribbonCount; r++)
{
float head = Mathf.Clamp01(req.headTailPairs[r * 2]);
float tail = Mathf.Clamp01(req.headTailPairs[r * 2 + 1]);
if (Mathf.Abs(head - tail) <= 1e-6f)
continue;
BuildRibbon(req, r, tail, head, chunks, pieces);
}
return pieces;
}
/// <summary>
/// 面片沿路径的一段连续进度区间(起点进度、终点进度、起终点的参数 t)
/// </summary>
private struct Span
{
public float fromProgress;
public float toProgress;
public float fromParam;
public float toParam;
public float ArcLength;
}
private static void BuildRibbon(Request req, int ribbonIndex, float tail, float head,
int chunks, List<RibbonPiece> output)
{
var spans = ResolveSpans(req, tail, head);
if (spans.Count == 0)
return;
float segments = req.segmentCount;
Color32 tint = (req.ribbonTints != null && ribbonIndex < req.ribbonTints.Length)
? req.ribbonTints[ribbonIndex]
: new Color32(255, 255, 255, 255);
bool global = req.uvSpace == SplineUvProgressSpace.SplineGlobal;
int firstOfRibbon = output.Count;
// 与本次新增的 piece 一一对应的实测弧长:RibbonLocal 的 uv 分母就用这一份加总
var arcs = new List<float>();
foreach (var span in spans)
{
if (span.ArcLength <= 0f)
continue;
var cuts = CollectCuts(req, span);
int intervalCount = cuts.Count - 1;
float spanWalked = 0f;
for (int i = 0; i < intervalCount; i++)
{
float a = cuts[i];
float b = cuts[i + 1];
float arc = MeasureArc(req, a, b);
int seg = Mathf.Clamp(Mathf.FloorToInt(a * segments), 0, req.segmentCount - 1);
float localA = Mathf.Clamp01(a * segments - seg);
float localB = Mathf.Clamp01(b * segments - seg);
if (localB - localA <= 1e-6f)
continue;
Vector2 p0, p1, p2, p3;
GetSegment(req, seg, out p0, out p1, out p2, out p3);
float q0, q1, q2, q3;
GetWidthSegment(req, seg, out q0, out q1, out q2, out q3);
var piece = new RibbonPiece();
piece.ribbonIndex = ribbonIndex;
piece.segmentFraction = localB - localA;
Restrict(p0, p1, p2, p3, localA, localB,
out piece.c0, out piece.c1, out piece.c2, out piece.c3);
Restrict(q0, q1, q2, q3, localA, localB,
out piece.w0, out piece.w1, out piece.w2, out piece.w3);
piece.startColor = Tint(ColorAt(req, a), tint);
piece.endColor = Tint(ColorAt(req, b), tint);
// SplineGlobal:落在路径上的绝对进度,环绕时在 1→0 处天然断开。
// spanWalked 只在这里用 —— 它要的是"在本区间内走了多长"。
// RibbonLocal 的 uv 留给下面那一趟,用和分子同一份来源的弧长来分配。
if (global)
{
piece.uvYStart = ProgressAt(span, spanWalked / span.ArcLength);
piece.uvYEnd = ProgressAt(span, (spanWalked + arc) / span.ArcLength);
spanWalked += arc;
output.Add(piece);
continue;
}
spanWalked += arc;
output.Add(piece);
arcs.Add(arc);
}
}
if (global)
return;
// RibbonLocal:整条面片(含环绕的两段)合成一个连续的 0→1。
// <code>
// 分子分母必须同源。以前分母用 MeasureSpanArc(整个区间一次量完的折线长),
// 分子用逐小块的 MeasureArc(切得更细、量得更长),两者能差 0.2%,
// 于是中间的小块算出 uv > 1,末片又被钉成 1 ⇒ 出现"某片 uv 非增"。
// 更糟的是原先靠"emit 计数 == 预计块数"来认最后一块:被跳过的退化小块
// 也已经计过一次数 ⇒ 1 被压到中间某块上,下一块又从 0.6 起重来 ——
// 真机门量到的"跨尾面片 uv 回跳 4 次"就是这么来的。
// 现在出完片再按这份弧长统一分配,两端自然落在 0 与 1,中间跳过几块都不影响单调。
// </code>
float totalArc = 0f;
for (int i = 0; i < arcs.Count; i++)
totalArc += arcs[i];
if (arcs.Count == 0 || totalArc <= 0f)
{
output.RemoveRange(firstOfRibbon, output.Count - firstOfRibbon);
return;
}
float cursor = 0f;
for (int i = 0; i < arcs.Count; i++)
{
int pieceIndex = firstOfRibbon + i;
var piece = output[pieceIndex];
float from = cursor;
cursor += arcs[i];
piece.uvYStart = from / totalArc;
piece.uvYEnd = i == arcs.Count - 1 ? 1f : cursor / totalArc;
output[pieceIndex] = piece;
}
}
/// <summary>
/// 把一对 (头部,尾部) 展开成沿路径的区间。
/// head &gt; tail 是一个区间;循环样条上 head &lt; tail 表示"跨过终点绕回去",
/// 拆成 [tail,1] 与 [0,head] 两个区间,面片仍然连续。非循环样条不做环绕,直接不出片。
/// </summary>
private static List<Span> ResolveSpans(Request req, float tail, float head)
{
var spans = new List<Span>(req.curveLoop ? 2 : 1);
if (head > tail)
{
AddSpan(spans, req, tail, head);
return spans;
}
if (!req.curveLoop)
return spans;
AddSpan(spans, req, tail, 1f);
AddSpan(spans, req, 0f, head);
return spans;
}
private static void AddSpan(List<Span> spans, Request req, float fromProgress, float toProgress)
{
if (toProgress - fromProgress <= 1e-6f)
return;
// 退化:校正后端点仍然重合(长度为零的样条会出现),这个区间不出
float fromParam = Mathf.Clamp01(req.progressToParametric(fromProgress));
float toParam = Mathf.Clamp01(req.progressToParametric(toProgress));
if (toParam - fromParam <= 1e-6f)
return;
var span = new Span
{
fromProgress = fromProgress,
toProgress = toProgress,
fromParam = fromParam,
toParam = toParam,
};
span.ArcLength = MeasureSpanArc(req, fromParam, toParam);
spans.Add(span);
}
/// <summary>
/// 跨段的区间弧长:先按样条段边界切开,再逐段量。
/// MeasureArc 只能量"同一段内"的两点,直接拿来量整段会静默少算。
/// </summary>
private static float MeasureSpanArc(Request req, float a, float b)
{
float segments = req.segmentCount;
float sum = 0f;
float cursor = a;
while (cursor < b - 1e-6f)
{
int seg = Mathf.Clamp(Mathf.FloorToInt(cursor * segments), 0, req.segmentCount - 1);
float nextBoundary = (seg + 1f) / segments;
float stop = Mathf.Min(b, nextBoundary);
sum += MeasureArc(req, cursor, stop);
cursor = stop;
}
return sum;
}
/// <summary>区间内按弧长占比折算出该处的进度值</summary>
private static float ProgressAt(Span span, float fraction)
{
return Mathf.Clamp01(span.fromProgress + (span.toProgress - span.fromProgress) * fraction);
}
/// <summary>
/// 切分点:等进度网格 + 所有落在区间内的样条段边界,去重后升序。
/// 有了段边界,每个小块必然完整落在同一段贝塞尔里。
/// </summary>
private static List<float> CollectCuts(Request req, Span span)
{
int chunks = Mathf.Max(1, req.chunksPerRibbon);
var cuts = new List<float>(chunks + req.segmentCount + 2) { span.fromParam, span.toParam };
for (int s = 1; s < req.segmentCount; s++)
{
float boundary = (float)s / req.segmentCount;
if (boundary > span.fromParam && boundary < span.toParam)
cuts.Add(boundary);
}
for (int k = 1; k < chunks; k++)
{
float progress = span.fromProgress
+ (span.toProgress - span.fromProgress) * k / chunks;
cuts.Add(Mathf.Clamp01(req.progressToParametric(progress)));
}
cuts.Sort();
var unique = new List<float>(cuts.Count);
for (int i = 0; i < cuts.Count; i++)
{
if (unique.Count > 0 && cuts[i] - unique[unique.Count - 1] <= 1e-6f)
continue;
unique.Add(cuts[i]);
}
return unique;
}
/// <summary>段 seg 的四个控制点:本点锚、本点出手柄、下一点入手柄、下一点锚</summary>
private static void GetSegment(Request req, int seg,
out Vector2 p0, out Vector2 p1, out Vector2 p2, out Vector2 p3)
{
int next = NextPoint(req, seg);
p0 = req.anchors[seg];
p1 = req.outHandles[seg];
p2 = req.inHandles[next];
p3 = req.anchors[next];
}
/// <summary>
/// 宽度也走同一条贝塞尔:本段两端取本点宽、下段两端取下点宽。
/// 与几何用同一个 [t0,t1] 限制,带宽过渡才不会和形状错位。
/// </summary>
private static void GetWidthSegment(Request req, int seg,
out float w0, out float w1, out float w2, out float w3)
{
int next = NextPoint(req, seg);
w0 = WidthAt(req, seg);
w1 = WidthAt(req, seg);
w2 = WidthAt(req, next);
w3 = WidthAt(req, next);
}
private static int NextPoint(Request req, int seg)
{
return req.curveLoop
? PositiveMod(seg + 1, req.pointCount)
: Mathf.Min(seg + 1, req.pointCount - 1);
}
private static float WidthAt(Request req, int index)
{
if (req.pointWidths == null || req.pointWidths.Length == 0)
return 0f;
return req.pointWidths[Mathf.Clamp(index, 0, req.pointWidths.Length - 1)];
}
/// <summary>按渐层取色;索引越界一律夹紧,绝不裸索引(渐层列表可能比点少)</summary>
private static Color ColorAt(Request req, float parametric)
{
if (req.pointColors == null || req.pointColors.Length == 0)
return Color.white;
int count = req.pointColors.Length;
// 与 XericRectSpline.GetDeltaT 同一套映射(那个是 internal,Release 侧够不到),
// 所以取色和几何形状保持同相
float usable = req.curveLoop ? count : Mathf.Max(1, count - 1);
float scaled = Mathf.Clamp01(parametric) * usable;
int index = Mathf.FloorToInt(scaled);
float dt = Mathf.Clamp01(scaled - index);
int a = req.curveLoop ? PositiveMod(index, count) : Mathf.Clamp(index, 0, count - 1);
int b = req.curveLoop ? PositiveMod(index + 1, count) : Mathf.Clamp(index + 1, 0, count - 1);
return Color.Lerp(req.pointColors[a], req.pointColors[b], dt);
}
private static Color32 Tint(Color baseColor, Color32 tint)
{
var t = (Color)tint;
var r = new Color(baseColor.r * t.r, baseColor.g * t.g,
baseColor.b * t.b, baseColor.a * t.a);
return r;
}
private static int PositiveMod(int value, int modulus)
{
if (modulus <= 0)
return 0;
return ((value % modulus) + modulus) % modulus;
}
// ---------------------------------------------------------------- 贝塞尔裁剪
/// <summary>
/// 三次贝塞尔在 [t0,t1] 上的精确限制:
/// Q0=B(t0),Q1=B(t0)+(t1-t0)/3·B'(t0),Q2=B(t1)-(t1-t0)/3·B'(t1),Q3=B(t1)。
/// </summary>
internal static void Restrict(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3,
float t0, float t1, out Vector2 q0, out Vector2 q1, out Vector2 q2, out Vector2 q3)
{
float span = t1 - t0;
q0 = Eval(p0, p1, p2, p3, t0);
q1 = q0 + Derivative(p0, p1, p2, p3, t0) * (span / 3f);
q3 = Eval(p0, p1, p2, p3, t1);
q2 = q3 - Derivative(p0, p1, p2, p3, t1) * (span / 3f);
}
internal static void Restrict(float p0, float p1, float p2, float p3,
float t0, float t1, out float q0, out float q1, out float q2, out float q3)
{
float span = t1 - t0;
q0 = Eval(p0, p1, p2, p3, t0);
q1 = q0 + Derivative(p0, p1, p2, p3, t0) * (span / 3f);
q3 = Eval(p0, p1, p2, p3, t1);
q2 = q3 - Derivative(p0, p1, p2, p3, t1) * (span / 3f);
}
internal static Vector2 Eval(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
var a = Vector2.Lerp(p0, p1, t);
var b = Vector2.Lerp(p1, p2, t);
var c = Vector2.Lerp(p2, p3, t);
var d = Vector2.Lerp(a, b, t);
var e = Vector2.Lerp(b, c, t);
return Vector2.Lerp(d, e, t);
}
internal static float Eval(float p0, float p1, float p2, float p3, float t)
{
var a = Mathf.Lerp(p0, p1, t);
var b = Mathf.Lerp(p1, p2, t);
var c = Mathf.Lerp(p2, p3, t);
var d = Mathf.Lerp(a, b, t);
var e = Mathf.Lerp(b, c, t);
return Mathf.Lerp(d, e, t);
}
/// <summary>导数 = 3 × 一阶差分控制点的二次贝塞尔</summary>
internal static Vector2 Derivative(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
var d0 = p1 - p0;
var d1 = p2 - p1;
var d2 = p3 - p2;
var a = Vector2.Lerp(d0, d1, t);
var b = Vector2.Lerp(d1, d2, t);
return Vector2.Lerp(a, b, t) * 3f;
}
internal static float Derivative(float p0, float p1, float p2, float p3, float t)
{
var d0 = p1 - p0;
var d1 = p2 - p1;
var d2 = p3 - p2;
var a = Mathf.Lerp(d0, d1, t);
var b = Mathf.Lerp(d1, d2, t);
return Mathf.Lerp(a, b, t) * 3f;
}
/// <summary>小块弧长(折线近似,够 uv 分配用)</summary>
private static float MeasureArc(Request req, float a, float b)
{
const int samples = 5;
int seg = Mathf.Clamp(Mathf.FloorToInt(a * req.segmentCount), 0, req.segmentCount - 1);
Vector2 p0, p1, p2, p3;
GetSegment(req, seg, out p0, out p1, out p2, out p3);
float la = Mathf.Clamp01(a * req.segmentCount - seg);
float lb = Mathf.Clamp01(b * req.segmentCount - seg);
var last = Eval(p0, p1, p2, p3, la);
float sum = 0f;
for (int i = 1; i <= samples; i++)
{
var next = Eval(p0, p1, p2, p3, Mathf.Lerp(la, lb, (float)i / samples));
sum += Vector2.Distance(last, next);
last = next;
}
return sum;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2849f073cdc1b8028779b7b6c5fedfe3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using XericLibrary.Runtime.XericComponent;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 由界面样条(XericRectSplinePath)驱动的曲线绘制组件。
/// <code>
/// 一条样条上可以同时铺多条"面片",每条由一对 (头部, 尾部) 进度值决定覆盖范围,
/// 于是 {1,0} 是铺满整条的底轨,{0.6f,0} 就是填到 60% 的进度条本体。
/// 进度默认经样条烘焙后的弧长重映射校正,画面上的推进长度才和进度值成正比;
/// 面片上的 uv0.y 被重映射成整条面片的 0→1,材质按 UV 就能做流动/遮罩类效果。
/// </code>
/// </summary>
[AddComponentMenu("Xeric Library/UI/UISplineCurveRenderer", 17)]
public class UISplineCurveRenderer : CurveCacheRendererBase
{
#region 字段属性
/// <summary>驱动本曲线的界面样条</summary>
public XericRectSplinePath splinePath;
/// <summary>
/// 面片对:偶数下标=头部、奇数下标=尾部,取值 [0,1]。
/// 例:{1,0} 一条铺满;{1,0, 0.6f,0} 两条=底轨 + 60% 填充。
/// </summary>
public float[] headTailPairs = { 1f, 0f };
/// <summary>可空;与面片同序时逐条叠乘到样条颜色上(底轨压暗、填充提亮)</summary>
public Color32[] ribbonTints;
/// <summary>进度值的含义:直接按参数,还是先经烘焙做弧长校正</summary>
public SplineProgressMapping progressMapping = SplineProgressMapping.BakedDistance;
/// <summary>烘焙采样数,越大校正越细;同时压住库侧 1/(N+1) 的系统偏差</summary>
[Range(8, 4096)]
public int bakeSubdivide = 256;
/// <summary>
/// 什么时候做弧长烘焙。默认 OnEditEnd:几何停止变化后补烘一次。
/// 烘焙是 O(subdivide) 的采样,放在每帧会把编辑器拖到卡。
/// </summary>
public SplineBakeTrigger bakeTrigger = SplineBakeTrigger.OnEditEnd;
/// <summary>已经烘过几次(排查用:编辑器里拖动一整轮应该只加 1)</summary>
public int BakeRunCount => m_BakeRunCount;
/// <summary>面片上 uv0.y 代表条内进度还是全样条进度</summary>
public SplineUvProgressSpace uvProgressSpace = SplineUvProgressSpace.RibbonLocal;
/// <summary>每条面片至少切成几块,决定 uv 与视觉距离对齐的精度</summary>
[Range(1, 64)]
public int chunksPerRibbon = 8;
/// <summary>基准带宽(样条所在矩形的局部像素)</summary>
[Min(0.01f)]
public float curveWidth = 10f;
/// <summary>带宽来源:常量,还是乘上样条的缩放渐层 Y</summary>
public SplineWidthSource widthSource = SplineWidthSource.ScaleGradientY;
/// <summary>单段贝塞尔的细分段数</summary>
[Range(2, 128)]
public int tessellationSegments = 32;
/// <summary>样条颜色渐层全透明时的兜底颜色</summary>
public Color fallbackColor = Color.white;
/// <summary>
/// 推给材质 _MainTex 的贴图。
/// Canvas 是用 MaterialPropertyBlock 把 Graphic.mainTexture 顶到 _MainTex 上的,
/// 所以必须走这个口子交出去;只改 material 的贴图会被默认白图覆盖掉。
/// </summary>
public Texture progressTexture;
/// <summary>SetProgress 作用在哪条面片的头部;-1 表示不指定</summary>
public int fillRibbonIndex = 1;
/// <summary>每帧跟随样条变化重建(关掉后只能靠 RebuildCurve 手动触发)</summary>
public bool followSplineChanges = true;
/// <summary>与曲线缓存同序的 uv 区间,x=起点 y=终点</summary>
private readonly List<Vector2> m_PieceUv = new List<Vector2>();
private bool m_NeedUvPatch;
private int m_LastBakeSignature = int.MinValue;
private bool m_BakePending;
private int m_BakeRunCount;
#endregion
#region 生命周期
public override Texture mainTexture
=> progressTexture != null ? progressTexture : base.mainTexture;
protected override void OnEnable()
{
base.OnEnable();
RebuildCurve();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (curveWidth < 0.01f) curveWidth = 0.01f;
if (tessellationSegments < 2) tessellationSegments = 2;
if (followSplineChanges && isActiveAndEnabled)
{
EditorApplication.delayCall += () =>
{
if (this != null && isActiveAndEnabled)
RebuildCurve();
};
}
}
#endif
protected override void LateUpdate()
{
if (followSplineChanges)
RebuildCurve();
base.LateUpdate();
// 只能在"刚刚重建过缓存"之后改写一次:
// 改写后 uv0.y 就不再是生成器写的段内 t,重复改写会把映射套两遍
if (m_NeedUvPatch)
{
m_NeedUvPatch = false;
ApplyRibbonUv();
}
}
#endregion
#region API
/// <summary>
/// 把指定面片的头部推到 value(进度条前进就调这个)
/// </summary>
public void SetProgress(float value)
{
int index = fillRibbonIndex >= 0 ? fillRibbonIndex : 0;
if (headTailPairs == null || headTailPairs.Length < (index + 1) * 2)
return;
headTailPairs[index * 2] = Mathf.Clamp01(value);
RebuildCurve();
}
/// <summary>
/// 读取指定面片的头部进度
/// </summary>
public float GetProgress()
{
int index = fillRibbonIndex >= 0 ? fillRibbonIndex : 0;
if (headTailPairs == null || headTailPairs.Length < (index + 1) * 2)
return 0f;
return headTailPairs[index * 2];
}
/// <summary>
/// 按当前样条与面片配置重建曲线
/// </summary>
public void RebuildCurve()
{
ClearAll();
m_PieceUv.Clear();
m_NeedUvPatch = false;
if (splinePath == null || rectTransform == null)
return;
var spline = splinePath.xericSpline;
if (spline == null || spline.Count < 2)
return;
// 锚点→世界的换算依赖矩形角点
splinePath.UpdateRectDepend();
var rect = rectTransform.rect;
if (Mathf.Abs(rect.width) < 1e-6f || Mathf.Abs(rect.height) < 1e-6f)
return;
int pointCount = spline.Count;
var anchors = new Vector2[pointCount];
var outHandles = new Vector2[pointCount];
var inHandles = new Vector2[pointCount];
var colors = new Color[pointCount];
var widths = new float[pointCount];
int signature = pointCount;
for (int i = 0; i < pointCount; i++)
{
anchors[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetPointPosition(i));
outHandles[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetEndHandlePosition(i));
inHandles[i] = (Vector2)rectTransform.InverseTransformPoint(splinePath.GetStartHandlePosition(i));
colors[i] = ColorAt(spline, i);
widths[i] = WidthAt(spline, i);
unchecked
{
signature = signature * 31 + anchors[i].GetHashCode()
+ outHandles[i].GetHashCode() * 7 + inHandles[i].GetHashCode() * 13;
}
}
// 非均匀缩放矩形会改变世界空间弧长的比例,签名要把矩形尺寸也算进去
unchecked
{
signature = signature * 31 + rect.width.GetHashCode() + rect.height.GetHashCode() * 3;
}
var request = new SplineRibbonBuilder.Request
{
anchors = anchors,
outHandles = outHandles,
inHandles = inHandles,
pointColors = colors,
pointWidths = widths,
pointCount = pointCount,
segmentCount = spline.useablePointCount,
curveLoop = spline.CurveLoop,
headTailPairs = headTailPairs,
ribbonTints = ribbonTints,
uvSpace = uvProgressSpace,
chunksPerRibbon = chunksPerRibbon,
progressToParametric = BuildMapping(spline, signature),
};
var pieces = SplineRibbonBuilder.Build(request);
if (pieces.Count == 0)
return;
var ctrl = new Vector2[4];
var pieceWidths = new float[4];
foreach (var piece in pieces)
{
ctrl[0] = piece.c0; ctrl[1] = piece.c1; ctrl[2] = piece.c2; ctrl[3] = piece.c3;
pieceWidths[0] = piece.w0; pieceWidths[1] = piece.w1;
pieceWidths[2] = piece.w2; pieceWidths[3] = piece.w3;
var entry = new CurveCacheEntry
{
startColor = piece.startColor,
endColor = piece.endColor,
tessellationSegments = PieceTessellation(piece),
};
AddCurve(entry, (Vector2[])ctrl.Clone(), (float[])pieceWidths.Clone());
m_PieceUv.Add(new Vector2(piece.uvYStart, piece.uvYEnd));
}
m_NeedUvPatch = m_PieceUv.Count > 0;
}
#endregion
#region 取数
/// <summary>
/// 造出"进度 → 参数 t"的映射。校正走样条烘焙的弧长重映射,
/// 库侧要求先烘焙且长度不足时会抛,所以这里自己把关并回落。
/// </summary>
private Func<float, float> BuildMapping(XericRectSpline spline, int signature)
{
if (progressMapping != SplineProgressMapping.BakedDistance)
return ParametricIdentity;
if (!EnsureBaked(spline, signature))
return ParametricIdentity;
return ParametricFromBakedDistance;
}
private float ParametricIdentity(float progress) => progress;
/// <summary>
/// 进度→参数 t。两端是边界真值:进度 0 就是曲线起点、1 就是终点,
/// 所以先把两端钉死再走重映射 —— 库里的弧长重映射原本按"采样点数"而不是"段数"
/// 做分母,进度 1 会落在 (N-1)/N,{1,0} 的面片就永远短一小截贴不到末尾。
/// 库侧已经修了分母,这里钉端点既是兜底也让旧 DLL 下也能立刻闭合。
/// </summary>
private float ParametricFromBakedDistance(float progress)
{
if (progress <= 0f) return 0f;
if (progress >= 1f) return 1f;
return Mathf.Clamp01(splinePath.RemappingTimeValue(progress));
}
/// <summary>
/// 按 <see cref="bakeTrigger"/> 决定这一趟烘不烘,返回 false 表示当前用不了弧长校正。
/// <code>
/// 烘焙是 O(subdivide) 的采样:以前几何签名一变就烘,编辑器里拖动控制点等于每帧烘一次。
/// OnEditEnd 的做法是"几何正在线上变"时先挂起、继续用上一次那份烘焙数据,
/// 等下一趟签名不再变化(编辑停了)才补烘那一次。
/// </code>
/// </summary>
private bool EnsureBaked(XericRectSpline spline, int signature)
{
int target = Mathf.Max(bakeSubdivide, spline.Count);
bool geometryChanged = signature != m_LastBakeSignature;
m_LastBakeSignature = signature;
switch (bakeTrigger)
{
case SplineBakeTrigger.Manual:
// 一次都不自动烘,等 BakeNow
return HasUsableBake(spline, target);
case SplineBakeTrigger.EveryRebuild:
if (!geometryChanged && HasUsableBake(spline, target))
return true;
return RunBake(spline, target);
default:
if (geometryChanged)
{
// 几何正在线上变:这一趟不烘,也**不用**上一份烘焙数据。
// 结构大改(清点重铺、切循环)后那份表对应的已经不是这条曲线了,
// 拿它做校正会把面片排到错处 —— 宁可这一趟退回参数映射,
// 等编辑停下来的下一趟烘完再恢复校正。
m_BakePending = true;
return false;
}
if (!m_BakePending)
return HasUsableBake(spline, target);
m_BakePending = false;
return RunBake(spline, target);
}
}
/// <summary>烘焙数据存在且长度够用(旧数据只要长度够就继续用,别再烘一次)</summary>
private static bool HasUsableBake(XericRectSpline spline, int target)
=> spline.BakeCount >= target;
private bool RunBake(XericRectSpline spline, int target)
{
splinePath.BakePoint(target);
m_BakeRunCount++;
return spline.BakeCount >= target;
}
/// <summary>
/// 手动烘一次(bakeTrigger 为 Manual 时用这个,或改完曲线想立刻精确时按)。
/// 烘完直接重建,按下就能看见贴合结果。
/// </summary>
public void BakeNow()
{
if (splinePath == null || splinePath.xericSpline == null)
return;
var spline = splinePath.xericSpline;
RunBake(spline, Mathf.Max(bakeSubdivide, spline.Count));
m_BakePending = false;
RebuildCurve();
}
private Color ColorAt(XericRectSpline spline, int index)
{
var gradient = spline.ColorGradient;
if (gradient == null || index < 0 || index >= gradient.Count)
return fallbackColor;
var color = gradient[index];
return color.a <= 0f ? fallbackColor : color;
}
private float WidthAt(XericRectSpline spline, int index)
{
switch (widthSource)
{
case SplineWidthSource.ScaleGradientY:
return curveWidth * Mathf.Abs(GradientAt(spline.ScaleGradient, index, Vector3.one).y);
case SplineWidthSource.FloatGradient:
// 额外参数本来就是"每点多大"的语义时用它最直观;
// 注意它默认全 0,不填就什么都画不出来
return curveWidth * Mathf.Max(0f, GradientAt(spline.FloatGradient, index, 1f));
default:
return curveWidth;
}
}
/// <summary>渐层可能比点数短(在 Inspector 里直接改 ControlPoint 长度就会这样),一律夹紧</summary>
private static T GradientAt<T>(SplineGradientBase<T> gradient, int index, T fallback)
{
if (gradient == null || index < 0 || index >= gradient.Count)
return fallback;
return gradient[index];
}
private int PieceTessellation(RibbonPiece piece)
{
int tess = Mathf.RoundToInt(tessellationSegments * Mathf.Clamp01(piece.segmentFraction));
return Mathf.Clamp(tess, 2, 512);
}
#endregion
#region uv 重写
/// <summary>
/// 把生成器写在每个小块里的"段内 t"重映射成整条面片的 0→1。
/// 默认材质只用到 uv0.x(边缘羽化),uv0.y 留给换上去的材质消费。
/// </summary>
private void ApplyRibbonUv()
{
if (m_PieceUv.Count == 0)
return;
for (int i = 0; i < m_Curves.Count && i < m_PieceUv.Count; i++)
{
var curve = m_Curves[i];
var range = m_PieceUv[i];
int end = curve.startVertexIndex + curve.vertexCount;
if (end > m_MeshVertexCache.Count)
end = m_MeshVertexCache.Count;
for (int v = curve.startVertexIndex; v < end; v++)
{
var vert = m_MeshVertexCache[v];
vert.uv0.y = Mathf.LerpUnclamped(range.x, range.y, vert.uv0.y);
m_MeshVertexCache[v] = vert;
}
}
SetVerticesDirty();
}
#endregion
}
/// <summary>进度值如何换算成曲线参数</summary>
public enum SplineProgressMapping
{
/// <summary>进度直接当参数 t:段与段的视觉长度不等</summary>
Parametric,
/// <summary>进度按烘焙后的弧长重映射:画面上推进的长度和进度成正比</summary>
BakedDistance,
}
/// <summary>弧长烘焙的触发时机(校正的数据源就是这份烘焙数据)</summary>
public enum SplineBakeTrigger
{
/// <summary>从不自动烘,只认 BakeNow / Inspector 上的烘焙按钮</summary>
Manual,
/// <summary>几何停止变化后补烘一次:编辑器里拖动控制点不会逐帧烘</summary>
OnEditEnd,
/// <summary>几何一变就烘(每帧一变烘一次,贵;只在需要立刻精确时用)</summary>
EveryRebuild,
}
/// <summary>带宽来源</summary>
public enum SplineWidthSource
{
/// <summary>恒用 curveWidth</summary>
Constant,
/// <summary>curveWidth × 该点缩放渐层的 Y(渐层默认是 1,不填也等价于常量)</summary>
ScaleGradientY,
/// <summary>curveWidth × 该点的额外参数。默认全 0,不填什么都画不出来</summary>
FloatGradient,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91cbc8bc694b85a8f15fefce8d3a72b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using UnityEngine;
namespace XericLibrary.Runtime.UIGraph
{
/// <summary>
/// 曲线进度条的播放驱动器:把时间推成 UISplineCurveRenderer 的进度。
/// 只负责"进度从哪来",几何与着色全在渲染器那一侧。
/// </summary>
[AddComponentMenu("Xeric Library/UI/UISplineProgressDriver", 18)]
[RequireComponent(typeof(UISplineCurveRenderer))]
public class UISplineProgressDriver : MonoBehaviour
{
/// <summary>自动推进(关掉就只认手动 SetProgress)</summary>
public bool autoDrive = true;
/// <summary>从 0 走到 1 所需秒数</summary>
[Min(0.01f)]
public float duration = 2.5f;
/// <summary>用 scaledTime,随引擎暂停/缩放</summary>
public bool useUnscaledTime = false;
/// <summary>走到尽头后往返还是从头再来</summary>
public bool pingPong = false;
/// <summary>起始进度</summary>
[Range(0f, 1f)]
public float startProgress = 0f;
private UISplineCurveRenderer m_Renderer;
private float m_Elapsed;
private void Awake()
{
m_Renderer = GetComponent<UISplineCurveRenderer>();
m_Elapsed = startProgress * duration;
if (m_Renderer != null)
m_Renderer.SetProgress(startProgress);
}
private void Update()
{
if (!autoDrive || m_Renderer == null)
return;
m_Elapsed += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
float cycle = m_Elapsed / duration;
float progress = pingPong
? Mathf.PingPong(cycle, 1f)
: cycle - Mathf.Floor(cycle);
m_Renderer.SetProgress(progress);
}
/// <summary>手动设进度,同时让自动推进从该处继续</summary>
public void SetProgress(float value)
{
value = Mathf.Clamp01(value);
m_Elapsed = value * duration;
if (m_Renderer != null)
m_Renderer.SetProgress(value);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f851c7a6653a78879ce665e643f5a28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+127
View File
@@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 65b82257fc9da6b4ba046b02555dd0d6
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
@@ -1,6 +1,6 @@
fileFormatVersion: 2
guid: 9abe5f919a1840a4dace5de52d721a06
TextScriptImporter:
guid: affe7d56dc01a48439aedc888552bf47
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
+638
View File
@@ -0,0 +1,638 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 705507994}
m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641275, b: 0.5748172, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 500
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &246206081
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 246206082}
- component: {fileID: 246206084}
- component: {fileID: 246206083}
m_Layer: 5
m_Name: LineRenderer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &246206082
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 246206081}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1.5, y: 1.5, z: 1.5}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 387643621}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 1000, y: 300}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &246206083
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 246206081}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5b33b2e663e78774c9f0c9af55018725, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
points:
- {x: 0, y: 0}
- {x: 58.7, y: 138.4}
- {x: 125.1, y: 80.6}
- {x: 238.9, y: 300}
- {x: 354, y: 100}
- {x: 512.2, y: 70}
- {x: 716.1, y: 192.5}
- {x: 938.8, y: 3.85}
- {x: 1000, y: 133}
thickness: 20
center: 1
--- !u!222 &246206084
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 246206081}
m_CullTransparentMesh: 1
--- !u!1 &387643617
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 387643621}
- component: {fileID: 387643620}
- component: {fileID: 387643619}
- component: {fileID: 387643618}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &387643618
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &387643619
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 1920, y: 1080}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
--- !u!223 &387643620
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 0
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!224 &387643621
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 387643617}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1231462426}
- {fileID: 246206082}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
--- !u!1 &705507993
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 705507995}
- component: {fileID: 705507994}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &705507994
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 705507993}
m_Enabled: 1
serializedVersion: 10
m_Type: 1
m_Shape: 0
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 1
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &705507995
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 705507993}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!1 &963194225
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 963194228}
- component: {fileID: 963194227}
- component: {fileID: 963194226}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &963194226
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
--- !u!20 &963194227
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &963194228
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 963194225}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1231462425
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1231462426}
- component: {fileID: 1231462428}
- component: {fileID: 1231462427}
m_Layer: 5
m_Name: Background
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1231462426
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1231462425}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 387643621}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1231462427
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1231462425}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 980e274b7ed2de945bdefcc1d0423472, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &1231462428
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1231462425}
m_CullTransparentMesh: 1
--- !u!1 &1721653935
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1721653938}
- component: {fileID: 1721653937}
- component: {fileID: 1721653936}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1721653936
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1721653935}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &1721653937
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1721653935}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1721653938
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1721653935}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 963194228}
- {fileID: 705507995}
- {fileID: 387643621}
- {fileID: 1721653938}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: eb3b7561320c3df409493f5b073278f0
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,91 @@
using UnityEngine;
using UnityEngine.UI;
namespace Radishmouse
{
[RequireComponent(typeof(CanvasRenderer))]
public class XericSimpleUILineRenderer : MaskableGraphic
{
public Vector2[] points;
public float thickness = 10f;
public bool center = true;
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (points.Length < 2)
return;
for (int i = 0; i < points.Length-1; i++)
{
// Create a line segment between the next two points
CreateLineSegment(points[i], points[i+1], vh);
int index = i * 5;
// Add the line segment to the triangles array
vh.AddTriangle(index, index+1, index+3);
vh.AddTriangle(index+3, index+2, index);
// These two triangles create the beveled edges
// between line segments using the end point of
// the last line segment and the start points of this one
if (i != 0)
{
vh.AddTriangle(index, index-1, index-3);
vh.AddTriangle(index+1, index-1, index-2);
}
}
}
/// <summary>
/// Creates a rect from two points that acts as a line segment
/// </summary>
/// <param name="point1">The starting point of the segment</param>
/// <param name="point2">The endint point of the segment</param>
/// <param name="vh">The vertex helper that the segment is added to</param>
private void CreateLineSegment(Vector3 point1, Vector3 point2, VertexHelper vh)
{
Vector3 offset = center ? (rectTransform.sizeDelta / 2) : Vector2.zero;
// Create vertex template
UIVertex vertex = UIVertex.simpleVert;
vertex.color = color;
// Create the start of the segment
Quaternion point1Rotation = Quaternion.Euler(0, 0, RotatePointTowards(point1, point2) + 90);
vertex.position = point1Rotation * new Vector3(-thickness / 2, 0);
vertex.position += point1 - offset;
vh.AddVert(vertex);
vertex.position = point1Rotation * new Vector3(thickness / 2, 0);
vertex.position += point1 - offset;
vh.AddVert(vertex);
// Create the end of the segment
Quaternion point2Rotation = Quaternion.Euler(0, 0, RotatePointTowards(point2, point1) - 90);
vertex.position = point2Rotation * new Vector3(-thickness / 2, 0);
vertex.position += point2 - offset;
vh.AddVert(vertex);
vertex.position = point2Rotation * new Vector3(thickness / 2, 0);
vertex.position += point2 - offset;
vh.AddVert(vertex);
// Also add the end point
vertex.position = point2 - offset;
vh.AddVert(vertex);
}
/// <summary>
/// Gets the angle that a vertex needs to rotate to face target vertex
/// </summary>
/// <param name="vertex">The vertex being rotated</param>
/// <param name="target">The vertex to rotate towards</param>
/// <returns>The angle required to rotate vertex towards target</returns>
private float RotatePointTowards(Vector2 vertex, Vector2 target)
{
return (float)(Mathf.Atan2(target.y - vertex.y, target.x - vertex.x) * (180 / Mathf.PI));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e20d14972d02c04eba084112d510af7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
Binary file not shown.