Signed-off-by: TRADER_FOER <lhf190@outlook.com>
This commit is contained in:
2026-06-12 16:25:52 +08:00
parent c99c10fd37
commit 4b7f25e47a
51 changed files with 449934 additions and 8525 deletions

View File

@@ -1,8 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DG.Tweening;
using Ichni;
using Ichni.RhythmGame;
@@ -10,6 +7,7 @@ using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using UnityEngine.UI.Extensions;
public partial class EventPoint : MonoBehaviour
{
@@ -24,7 +22,7 @@ public partial class EventPoint : MonoBehaviour
public RectTransform LeftSide;
public RectTransform RightSide;
public Button selectButton;
public RawImage CurveCanvas;
public UILineRenderer Linerender;
public FlexibleFloatTab FatherTab;
public TMP_Text ViewText;
@@ -33,6 +31,20 @@ public partial class EventPoint : MonoBehaviour
public int BeatDeviver => FatherTab.BeatDeviver;
public void Initialize(AnimatedFloat animatedFloat)
{
// 初始化 UILineRenderer
if (Linerender != null)
{
// 移除残留的旧 RawImage 组件(预制体迁移用)
var oldRawImage = Linerender.GetComponent<RawImage>();
if (oldRawImage != null)
Destroy(oldRawImage);
Linerender.RelativeSize = true;
Linerender.color = Color.green;
Linerender.LineThickness = 2;
Linerender.raycastTarget = false;
}
this.animatedFloat = animatedFloat;
transform.localPosition = new Vector3(
animatedFloat.startTime / EditorManager.instance.timeline.timePerBeat * BeatDeviver, 0, 0
@@ -44,9 +56,12 @@ public partial class EventPoint : MonoBehaviour
EvDrawimage.transform.localPosition = new Vector3(EvDrawimage.rectTransform.sizeDelta.x / 2, 0, 0);
OvDrawimage.transform.localPosition = RightSide.localPosition;
CurveCanvas.rectTransform.sizeDelta = new Vector2(EvDrawimage.rectTransform.sizeDelta.x, EvDrawimage.rectTransform.sizeDelta.y);
// 锁定 UILineRenderer 位置以匹配 EvDrawimage
Linerender.rectTransform.sizeDelta = new Vector2(EvDrawimage.rectTransform.sizeDelta.x, EvDrawimage.rectTransform.sizeDelta.y);
Linerender.rectTransform.anchorMin = EvDrawimage.rectTransform.anchorMin;
Linerender.rectTransform.anchorMax = EvDrawimage.rectTransform.anchorMax;
Linerender.rectTransform.pivot = EvDrawimage.rectTransform.pivot;
Linerender.rectTransform.anchoredPosition = EvDrawimage.rectTransform.anchoredPosition;
@@ -222,111 +237,26 @@ public partial class EventPoint : MonoBehaviour
}
public partial class EventPoint//显示?
{
public IEnumerator GenerateTextureCoroutine(int width, int height, float value)
{
Task<Color[]> task = Task.Run(() => GenerateTextureColors(width, height, value));
while (!task.IsCompleted)
{
yield return null; // 等待下一帧
}
Color[] textureColors = task.Result;
Texture2D Texture = new Texture2D(width, height);
Texture.SetPixels(textureColors);
Texture.Apply();
CurveCanvas.texture = Texture;
// CurveCanvas.color = new Color(1, 1, 1, 0);
// CurveCanvas.DOColor(new Color(1, 1, 1, 1), 0.2f).SetEase(Ease.InOutSine);
}
public Color[] GenerateTextureColors(int width, int height, float value)
{
Color[] pixels = new Color[width * height];
// 初始化所有像素为透明
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = new Color(0, 0, 0, 0);
}
int LastEventPointY = 0;
for (int i = 0; i < width; i++)
{
float t = (float)i / width;
int f = (int)(
(height / 2) + (animatedFloat.startValue * value + ((animatedFloat.endValue - animatedFloat.startValue)
* AnimationCurveEvaluator.Evaluate(animatedFloat.animationCurveType, t) * value))
);
// 绘制垂直线段 - 保留超出边界的红色标记
if (LastEventPointY < f)
{
for (int j = LastEventPointY; j < f; j++)
{
// 检查是否超出边界
bool isOutOfBounds = j < 0 || j >= height;
// 计算实际坐标(循环调整)
int actualY = j;
while (actualY < 0) actualY += height;
while (actualY >= height) actualY -= height;
int index = actualY * width + i;
if (index >= 0 && index < pixels.Length)
{
// 根据是否超出边界设置颜色
pixels[index] = isOutOfBounds ? Color.red : Color.green;
}
}
}
else
{
for (int j = LastEventPointY; j > f; j--)
{
// 检查是否超出边界
bool isOutOfBounds = j < 0 || j >= height;
// 计算实际坐标(循环调整)
int actualY = j;
while (actualY < 0) actualY += height;
while (actualY >= height) actualY -= height;
int index = actualY * width + i;
if (index >= 0 && index < pixels.Length)
{
// 根据是否超出边界设置颜色
pixels[index] = isOutOfBounds ? Color.red : Color.green;
}
}
}
// 绘制当前点 - 保留超出边界的红色标记
bool isFOutOfBounds = f < 0 || f >= height;
int actualF = f;
while (actualF < 0) actualF += height;
while (actualF >= height) actualF -= height;
int currentIndex = actualF * width + i;
if (currentIndex >= 0 && currentIndex < pixels.Length)
{
// 根据是否超出边界设置颜色
pixels[currentIndex] = isFOutOfBounds ? Color.red : Color.green;
}
LastEventPointY = f;
}
return pixels;
}
public void ReDraw(float value)
{
int width = (int)CurveCanvas.rectTransform.sizeDelta.x / 5;
int height = (int)CurveCanvas.rectTransform.sizeDelta.y / 5;
if (Linerender == null) return;
// 获取颜色数组(可在多线程环境中调用)
int width = Mathf.Max(2, (int)Linerender.rectTransform.sizeDelta.x / 5);
int height = Mathf.Max(1, (int)Linerender.rectTransform.sizeDelta.y / 5);
// 在主线程中创建和设置纹理Unity对象操作必须在主线程
StartCoroutine(GenerateTextureCoroutine(width, height, value));
// 使用 UILineRenderer 直接绘制曲线(替代原来的纹理生成
Linerender.RelativeSize = true;
Linerender.color = Color.green;
Vector2[] points = new Vector2[width];
for (int i = 0; i < width; i++)
{
float t = (float)i / (width - 1);
float curveVal = animatedFloat.startValue * value + ((animatedFloat.endValue - animatedFloat.startValue)
* AnimationCurveEvaluator.Evaluate(animatedFloat.animationCurveType, t) * value);
float yNorm = 0.5f + curveVal / height;
points[i] = new Vector2(t, yNorm);
}
Linerender.Points = points;
// 其余的非纹理相关代码保持不变
if (NextEventPoint != null)

View File

@@ -3,10 +3,11 @@ using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.UI.Extensions;
namespace Ichni.Editor
{
[RequireComponent(typeof(RawImage))]
[RequireComponent(typeof(RectTransform))]
public class KeyframeVisualizer : MonoBehaviour
{
[Header("Settings")]
@@ -15,123 +16,123 @@ namespace Ichni.Editor
public Color gridColor = new Color(0.3f, 0.3f, 0.3f, 0.5f);
public float pointSize = 15f;
public float tangentHandleLength = 40f;
public float lineThickness = 2f;
[Header("References")]
public AnimationCurve curve;
public RawImage rawImage;
public UILineRenderer curveLine;
public UILineRenderer gridLines;
public UILineRenderer borderLine;
// 当用户松开鼠标编辑结束时触发用于同步外部UI
public Action OnEditFinished;
private Texture2D _texture;
private Color32[] _buffer;
private List<CurvePoint> _activePoints = new List<CurvePoint>();
// 缓存特定颜色以提高性能
private Color32 _cClear = new Color32(0, 0, 0, 0);
private Color32 _cCurve;
private Color32 _cGrid;
private void Awake()
{
if (rawImage == null) rawImage = GetComponent<RawImage>();
_cCurve = curveColor;
_cGrid = gridColor;
// 移除场景/预制体中残留的旧 RawImage 组件
var oldRawImage = GetComponent<RawImage>();
if (oldRawImage != null)
Destroy(oldRawImage);
SetupUILineRenderers();
}
private void SetupUILineRenderers()
{
if (curveLine == null)
curveLine = CreateChildLineRenderer("CurveLine", curveColor);
if (gridLines == null)
gridLines = CreateChildLineRenderer("GridLines", gridColor);
if (borderLine == null)
borderLine = CreateChildLineRenderer("BorderLine", Color.white);
curveLine.LineThickness = lineThickness;
gridLines.LineThickness = 1f;
borderLine.LineThickness = 1.5f;
}
private UILineRenderer CreateChildLineRenderer(string name, Color color)
{
GameObject go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(transform, false);
RectTransform rt = go.GetComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
UILineRenderer line = go.AddComponent<UILineRenderer>();
line.color = color;
line.RelativeSize = true;
line.raycastTarget = false;
return line;
}
private void OnEnable()
{
if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1);
InitTexture();
RebuildInteractablePoints();
DrawCurveToRawImage();
DrawCurve();
}
// === 核心绘制逻辑 (高性能) ===
public void DrawCurveToRawImage()
{
if (curve == null || _texture == null) return;
// 1. 清屏
int len = _buffer.Length;
for (int i = 0; i < len; i++) _buffer[i] = _cClear;
int w = resolution.x;
int h = resolution.y;
// 2. 绘制网格 (0.25, 0.5, 0.75)
DrawGridLine(w, h, 0.25f);
DrawGridLine(w, h, 0.5f);
DrawGridLine(w, h, 0.75f);
// 绘制边框
DrawRect(0, 0, w - 1, h - 1, Color.white);
// 3. 绘制曲线
// 限制范围在 0-1
int prevY = -1;
for (int x = 0; x < w; x++)
{
float t = (float)x / (w - 1);
float val = curve.Evaluate(t);
// 映射到像素高度并Clap防止数组越界
int y = Mathf.FloorToInt(Mathf.Clamp01(val) * (h - 1));
// 绘制点
int idx = y * w + x;
if (idx >= 0 && idx < len) _buffer[idx] = _cCurve;
// 垂直补间(防止曲线断裂)
if (prevY != -1 && Mathf.Abs(y - prevY) > 1)
{
int step = y > prevY ? 1 : -1;
for (int k = prevY + step; k != y; k += step)
{
int fillIdx = k * w + x;
if (fillIdx >= 0 && fillIdx < len) _buffer[fillIdx] = _cCurve;
}
}
prevY = y;
}
_texture.SetPixels32(_buffer);
_texture.Apply();
rawImage.texture = _texture;
}
private void InitTexture()
{
if (_texture == null || _texture.width != resolution.x || _texture.height != resolution.y)
{
_texture = new Texture2D(resolution.x, resolution.y, TextureFormat.ARGB32, false);
_texture.filterMode = FilterMode.Bilinear;
_buffer = new Color32[resolution.x * resolution.y];
}
}
private void DrawGridLine(int w, int h, float percent)
{
int x = (int)(w * percent);
int y = (int)(h * percent);
for (int i = 0; i < h; i++) _buffer[i * w + x] = _cGrid; // 竖线
for (int i = 0; i < w; i++) _buffer[y * w + i] = _cGrid; // 横线
}
private void DrawRect(int x1, int y1, int x2, int y2, Color32 c)
{
int w = resolution.x;
for (int x = x1; x <= x2; x++) { _buffer[y1 * w + x] = c; _buffer[y2 * w + x] = c; }
for (int y = y1; y <= y2; y++) { _buffer[y * w + x1] = c; _buffer[y * w + x2] = c; }
}
// === 交互点生成 ===
public void RebuildInteractablePoints() // 原名 CreateKeyframeImages
// === 核心绘制逻辑 (UILineRenderer) ===
public void DrawCurve()
{
if (curve == null) return;
// 清理旧点
foreach (Transform child in transform) Destroy(child.gameObject);
int sampleCount = Mathf.Max(2, resolution.x);
// 1. 绘制曲线
Vector2[] curvePoints = new Vector2[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
float t = (float)i / (sampleCount - 1);
float val = Mathf.Clamp01(curve.Evaluate(t));
curvePoints[i] = new Vector2(t, val);
}
curveLine.Points = curvePoints;
// 2. 绘制网格 (0.25, 0.5, 0.75)
float[] gridPositions = { 0.25f, 0.5f, 0.75f };
List<Vector2[]> segments = new List<Vector2[]>();
foreach (float p in gridPositions)
{
segments.Add(new Vector2[] { new Vector2(0, p), new Vector2(1, p) }); // 横线
segments.Add(new Vector2[] { new Vector2(p, 0), new Vector2(p, 1) }); // 竖线
}
gridLines.Segments = segments;
// 3. 绘制边框
borderLine.Points = new Vector2[]
{
new Vector2(0, 0),
new Vector2(1, 0),
new Vector2(1, 1),
new Vector2(0, 1),
new Vector2(0, 0)
};
}
// === 交互点生成 ===
public void RebuildInteractablePoints()
{
if (curve == null) return;
// 清理旧点保留子UILineRenderer
var toDestroy = new List<GameObject>();
foreach (Transform child in transform)
{
if (child == curveLine?.transform ||
child == gridLines?.transform ||
child == borderLine?.transform)
continue;
toDestroy.Add(child.gameObject);
}
foreach (var go in toDestroy)
Destroy(go);
_activePoints.Clear();
for (int i = 0; i < curve.length; i++)
@@ -180,8 +181,7 @@ namespace Ichni.Editor
// === 坐标同步逻辑 ===
public void RefreshPointsPosition()
{
Vector2 size = rawImage.rectTransform.rect.size;
float canvasAspect = size.x / size.y;
Vector2 size = ((RectTransform)transform).rect.size;
foreach (var point in _activePoints)
{
@@ -207,7 +207,7 @@ namespace Ichni.Editor
// 斜率 = (y / x) * aspect => y = (tangent / aspect) * x
// 令视觉上的 x 为 1 或 -1
float xDir = (point.type == PointType.InTangent) ? -1f : 1f;
float yDir = (tangent / canvasAspect) * xDir;
float yDir = (tangent / (size.x / size.y)) * xDir;
visualDir = new Vector2(xDir, yDir).normalized;
}
@@ -218,10 +218,9 @@ namespace Ichni.Editor
}
// 处理点被拖拽
// 在 KeyframeVisualizer 类中修改 OnPointDragged 方法
public void OnPointDragged(CurvePoint point, Vector2 screenDelta)
{
Vector2 size = rawImage.rectTransform.rect.size;
Vector2 size = ((RectTransform)transform).rect.size;
int index = point.keyIndex;
Keyframe key = curve.keys[index];
@@ -255,21 +254,15 @@ namespace Ichni.Editor
}
// 5. 计算斜率 (Tangent)
// 物理斜率 = (DeltaValue / DeltaTime)
// 对应 UI = (dirVec.y / size.y) / (dirVec.x / size.x)
float canvasAspect = size.x / size.y;
float tangent = (dirVec.y / dirVec.x) * canvasAspect;
if (point.type == PointType.InTangent) key.inTangent = tangent;
else key.outTangent = tangent;
// 6. 更新手柄位置:让手柄视觉上严格对齐鼠标方向,但保持固定长度(可选)
// 如果你不希望手柄被拉长,可以将 dirVec 归一化再乘上固定长度
// point.rectTransform.anchoredPosition = keyPos + dirVec.normalized * tangentHandleLength;
}
curve.MoveKey(index, key);
DrawCurveToRawImage();
DrawCurve();
RefreshPointsPosition(); // 统一刷新位置,确保手柄视觉表现一致
}
}
@@ -309,4 +302,4 @@ namespace Ichni.Editor
visualizer.OnEditFinished?.Invoke();
}
}
}
}

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 903817bad09ed754ca52d7ea5ba15e97
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,37 +0,0 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Ichni.NodeScript
{
public class ConnectorSlot : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
public IInput connectorIn;
public IOutput connectorOut;
public bool isInput;
public NodeObject ownerNode;
public RectTransform connectorRect;
void Awake() { connectorRect = GetComponent<RectTransform>(); }
public void OnPointerDown(PointerEventData e) => NodeManager.Instance.StartWireDrag(this, e);
public void OnDrag(PointerEventData e) => NodeManager.Instance.UpdateWireDrag(e);
public void OnPointerUp(PointerEventData e) => NodeManager.Instance.EndWireDrag(e);
/// <summary>InputAny / OutputAny 类型锁定后刷新连接点颜色</summary>
public void RefreshAppearance()
{
var img = GetComponentInChildren<Image>();
if (img == null) return;
if (isInput && connectorIn != null)
{
img.color = connectorIn.ConnectorColor;
}
else if (!isInput && connectorOut != null)
{
img.color = connectorOut.ConnectorColor;
}
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4d5e4efd89e8c6041bdcdeae97e94191

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 36b55b766114d5d4bb9cfb9f673bbbae
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,280 +0,0 @@
using System;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.NodeScript
{
// ==================== 图入口 ====================
public class NodeStart : NodeBase
{
public Output<Signal> exec = new("Exec");
public Output<GameElement> element = new("Element");
[NonSerialized] public GameElement boundElement;
public override LoopResult Loop()
{
exec.SetValue(Signal.Default);
element.SetValue(boundElement);
return LoopResult.Complete();
}
}
public class NodeEntry : NodeBase
{
public Output<Signal> exec = new("Exec");
public override LoopResult Loop()
{
exec.SetValue(Signal.Default);
return LoopResult.Complete();
}
}
// ==================== 统一多类型数学运算 ====================
public class NodeMath : NodeBase
{
enum Op { Add, Subtract, Multiply, Divide }
Op _op;
public InputAny a = new("A");
public InputAny b = new("B") { IsFixedType = true };
public OutputAny result = new("Result");
static readonly Dictionary<(Type, Op), Func<object, object, object>> _ops = new()
{
[(typeof(float), Op.Add)] = (x, y) => (float)x + (float)y,
[(typeof(float), Op.Subtract)] = (x, y) => (float)x - (float)y,
[(typeof(float), Op.Multiply)] = (x, y) => (float)x * (float)y,
[(typeof(float), Op.Divide)] = (x, y) => (float)y != 0f ? (float)x / (float)y : 0f,
[(typeof(int), Op.Add)] = (x, y) => (int)x + (int)y,
[(typeof(int), Op.Subtract)] = (x, y) => (int)x - (int)y,
[(typeof(int), Op.Multiply)] = (x, y) => (int)x * (int)y,
[(typeof(int), Op.Divide)] = (x, y) => (int)y != 0 ? (int)x / (int)y : 0,
[(typeof(Vector2), Op.Add)] = (x, y) => (Vector2)x + (Vector2)y,
[(typeof(Vector2), Op.Subtract)] = (x, y) => (Vector2)x - (Vector2)y,
[(typeof(Vector2), Op.Multiply)] = (x, y) => (Vector2)x * (float)y,
[(typeof(Vector2), Op.Divide)] = (x, y) => (float)y != 0f ? (Vector2)x / (float)y : Vector2.zero,
[(typeof(Vector3), Op.Add)] = (x, y) => (Vector3)x + (Vector3)y,
[(typeof(Vector3), Op.Subtract)] = (x, y) => (Vector3)x - (Vector3)y,
[(typeof(Vector3), Op.Multiply)] = (x, y) => (Vector3)x * (float)y,
[(typeof(Vector3), Op.Divide)] = (x, y) => (float)y != 0f ? (Vector3)x / (float)y : Vector3.zero,
[(typeof(Color), Op.Add)] = (x, y) => (Color)x + (Color)y,
[(typeof(Color), Op.Subtract)] = (x, y) => (Color)x - (Color)y,
[(typeof(Color), Op.Multiply)] = (x, y) => (Color)x * (float)y,
[(typeof(string), Op.Add)] = (x, y) => (string)x + (string)y,
};
public override void BuildUI(NodeUIBuilder ui)
{
ui.Dropdown("Op", new[] { "+", "-", "*", "/" }, 0, i => _op = (Op)i);
}
public override LoopResult Loop()
{
if (!a.IsConnected && !b.IsConnected) return LoopResult.Complete();
if (!EnsureInputsReady(out var hang)) return hang;
var type = a.DataType ?? b.DataType ?? typeof(float);
// 单输入 → 直通
if (!a.IsConnected) { result.SetValueRaw(SafeGet(b)); return LoopResult.Complete(); }
if (!b.IsConnected) { result.SetValueRaw(SafeGet(a)); return LoopResult.Complete(); }
// 双输入 → LUT 运算
var key = (type, _op);
if (_ops.TryGetValue(key, out var func))
result.SetValueRaw(func(SafeGet(a), SafeGet(b)));
else
result.SetValueRaw(SafeGet(a)); // fallback: 直通 a
return LoopResult.Complete();
}
static object SafeGet(InputAny input)
{
try { return input.GetValue(); }
catch { return null; }
}
}
// ==================== 分支 ====================
public class NodeBranch : NodeBase
{
public Input<Signal> exec = new("Exec");
public InputAny condition = new("Condition");
public Output<Signal> trueOut = new("True");
public Output<Signal> falseOut = new("False");
public override LoopResult Loop()
{
if (!exec.IsConnected) return LoopResult.Complete();
if (!EnsureInputsReady(out var hang)) return hang;
float cond = condition.IsConnected ? condition.GetValue<float>() : 0f;
// int → float 兼容
if (condition.DataType == typeof(int))
cond = condition.GetValue<int>();
if (cond > 0f) trueOut.SetValue(Signal.Default);
else falseOut.SetValue(Signal.Default);
return LoopResult.Complete();
}
}
// ==================== For 循环多周期状态机Rect 容器留待 Phase 4 ====================
public class NodeForLoop : NodeBase
{
public Input<Signal> exec = new("Exec");
public InputAny count = new("Count");
public Output<Signal> loopBody = new("LoopBody");
public Output<int> index = new("Index");
public Output<Signal> completed = new("Completed");
int _current;
int _total;
bool _started;
bool _waiting;
public override LoopResult Loop()
{
if (!exec.IsConnected) return LoopResult.Complete();
if (!_started && !EnsureInputsReady(out var hang)) return hang;
if (!_started)
{
_total = count.IsConnected ? count.GetValue<int>() : 0;
_current = 0;
_started = true;
}
// 空转一周期让下游消费上一轮输出
if (_waiting) { _waiting = false; return LoopResult.Wait(); }
if (_current < _total)
{
index.SetValue(_current);
loopBody.SetValue(Signal.Default);
_current++;
_waiting = true;
return LoopResult.Repeat();
}
_started = false;
completed.SetValue(Signal.Default);
return LoopResult.Complete();
}
}
// ==================== GameElement 操作 ====================
public class NodeGameElement : NodeBase
{
public Input<Signal> exec = new("Exec");
public Input<GameElement> RootNode = new("Root");
public Input<GameElement> SourceNode = new("Source");
public Output<GameElement> newElement = new("OutPut");
public Output<Signal> completed = new("Done");
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var root = RootNode.Value;
var source = SourceNode.Value;
if (source == null)
{
ElementFolder.GenerateElement("Folder(Cp)", Guid.NewGuid(), new List<string>(), true, RootNode.IsConnected ? root : null);
}
else if (root != null)
{
EditorManager.instance.operationManager.CopyPasteDeleteModule.CopyElement(source);
EditorManager.instance.operationManager.CopyPasteDeleteModule.PasteElement(root);
}
completed.SetValue(Signal.Default);
return LoopResult.Complete();
}
}
public class NodeGetTransform : NodeBase
{
public Input<GameElement> element = new("Element");
public Output<Vector3> position = new("Pos"), rotation = new("Rot"), scale = new("Scl");
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
if (element.Value is IHaveTransformSubmodule ts)
{
position.SetValue(ts.transformSubmodule.originalPosition);
rotation.SetValue(ts.transformSubmodule.originalEulerAngles);
scale.SetValue(ts.transformSubmodule.originalScale);
}
return LoopResult.Complete();
}
}
public class NodeSetTransform : NodeBase
{
public Input<Signal> exec = new("Exec");
public Input<GameElement> element = new("Element");
public Input<Vector3> Pos = new("Pos"), Rot = new("Rot"), Scale = new("Scl");
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
if (element.Value is IHaveTransformSubmodule ts)
{
if (Pos.IsConnected) { ts.transformSubmodule.originalPosition = Pos.Value; ts.transformSubmodule.positionDirtyMark = true; }
if (Rot.IsConnected) { ts.transformSubmodule.originalEulerAngles = Rot.Value; ts.transformSubmodule.eulerAnglesDirtyMark = true; }
if (Scale.IsConnected) { ts.transformSubmodule.originalScale = Scale.Value; ts.transformSubmodule.scaleDirtyMark = true; }
}
return LoopResult.Complete();
}
}
public class NodeChildByIndex : NodeBase
{
public Input<GameElement> parent = new("Parent");
public int index;
public Output<GameElement> child = new("Child");
public override void BuildUI(NodeUIBuilder ui) { ui.FloatField("Idx", 0, i => index = (int)i); }
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var p = parent.Value;
if (p != null && index >= 0 && index < p.childElementList.Count)
child.SetValue(p.childElementList[index]);
return LoopResult.Complete();
}
}
public class NodeChildCount : NodeBase
{
public Input<GameElement> parent = new("Parent");
public Output<int> count = new("Count");
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var p = parent.Value;
count.SetValue(p != null ? p.childElementList.Count : 0);
return LoopResult.Complete();
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: da71a524cc8687941840c85db8590b1c

View File

@@ -1,445 +0,0 @@
using System;
using System.Collections.Generic;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.NodeScript
{
// ==================== 通用常量 ====================
public class NodeConst : NodeBase
{
enum ValueType { Float, Int, Bool, Vector2, Vector3, Color }
ValueType _type;
public float floatVal;
public int intVal;
public bool boolVal;
public Vector2 vector2Val;
public Vector3 vector3Val;
public Color colorVal;
public OutputAny value = new("Value");
public override void InitConnectors()
{
base.InitConnectors();
value.LockType(typeof(float));
}
public override void BuildUI(NodeUIBuilder ui)
{
ui.TypeDropdown(new[] { "float", "int", "bool", "Vector2", "Vector3", "Color" }, 0, i =>
{
_type = (ValueType)i;
Type t = _type switch
{
ValueType.Float => typeof(float),
ValueType.Int => typeof(int),
ValueType.Bool => typeof(bool),
ValueType.Vector2 => typeof(Vector2),
ValueType.Vector3 => typeof(Vector3),
ValueType.Color => typeof(Color),
_ => typeof(float),
};
value.LockType(t);
});
switch (_type)
{
case ValueType.Float: ui.FloatField("F", 0f, v => floatVal = v); break;
case ValueType.Int: ui.FloatField("I", 0f, v => intVal = (int)v); break;
case ValueType.Bool: ui.Toggle("B", false, v => boolVal = v); break;
case ValueType.Vector2: ui.FloatField("X", 0f, v => vector2Val.x = v); ui.FloatField("Y", 0f, v => vector2Val.y = v); break;
case ValueType.Vector3: ui.FloatField("X", 0f, v => vector3Val.x = v); ui.FloatField("Y", 0f, v => vector3Val.y = v); ui.FloatField("Z", 0f, v => vector3Val.z = v); break;
case ValueType.Color: ui.FloatField("R", 1f, v => colorVal.r = v); ui.FloatField("G", 1f, v => colorVal.g = v); ui.FloatField("B", 1f, v => colorVal.b = v); ui.FloatField("A", 1f, v => colorVal.a = v); break;
}
}
public override LoopResult Loop()
{
object v = _type switch
{
ValueType.Float => floatVal,
ValueType.Int => intVal,
ValueType.Bool => boolVal,
ValueType.Vector2 => vector2Val,
ValueType.Vector3 => vector3Val,
ValueType.Color => colorVal,
_ => 0f,
};
value.SetValueRaw(v);
return LoopResult.Complete();
}
}
// ==================== 通用拆分 ====================
public class NodeSplit : NodeBase
{
public InputAny input = new("Input");
public Output<float> x = new("X"), y = new("Y"), z = new("Z"), w = new("W");
public override LoopResult Loop()
{
if (!input.IsConnected) return LoopResult.Complete();
if (!EnsureInputsReady(out var hang)) return hang;
var t = input.DataType;
if (t == typeof(Vector2))
{
var v = input.GetValue<Vector2>();
x.SetValue(v.x); y.SetValue(v.y);
}
else if (t == typeof(Vector3))
{
var v = input.GetValue<Vector3>();
x.SetValue(v.x); y.SetValue(v.y); z.SetValue(v.z);
}
else if (t == typeof(Color))
{
var v = input.GetValue<Color>();
x.SetValue(v.r); y.SetValue(v.g); z.SetValue(v.b); w.SetValue(v.a);
}
return LoopResult.Complete();
}
}
// ==================== 通用合并 ====================
public class NodeCombine : NodeBase
{
enum CombineType { Vector2, Vector3, Color }
CombineType _type;
public Input<float> x = new("X"), y = new("Y"), z = new("Z"), w = new("W");
public OutputAny output = new("Result");
public override void InitConnectors()
{
base.InitConnectors();
output.LockType(typeof(Vector2));
}
public override void BuildUI(NodeUIBuilder ui)
{
ui.TypeDropdown(new[] { "Vector2", "Vector3", "Color" }, 0, i =>
{
_type = (CombineType)i;
Type t = _type == CombineType.Vector2 ? typeof(Vector2) : _type == CombineType.Vector3 ? typeof(Vector3) : typeof(Color);
output.LockType(t);
});
}
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
object v = _type switch
{
CombineType.Vector2 => new Vector2(x.Value, y.Value),
CombineType.Vector3 => new Vector3(x.Value, y.Value, z.Value),
CombineType.Color => new Color(x.Value, y.Value, z.Value, w.Value),
_ => null,
};
output.SetValueRaw(v);
return LoopResult.Complete();
}
}
// ==================== Lerp 线性插值 ====================
public class NodeLerp : NodeBase
{
public InputAny a = new("A");
public InputAny b = new("B");
public InputAny t = new("T") { IsFixedType = true };
public OutputAny result = new("Result");
public override void InitConnectors()
{
base.InitConnectors();
t.LockType(typeof(float));
}
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var type = a.DataType ?? b.DataType ?? typeof(float);
float factor = t.IsConnected ? Mathf.Clamp01(t.GetValue<float>()) : 0.5f;
object v = null;
if (type == typeof(float))
v = Mathf.Lerp(a.GetValue<float>(), b.GetValue<float>(), factor);
else if (type == typeof(int))
v = Mathf.RoundToInt(Mathf.Lerp(a.GetValue<int>(), b.GetValue<int>(), factor));
else if (type == typeof(Vector2))
v = Vector2.Lerp(a.GetValue<Vector2>(), b.GetValue<Vector2>(), factor);
else if (type == typeof(Vector3))
v = Vector3.Lerp(a.GetValue<Vector3>(), b.GetValue<Vector3>(), factor);
else if (type == typeof(Color))
v = Color.Lerp(a.GetValue<Color>(), b.GetValue<Color>(), factor);
result.SetValueRaw(v);
return LoopResult.Complete();
}
}
// ==================== 比较运算 ====================
public class NodeCompare : NodeBase
{
enum CmpOp { Equal, NotEqual, Greater, Less, GreaterOrEqual, LessOrEqual }
CmpOp _op;
public InputAny a = new("A");
public InputAny b = new("B");
public Output<bool> result = new("Result");
public override void BuildUI(NodeUIBuilder ui)
{
ui.Dropdown("Op", new[] { "==", "!=", ">", "<", ">=", "<=" }, 0, i => _op = (CmpOp)i);
}
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var type = a.DataType ?? b.DataType ?? typeof(float);
bool r = false;
if (type == typeof(float)) r = Compare(a.GetValue<float>(), b.GetValue<float>());
else if (type == typeof(int)) r = Compare(a.GetValue<int>(), b.GetValue<int>());
else if (type == typeof(string)) r = Compare(a.GetValue<string>(), b.GetValue<string>());
result.SetValue(r);
return LoopResult.Complete();
}
bool Compare<T>(T x, T y) where T : IComparable<T>
{
int c = x.CompareTo(y);
return _op switch
{
CmpOp.Equal => c == 0,
CmpOp.NotEqual => c != 0,
CmpOp.Greater => c > 0,
CmpOp.Less => c < 0,
CmpOp.GreaterOrEqual => c >= 0,
CmpOp.LessOrEqual => c <= 0,
_ => false,
};
}
}
// ==================== 二选一 ====================
public class NodeSelect : NodeBase
{
public InputAny condition = new("Cond");
public InputAny trueValue = new("True");
public InputAny falseValue = new("False");
public OutputAny result = new("Result");
bool _manualCond;
public override void BuildUI(NodeUIBuilder ui)
{
ui.Toggle("Cond", false, v => _manualCond = v);
}
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
bool cond = condition.IsConnected ? condition.GetValue<bool>() : _manualCond;
// int → bool 兼容
if (!cond && condition.IsConnected && condition.DataType == typeof(int))
cond = condition.GetValue<int>() != 0;
var src = cond ? trueValue : falseValue;
result.SetValueRaw(src.GetValue());
return LoopResult.Complete();
}
}
// ==================== 万能赋值(反向写入) ====================
public class NodeSet : NodeBase
{
public InputAny targetRef = new("TargetRef");
public InputAny value = new("Value");
public override LoopResult Loop()
{
if (!targetRef.IsConnected) return LoopResult.Complete();
if (!EnsureInputsReady(out var hang)) return hang;
targetRef.WriteBack(value.GetValue());
return LoopResult.Complete();
}
}
// ==================== Variable<T> ====================
public class Variable<T> { public T Value; }
public class NodeVariable<T> : NodeBase
{
public Variable<T> var = new();
public Input<Signal> signal = new("Signal");
public Input<T> set = new("Set");
public Output<T> get = new("Value");
public override void InitConnectors()
{
base.InitConnectors();
(get as Output<T>).SetWriteBack(v => var.Value = v);
}
public override LoopResult Loop()
{
// Signal 有连接时必须等待(循环体内等待触发)
if (signal.IsConnected)
{
if (!EnsureInputsReady(out var hang)) return hang;
}
if (set.IsConnected)
var.Value = set.Value;
get.SetValue(var.Value);
return LoopResult.Complete();
}
}
// ==================== 调试 ====================
public class NodeDebugLog : NodeBase
{
public Input<Signal> exec = new("Exec");
public InputAny value = new("Value");
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
Debug.Log($"[DebugLog] {NodeName}: {value.GetValue()}");
return LoopResult.Complete();
}
}
public class NodeLog : NodeBase
{
public InputAny value = new("Value");
public string prefix = "";
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
Debug.Log($"[NodeLog] {prefix}{value.GetValue()}");
return LoopResult.Complete();
}
}
// ==================== List<T> ====================
public class NodeList<T> : NodeBase
{
public Variable<List<T>> list = new() { Value = new List<T>() };
public Output<List<T>> output = new("List");
public override LoopResult Loop()
{
output.SetValue(list.Value);
return LoopResult.Complete();
}
}
public class NodeListAdd<T> : NodeBase
{
public Input<List<T>> list = new("List");
public Input<T> item = new("Item");
public Output<List<T>> output = new("List");
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var l = list.Value;
if (l != null && item.IsConnected) { l.Add(item.Value); output.SetValue(l); }
return LoopResult.Complete();
}
}
public class NodeListGet<T> : NodeBase
{
public Input<List<T>> list = new("List");
public int index;
public Output<T> element = new("Element");
public override void BuildUI(NodeUIBuilder ui) { ui.FloatField("Idx", 0, i => index = (int)i); }
public override LoopResult Loop()
{
if (!EnsureInputsReady(out var hang)) return hang;
var l = list.Value;
if (l != null && index >= 0 && index < l.Count) element.SetValue(l[index]);
return LoopResult.Complete();
}
}
public class NodeForEach : NodeBase
{
public Input<Signal> exec = new("Exec");
public InputAny list = new("List");
public Output<Signal> loopBody = new("LoopBody"), completed = new("Completed");
public OutputAny current = new("Current");
public OutputAny indexOut = new("Index");
System.Collections.IList _list;
int _i;
bool _started;
bool _waiting;
public override void InitConnectors()
{
base.InitConnectors();
indexOut.LockType(typeof(int));
}
public override void OnTypePropagated(Type t)
{
base.OnTypePropagated(t);
if (t != null && t.IsGenericType && t.GetGenericTypeDefinition() == typeof(List<>))
current.LockType(t.GetGenericArguments()[0]);
}
public override LoopResult Loop()
{
if (!exec.IsConnected) return LoopResult.Complete();
if (!_started && !EnsureInputsReady(out var hang)) return hang;
if (!_started)
{
_list = list.GetValue() as System.Collections.IList;
if (_list == null || _list.Count == 0)
{
completed.SetValue(Signal.Default);
return LoopResult.Complete();
}
_i = 0;
_started = true;
}
if (_waiting) { _waiting = false; return LoopResult.Wait(); }
if (_i < _list.Count)
{
indexOut.SetValueRaw(_i);
current.SetValueRaw(_list[_i]);
loopBody.SetValue(Signal.Default);
_i++;
_waiting = true;
return LoopResult.Repeat();
}
_started = false;
completed.SetValue(Signal.Default);
return LoopResult.Complete();
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d6d155dc132fc24448aa1896c024f9a6

View File

@@ -1,404 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Ichni.RhythmGame;
using UnityEngine;
namespace Ichni.NodeScript
{
// ============================================================
// NodeStatus — 生命周期状态
// ============================================================
public enum NodeStatus { Ready, Hang, Complete }
// ============================================================
// LoopResult — Loop() 返回值
// ============================================================
public struct LoopResult
{
public List<NodeBase> Triggers;
public bool RemoveFromRuntime;
public bool TriggerDownstream;
/// <summary>完成,退出 runtime 并触发下游</summary>
public static LoopResult Complete() => new() { RemoveFromRuntime = true, TriggerDownstream = true };
/// <summary>挂起,等待前置节点,不触发下游</summary>
public static LoopResult Hang(NodeBase trigger) => new() { Triggers = new List<NodeBase> { trigger }, RemoveFromRuntime = false };
/// <summary>挂起,等待多个前置节点</summary>
public static LoopResult Hang(List<NodeBase> triggers) => new() { Triggers = triggers, RemoveFromRuntime = false };
/// <summary>保持活跃,触发下游但不退出 runtime循环节点逐周期用</summary>
public static LoopResult Repeat() => new() { RemoveFromRuntime = false, TriggerDownstream = true };
/// <summary>空转一周期,不触发下游(循环节点等下游消费用)</summary>
public static LoopResult Wait() => new() { RemoveFromRuntime = false };
}
// ============================================================
// 连接器接口(简化,移除 HasReceived
// ============================================================
public interface IInput
{
string Name { get; set; }
Type DataType { get; }
Color ConnectorColor { get; }
bool IsConnected { get; }
}
public interface IOutput
{
string Name { get; set; }
Type DataType { get; }
Color ConnectorColor { get; }
bool IsConnected { get; }
}
// ============================================================
// NodeBase — 纯数据/逻辑
// ============================================================
public abstract class NodeBase
{
public string NodeName;
public NodeObject nodeObject;
public NodeStatus Status { get; set; } = NodeStatus.Ready;
public int L { get; set; } = -1;
public abstract LoopResult Loop();
public virtual void BuildUI(NodeUIBuilder ui) { }
/// <summary>当某个 InputAny 连线锁定类型后,传播到同节点其他未锁定端口</summary>
public virtual void OnTypePropagated(Type lockedType)
{
foreach (var any in GetAnyInputs())
{
if (any.IsFixedType) continue;
if (any.DataType == null)
any.LockType(lockedType);
}
foreach (var any in GetAnyOutputs())
{
if (any.DataType == null)
any.LockType(lockedType);
}
}
public List<InputAny> GetAnyInputs()
{
return GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(f => typeof(InputAny).IsAssignableFrom(f.FieldType))
.Select(f => f.GetValue(this) as InputAny)
.Where(a => a != null)
.ToList();
}
/// <summary>检查所有已连输入的上游是否都 Complete否则自动返回 Hang</summary>
protected bool EnsureInputsReady(out LoopResult hang)
{
var preceding = GetPrecedingNodes();
var pending = preceding.Where(p => p.Status != NodeStatus.Complete).ToList();
if (pending.Count > 0) { hang = LoopResult.Hang(pending); return false; }
hang = default; return true;
}
public List<OutputAny> GetAnyOutputs()
{
return GetType().GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(f => typeof(OutputAny).IsAssignableFrom(f.FieldType))
.Select(f => f.GetValue(this) as OutputAny)
.Where(a => a != null)
.ToList();
}
/// <summary>收集所有前置依赖节点(输入连线的上游)</summary>
public List<NodeBase> GetPrecedingNodes()
{
var result = new List<NodeBase>();
foreach (var any in GetAnyInputs())
{
var src = any.GetSourceNode();
if (src != null) result.Add(src);
}
foreach (var f in GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (!f.FieldType.IsGenericType) continue;
var def = f.FieldType.GetGenericTypeDefinition();
if (def != typeof(Input<>)) continue;
var input = f.GetValue(this);
var srcMethod = f.FieldType.GetMethod("GetSourceNode");
var node = srcMethod?.Invoke(input, null) as NodeBase;
if (node != null) result.Add(node);
}
return result;
}
// 反射
public List<(string name, IInput connector, Type type)> GetInputs()
{
var results = new List<(string, IInput, Type)>();
foreach (var f in GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (!typeof(IInput).IsAssignableFrom(f.FieldType)) continue;
var val = f.GetValue(this) as IInput;
if (val == null) continue;
results.Add((f.Name, val, val.DataType ?? typeof(object)));
}
return results;
}
public List<(string name, IOutput connector, Type type)> GetOutputs()
{
var results = new List<(string, IOutput, Type)>();
foreach (var f in GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (!typeof(IOutput).IsAssignableFrom(f.FieldType)) continue;
var val = f.GetValue(this) as IOutput;
if (val == null) continue;
results.Add((f.Name, val, val.DataType ?? typeof(object)));
}
return results;
}
public virtual void InitConnectors()
{
foreach (var f in GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (!typeof(IInput).IsAssignableFrom(f.FieldType) &&
!typeof(IOutput).IsAssignableFrom(f.FieldType)) continue;
var c = f.GetValue(this);
if (c == null) { c = Activator.CreateInstance(f.FieldType); f.SetValue(this, c); }
var nbField = f.FieldType.GetField("_nodeBase", BindingFlags.NonPublic | BindingFlags.Instance);
nbField?.SetValue(c, this);
}
}
}
// ============================================================
// Input<T> — 拉扯式输入,从上游 Output<T> 直接读取
// ============================================================
public class Input<T> : IInput
{
internal NodeBase _nodeBase;
Output<T> _source;
IOutput _anySource; // OutputAny 等非泛型源
public string Name { get; set; }
public Type DataType => typeof(T);
public bool IsConnected => _source != null || _anySource != null;
public Color ConnectorColor => NodeColors.Get(typeof(T));
public T Value
{
get
{
if (_source != null) return _source._value;
if (_anySource is OutputAny oa) return oa.GetValue<T>();
return default;
}
}
public Input() { Name = typeof(T).Name; }
public Input(string name) { Name = name ?? typeof(T).Name; }
public T Pull() => Value;
public void Connect(Output<T> src) { _source = src; _anySource = null; }
public void Disconnect(Output<T> src) { if (_source == src) _source = null; }
public void ConnectAny(IOutput src) { _anySource = src; _source = null; }
public void DisconnectAny() { _anySource = null; }
public NodeBase GetSourceNode()
{
if (_source != null) return _source._nodeBase;
var f = _anySource?.GetType().GetField("_nodeBase", BindingFlags.NonPublic | BindingFlags.Instance);
return f?.GetValue(_anySource) as NodeBase;
}
}
// ============================================================
// Output<T> — 存值,不再推送;保留 _targets 供拓扑/BFS 用
// ============================================================
public class Output<T> : IOutput
{
internal NodeBase _nodeBase;
internal T _value;
internal readonly List<Input<T>> _targets = new();
// 反向写入回调NodeSet 写回 Variable 时触发
internal Action<T> _writeBack;
public string Name { get; set; }
public Type DataType => typeof(T);
public bool IsConnected => _targets.Count > 0;
public T Value => _value;
public Color ConnectorColor => NodeColors.Get(typeof(T));
public Output() { Name = typeof(T).Name; }
public Output(string name) { Name = name ?? typeof(T).Name; }
public void SetValue(T v)
{
_value = v;
_writeBack?.Invoke(v);
}
public void Connect(Input<T> i) { if (i != null && !_targets.Contains(i)) _targets.Add(i); }
public void Disconnect(Input<T> i) { if (i != null) _targets.Remove(i); }
internal void SetWriteBack(Action<T> cb) { _writeBack = cb; }
}
// ============================================================
// InputAny — 未锁定输入端口,连线后类型自动确定
// ============================================================
public class InputAny : IInput
{
internal NodeBase _nodeBase;
IOutput _source;
public string Name { get; set; }
public Type DataType { get; private set; }
public bool IsConnected => _source != null;
public bool IsFixedType { get; set; }
public Color ConnectorColor => DataType != null ? NodeColors.Get(DataType) : Color.grey;
public InputAny() { Name = "Any"; }
public InputAny(string name) { Name = name ?? "Any"; }
public T GetValue<T>()
{
if (_source is Output<T> ot) return ot.Value;
if (_source is OutputAny oa) return oa.GetValue<T>();
return default;
}
public object GetValue()
{
if (_source is OutputAny oa) return oa._value;
if (_source != null)
{
var p = _source.GetType().GetProperty("Value");
return p?.GetValue(_source);
}
return null;
}
public NodeBase GetSourceNode()
{
var f = _source?.GetType().GetField("_nodeBase", BindingFlags.NonPublic | BindingFlags.Instance);
return f?.GetValue(_source) as NodeBase;
}
// 反向写入NodeSet 通过 targetRef 写回上游 Variable
public void WriteBack(object value)
{
if (_source == null) return;
if (_source is OutputAny oa)
{
oa.SetValueRaw(value);
// 触发 writeback
if (oa._writeBack != null)
oa._writeBack.DynamicInvoke(value);
}
else
{
var setMethod = _source.GetType().GetMethod("SetValue");
if (setMethod != null)
{
setMethod.Invoke(_source, new[] { value });
}
}
}
public void ConnectAny(IOutput src)
{
_source = src;
if (src.DataType != null)
{
LockType(src.DataType);
_nodeBase?.OnTypePropagated(src.DataType);
}
}
public void DisconnectAny() { _source = null; }
public void LockType(Type t)
{
if (t != null && DataType != t) DataType = t;
}
}
// ============================================================
// OutputAny — 未锁定输出端口,类型跟随同节点 InputAny
// ============================================================
public class OutputAny : IOutput
{
internal NodeBase _nodeBase;
internal object _value;
internal readonly List<InputAny> _targets = new();
internal Action<object> _writeBack;
public string Name { get; set; }
public Type DataType { get; private set; }
public bool IsConnected => _targets.Count > 0;
public Color ConnectorColor => DataType != null ? NodeColors.Get(DataType) : Color.grey;
public OutputAny() { Name = "Out"; }
public OutputAny(string name) { Name = name ?? "Out"; }
public T GetValue<T>()
{
if (_value is T tv) return tv;
Debug.LogError($"[OutputAny:{Name}] Type mismatch: stored={_value?.GetType().Name}, requested={typeof(T).Name}");
return default;
}
public object GetValue() => _value;
public void SetValue<T>(T v) { _value = v; }
public void SetValueRaw(object v) { _value = v; }
public void ConnectAny(InputAny i) { if (i != null && !_targets.Contains(i)) _targets.Add(i); }
public void DisconnectAny(InputAny i) { if (i != null) _targets.Remove(i); }
public void LockType(Type t)
{
if (t != null && DataType != t) DataType = t;
}
}
// ============================================================
// 颜色表
// ============================================================
public static class NodeColors
{
static readonly Dictionary<Type, Color> _table = new()
{
{ typeof(float), Color.blue },
{ typeof(int), Color.cyan },
{ typeof(bool), new Color(1f, 0.5f, 0f) },
{ typeof(string), Color.magenta },
{ typeof(Vector2), Color.yellow },
{ typeof(Vector3), Color.green },
{ typeof(Color), new Color(0.8f, 0.4f, 0.6f) },
{ typeof(GameElement), Color.mediumPurple },
{ typeof(Signal), Color.ghostWhite },
{ typeof(List<GameElement>), new Color(0.6f, 0.3f, 0.8f) },
};
public static Color Get(Type t) => t != null && _table.TryGetValue(t, out var c) ? c : Color.grey;
}
// ============================================================
// Signal — 触发信号,不参与值传递
// ============================================================
public struct Signal
{
public static readonly Signal Default = new();
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 335508d8cda287449ad8009a1c9e61a8

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 6cdb8cc0b3fb21f49b1a761000205509

View File

@@ -1,135 +0,0 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Ichni.NodeScript
{
public class NodeObject : MonoBehaviour, IDragHandler, IPointerClickHandler
{
public TMP_Text TitleText;
public NodeBase nodeBase;
public Image statusImage;
public Transform inputsRoot;
public Transform outputsRoot;
public Transform middleRoot;
public GameObject connectorSlotPrefabI;
public GameObject connectorSlotPrefabO;
public GameObject dropdownPrefab;
public GameObject inputPrefab;
bool _selected;
public bool Selected
{
get => _selected;
set
{
_selected = value;
if (_bg == null) _bg = GetComponent<Image>();
if (_bg != null) _bg.color = value ? new Color(0.3f, 0.5f, 0.9f, 0.4f) : new Color(0.15f, 0.15f, 0.15f, 0.3f);
}
}
Image _bg;
public void Init()
{
if (nodeBase == null) return;
TitleText.text = nodeBase.NodeName;
nodeBase.nodeObject = this;
_bg = GetComponent<Image>();
if (_bg == null) { _bg = gameObject.AddComponent<Image>(); _bg.color = new Color(0.15f, 0.15f, 0.15f, 0.3f); }
nodeBase.InitConnectors();
BuildConnectors();
nodeBase.BuildUI(new NodeUIBuilder(middleRoot, dropdownPrefab, inputPrefab));
NodeManager.Instance?.ComputeLValues();
UpdateLDisplay();
}
public void UpdateLDisplay()
{
if (TitleText != null && nodeBase != null)
TitleText.text = $"{nodeBase.NodeName} (L:{nodeBase.L})";
}
public void UpdateStatusDisplay()
{
if (statusImage == null || nodeBase == null) return;
statusImage.color = nodeBase.Status switch
{
NodeStatus.Ready => new Color(0.3f, 0.3f, 0.3f, 0.8f),
NodeStatus.Hang => new Color(0.8f, 0.6f, 0.1f, 0.9f),
NodeStatus.Complete => new Color(0.2f, 0.8f, 0.3f, 0.9f),
_ => Color.white,
};
}
// ========== 选中 / 拖拽 ==========
public void OnPointerClick(PointerEventData e)
{
if (e.button != PointerEventData.InputButton.Left) return;
NodeManager.Instance.SelectNode(this, e);
}
public void OnDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left) return;
var rt = GetComponent<RectTransform>();
rt.anchoredPosition += eventData.delta / GetComponentInParent<Canvas>().scaleFactor;
NodeManager.Instance.RefreshAllLines();
}
// ========== 插槽构建 ==========
void BuildConnectors()
{
foreach (var (name, connector, type) in nodeBase.GetInputs())
CreateSlot(name, connector, true);
foreach (var (name, connector, type) in nodeBase.GetOutputs())
CreateSlot(name, connector, false);
}
void CreateSlot(string name, object connector, bool isInput)
{
IInput inp = connector as IInput;
IOutput outp = connector as IOutput;
if (inp == null && outp == null) return;
var conName = inp?.Name ?? outp.Name;
if (inp != null) inp.Name = name;
if (outp != null) outp.Name = name;
var prefab = isInput ? connectorSlotPrefabI : connectorSlotPrefabO;
var parent = isInput ? inputsRoot : outputsRoot;
if (prefab == null || parent == null) return;
var go = Instantiate(prefab, parent);
var img = go.GetComponentInChildren<Image>();
if (img == null)
{
var imgGo = new GameObject("Dot", typeof(Image));
imgGo.transform.SetParent(go.transform, false);
img = imgGo.GetComponent<Image>();
}
img.color = (inp as IInput)?.ConnectorColor ?? (outp as IOutput)?.ConnectorColor ?? Color.white;
img.raycastTarget = true;
var text = go.GetComponentInChildren<TMP_Text>();
if (text != null) text.text = name;
var imgRt = img.GetComponent<RectTransform>();
if (imgRt.sizeDelta.sqrMagnitude < 1f) imgRt.sizeDelta = new Vector2(20, 20);
var slot = img.GetComponent<ConnectorSlot>();
if (slot == null) slot = img.gameObject.AddComponent<ConnectorSlot>();
slot.isInput = isInput;
slot.connectorIn = inp;
slot.connectorOut = outp;
slot.ownerNode = this;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: bc99ff1458babbf418cadaa025d9c4fc

View File

@@ -1,97 +0,0 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.NodeScript
{
public class NodeUIBuilder
{
Transform parent;
GameObject dropdownPrefab;
GameObject inputPrefab;
public NodeUIBuilder(Transform parent, GameObject dropdownPrefab, GameObject inputPrefab)
{
this.parent = parent;
this.dropdownPrefab = dropdownPrefab;
this.inputPrefab = inputPrefab;
}
public NodeUIBuilder Dropdown(string label, string[] options, int defaultIndex, Action<int> onChanged)
{
if (parent == null || dropdownPrefab == null || options == null || options.Length == 0) return this;
var go = UnityEngine.Object.Instantiate(dropdownPrefab, parent);
go.name = "dd_" + label;
var dd = go.GetComponentInChildren<TMP_Dropdown>();
if (dd != null)
{
dd.ClearOptions();
foreach (var o in options) dd.options.Add(new TMP_Dropdown.OptionData(o));
dd.value = defaultIndex;
dd.onValueChanged.AddListener(i => onChanged?.Invoke(i));
}
var lbl = go.GetComponentInChildren<TMP_Text>();
if (lbl != null && lbl.transform != dd?.transform) lbl.text = label;
return this;
}
public NodeUIBuilder FloatField(string label, float defaultValue, Action<float> onChanged)
{
if (parent == null || inputPrefab == null) return this;
var go = UnityEngine.Object.Instantiate(inputPrefab, parent);
go.name = "inp_" + label;
var input = go.GetComponentInChildren<TMP_InputField>();
if (input != null)
{
input.text = defaultValue.ToString("0.##");
input.contentType = TMP_InputField.ContentType.DecimalNumber;
input.onValueChanged.AddListener(s =>
{
if (float.TryParse(s, out float v)) onChanged?.Invoke(v);
});
}
var lbl = go.GetComponentInChildren<TMP_Text>();
if (lbl != null && lbl.transform != input?.transform) lbl.text = label;
return this;
}
/// <summary>类型选择下拉NodeConst / NodeCombine 用)</summary>
public NodeUIBuilder TypeDropdown(string[] options, int defaultIndex, Action<int> onChanged)
{
return Dropdown("Type", options, defaultIndex, onChanged);
}
/// <summary>Bool 开关NodeSelect 的 condition 备选)</summary>
public NodeUIBuilder Toggle(string label, bool defaultValue, Action<bool> onChanged)
{
if (parent == null || dropdownPrefab == null) return this;
var go = UnityEngine.Object.Instantiate(dropdownPrefab, parent);
go.name = "tgl_" + label;
var dd = go.GetComponentInChildren<TMP_Dropdown>();
if (dd != null)
{
dd.ClearOptions();
dd.options.Add(new TMP_Dropdown.OptionData("False"));
dd.options.Add(new TMP_Dropdown.OptionData("True"));
dd.value = defaultValue ? 1 : 0;
dd.onValueChanged.AddListener(i => onChanged?.Invoke(i == 1));
}
var lbl = go.GetComponentInChildren<TMP_Text>();
if (lbl != null && lbl.transform != dd?.transform) lbl.text = label;
return this;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 90b84ea9f6d5bbf4690ad415ef0bb1c9

View File

@@ -1,14 +0,0 @@
节点逻辑重构
在这次重构中我们不需要改UI部分而是聚焦于node manager对node的指令的控制和node的行为的表现。我打算把系统运行的控制权集中在manager上使用类似生命周期的方式控制节点指令。
逻辑流程每个node都有一个status为ready hang或complete刚开始启动的时候所有的都为ready或者complete然后启动start节点。
Manager拥有一个触发表在开始一次周期前将触发表并入运行时表中并清空触发表。
在周期中遍历运行时表运行节点内置的loop方法 收集下一次的触发以及要从运行时表中去除的东西。
节点不再传递值会从前面的节点拿取值或者引用启动后每周期检查如果前方的节点不处于complete那么status就改为hang将自己维持在运行时表中并且将前面的node加入触发表取值满足后进行运算返回complete。
特殊节点变量节点不存在hang在启动之后立刻返回值或者引用并且complete这些节点拥有一个signal输入这个输入通常不需要连接只有在循环节点内这个signal才用得到。
循环和子函数节点可能需要一种新的状态表示其内部正在运行。这些节点可能需要一个新的UI控件比如一个rect当别的节点拖放在这个rect里面就相当于在节点内。我们的UI支持自动调整你只需要在之前放选项的地方也就是UI builder所需要的地方直接加入一个可以手动放大缩小范围的rect并且对他添加脚本就好而像for循环原有的每个循环都会有的Index和Signal输出点就可能不用放在第三列而是放在位于输入节点的第一列以拉取到rect里面循环还包含一个子控制器监控里面的节点运行情况不过这里面节点的运行状况依然交由主manager子控制器只负责检查是否完成重置节点并开启下一步的循环。
子函数节点包含子函数定义节点和子函数执行节点。定义值点只有一个输入string name我们使用rect外连接到rect内的变量节点代表输入以及连接到rect外的set节点代表输出。
Manager可能需要扫描所有文件注册这些子函数定义节点然后在其他地方的执行节点中表现为input输入和output输出。
关于signalSignal依然存在仅用于触发操作拥有signal并且signal输入已连接的节点会像上文一样等待signal。
其他节点拥有一个“到stars节点的最短距离”l
举例L等于零的输出可以接在l等于七的输入上而l等于七的输出不能接在l等于二的输入上。确保单向逻辑。
关于引用和动态类型比如set节点输入第一项为原值比如一个变量int。第二项也为int用于更改第一个项指向的值。引用节点没有输出只有输入并且要根据输入的第一项的类型更改输入第二项的类型可能要为此实现一个input<any>之类的东西,

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: c8fec166f647d414c8d2fe21bfda2c86
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,455 +0,0 @@
# NodeScript 重构大纲
## 一、重构目标
将节点系统从**推送式push-based求值**改为**Manager 集中控制的拉取式pull-based生命周期循环**。不改 UI 层,聚焦于 NodeManager 对节点的调度逻辑和节点自身的行为表现。
---
## 二、核心架构变更
### 2.1 节点生命周期状态
`NodeBase` 新增状态枚举:
```csharp
enum NodeStatus { Ready, Hang, Complete }
```
- **Ready** — 节点等待被触发执行
- **Hang** — 节点因前置节点未完成而挂起,保留在运行时表中等待下一周期
- **Complete** — 节点已完成本轮运算
变量节点的特殊规则:不存在 Hang 状态,启动后立即返回值/引用并 Complete。
### 2.2 触发表 + 运行时表Manager 侧)
Manager 维护两张表:
| 表 | 作用 |
|---|---|
| **触发表** `triggerTable` | 收集本周期要加入运行的节点,周期开始前并入运行时表 |
| **运行时表** `runtimeTable` | 当前周期正在遍历的节点集合 |
周期流程:
1. `triggerTable` → 并入 `runtimeTable`,清空 `triggerTable`
2. 遍历 `runtimeTable`,调用每个节点的 `Loop()` 方法
3. `Loop()` 返回:下一轮要触发的节点(加入 `triggerTable`+ 是否从 `runtimeTable` 移除自己
### 2.3 拉取式取值
节点不再通过 Output → Input 推送数据。改为:
- 每个周期节点从前置节点的 Output 中**主动拉取**值或引用
- 如果前置节点不处于 `Complete`,本节点 `Status = Hang`,将前置节点加入 `triggerTable`,自己保留在 `runtimeTable`
- 取值条件满足后,执行运算,返回 `Complete`
### 2.4 最短距离 L单向逻辑保证
每个节点计算到 Start 节点的最短距离 `L`,用于约束连线方向:
- L=0 的节点的输出可以连接到 L=7 的节点的输入
- L=7 的节点的输出**不能**连接到 L=2 的节点的输入
- 确保逻辑单向流动
---
## 三、文件级重构计划
### 3.1 NodeCore.cs — 核心类型重定义
| 变更项 | 说明 |
|---|---|
| 移除 `Input<T>.Notify()` / `Output<T>.SetValue()` 推送链 | 不再需要推送机制 |
| `Input<T>` 改为存储对上游 `Output<T>` 的引用,通过 `Pull()` 取值 | 拉取模式 |
| `NodeBase` 新增 `NodeStatus Status` 属性 | 生命周期状态 |
| `NodeBase` 新增抽象方法 `Loop()` | 替代 `Evaluate()`,返回 `(List<NodeBase> triggers, bool removeFromRuntime)` |
| `NodeBase` 新增 `int L` 属性 | 到 Start 节点的最短距离 |
| `NodeBase` 新增 `List<NodeBase> GetPrecedingNodes()` | 获取所有前置依赖节点 |
| 保留 `IInput` / `IOutput` 接口 | UI 层依赖不变 |
| 保留 `Signal` 结构体 | 仅用于触发操作 |
| 新增 `InputAny` 概念(见 3.4 | 动态类型支持 |
### 3.2 NodeManager.cs — 生命周期调度
| 变更项 | 说明 |
|---|---|
| 新增 `HashSet<NodeBase> triggerTable` | 触发表 |
| 新增 `HashSet<NodeBase> runtimeTable` | 运行时表 |
| 新增 `void RunCycle()` | 单周期执行逻辑 |
| 重写 `RunGraph()` | 初始化触发表为 Start/Entry 节点,循环调用 `RunCycle()` 直到运行时表为空 |
| 新增 `void ComputeLValues()` | 在连线变更后重新计算所有节点的最短距离 L |
| 新增 `bool ValidateConnection(NodeBase from, NodeBase to)` | 连线前验证 L 约束 |
| 新增子控制器管理 | 用于循环节点和子函数节点的内部运行监控 |
| 新增 `void RegisterSubFunction(NodeBase definition)` | 扫描注册子函数定义 |
| `TryConnect()` 中增加 L 约束检查 | 阻止反向连线 |
| `SaveToFile()` / `LoadFromFile()` 适配新状态 | 保存/加载兼容 |
| UI 部分(拖线、复制粘贴等)保持不变 | — |
### 3.3 NodeObject.cs / ConnectorSlot.cs / NodeUIBuilder.cs — UI 层
**原则上不修改**,仅可能的微调:
- `NodeObject.Init()` 中调用 `nodeBase.InitConnectors()` 后触发 L 值计算
- 循环/子函数节点的 Rect 容器支持(见 3.6
### 3.4 动态类型 — InputAny / OutputAny 机制(多类型统一节点的基石)
#### 3.4.1 问题
当前同一功能、不同类型的节点大量重复:
| 功能 | 现有节点 | 覆盖类型 |
|---|---|---|
| 常量 | `NodeConstFloat`, `NodeConstVector2`, `NodeConstVector3`, `NodeConstColor` | float, Vector2, Vector3, Color |
| 拆分 | `NodeSplitV2`, `NodeSplitV3` | Vector2, Vector3 |
| 合并 | `NodeCombineV2`, `NodeCombineV3` | Vector2, Vector3 |
| 数学 | `NodeMath`(仅 float | float |
这些节点的**逻辑完全一致**,仅类型不同。引入 `InputAny` / `OutputAny` 后,一个节点覆盖所有类型。
#### 3.4.2 类型列表
```csharp
// 系统支持的全部可连线类型
static readonly HashSet<Type> SupportedTypes = new()
{
typeof(float), typeof(int), typeof(bool), typeof(string),
typeof(Vector2), typeof(Vector3), typeof(Color),
typeof(GameElement), typeof(List<GameElement>),
typeof(Signal),
};
```
#### 3.4.3 InputAny / OutputAny 设计
```csharp
/// <summary>未锁定的输入端口,连线后类型自动锁定</summary>
public class InputAny : IInput
{
object _sourceOutput; // 连到的 Output可能是 Output<T> 或 OutputAny
public string Name { get; set; }
public Type DataType { get; private set; } // null 表示未锁定,连线后锁定
public bool IsConnected => _sourceOutput != null;
public bool HasReceived { get; set; }
public Color ConnectorColor => DataType != null ? NodeColors.Get(DataType) : Color.grey;
/// <summary>直接取已连接的上游值(泛型方式)</summary>
public T GetValue<T>() { ... }
/// <summary>取值为 object</summary>
public object GetValue() { ... }
/// <summary>连线时由 Manager 调用,锁定端口类型</summary>
internal void LockType(Type t) { DataType = t; }
}
/// <summary>未锁定的输出端口,类型由同节点的 InputAny 传播决定</summary>
public class OutputAny : IOutput
{
object _value;
public string Name { get; set; }
public Type DataType { get; private set; } // null 表示未锁定
public bool IsConnected => _targets.Count > 0;
public Color ConnectorColor => DataType != null ? NodeColors.Get(DataType) : Color.grey;
public T GetValue<T>() { ... }
public void SetValue<T>(T v) { _value = v; }
/// <summary>由节点的某个 InputAny 锁定后传播过来</summary>
internal void LockType(Type t) { DataType = t; }
}
```
#### 3.4.4 类型传播规则
一个节点上存在多个 `InputAny` / `OutputAny` 时,类型按以下优先级传播:
```
规则 1外部优先: 任一 InputAny 被连线 → 锁定该端口类型 → 传播到同节点所有未锁定的 InputAny / OutputAny
规则 2冲突检测: 两个已连线的 InputAny 类型不一致 → Manager 阻止连线,报 "Type mismatch"
规则 3OutputAny: 总是跟随同节点的首个已锁定 InputAny 的类型
规则 4无输入节点: 常量类节点无 InputAnyOutputAny 类型由节点内置字段/UI 选择决定
```
#### 3.4.5 ConnectorSlot UI 适配
- 未锁定的 `InputAny` / `OutputAny` 连接点显示**灰色**
- 连线锁定后,动态更新连接点的颜色以匹配锁定类型
- `ConnectorSlot` 需要监听 `DataType` 变化并刷新颜色
- Manager 在 `TryConnect` 成功后调用 `Slot.RefreshAppearance()`
---
### 3.5 多类型统一节点设计
以下节点取代现有的同功能多类型节点。
#### 3.5.1 NodeConst — 通用常量(取代 NodeConstFloat/V2/V3/Color
```
NodeConst:
[UI] Dropdown: Type (float / int / bool / Vector2 / Vector3 / Color)
[UI] 根据所选类型动态显示对应输入控件
OutputAny value ← 类型由 UI 选择锁定
```
逻辑无输入Loop 中直接 Complete。输出值被下游拉取。
#### 3.5.2 NodeMath — 通用数学运算(扩展覆盖类型)
```
NodeMath:
[UI] Dropdown: Op (Add / Sub / Mul / Div)
InputAny a ← 连线后锁定类型
InputAny b ← 跟随 a 的类型(或相反,谁先连跟谁)
OutputAny result ← 类型来源同 InputAny 的锁定类型
```
支持的运算映射LUT 注册):
| 类型 | Add | Sub | Mul | Div |
|---|---|---|---|---|
| float, int | `+` | `-` | `*` | `/` |
| Vector2, Vector3 | `+` | `-` | `* float` | `/ float` |
| string | 拼接 | — | — | — |
| Color | `+` (叠加) | `-` | `* float` | — |
> 实现:用 `Dictionary<(Type, Op), Func<object, object, object>>` 查表分派。
#### 3.5.3 NodeSplit — 通用拆分(取代 NodeSplitV2/V3
```
NodeSplit:
InputAny input ← Vector2 → 输出 X(float), Y(float)
← Vector3 → 输出 X(float), Y(float), Z(float)
OutputAny x, y, z ← z 仅在 Vector3 时激活
```
- 默认所有 OutputAny 端口可见但灰掉,`input` 锁定类型后按需激活
- 对于 Color输出 R, G, B, A (float)
#### 3.5.4 NodeCombine — 通用合并(取代 NodeCombineV2/V3
```
NodeCombine:
[UI] Dropdown: Type (Vector2 / Vector3 / Color)
InputAny x, y, z, w ← 数量按类型动态显示
OutputAny output ← 类型跟随 UI 选择
```
#### 3.5.5 NodeLerp — 线性插值(新节点)
```
NodeLerp:
InputAny a, b ← 连线锁定类型
InputAny t ← 预期 float不受 a/b 锁定影响,标记为 fixed-type
OutputAny result ← 跟随 a/b 类型
```
支持 float, int, Vector2, Vector3, Color。
#### 3.5.6 NodeCompare — 比较运算(新节点)
```
NodeCompare:
[UI] Dropdown: Op (==, !=, >, <, >=, <=)
InputAny a, b ← 连线锁定类型(支持 float, int
OutputAny result ← 固定 bool
```
> 注意:> \ < 仅在数值类型有效,== / != 可扩展至 string。
#### 3.5.7 NodeSelect — 二选一(新节点)
```
NodeSelect:
InputAny condition ← 如果未连线,用 [UI] Toggle(bool);如果连线则类型锁定 bool
InputAny trueValue, falseValue ← 类型互相跟随
OutputAny result ← 跟随 trueValue/falseValue 类型
```
#### 3.5.8 NodeSet — 万能赋值(新节点,引用语义)
```
NodeSet:
InputAny targetRef ← 连接到变量节点的 get 输出(锁定为目标类型)
InputAny value ← 跟随 targetRef 类型
// 无 OutputAny纯副作用节点
```
> 关键targetRef 不仅是取值,还要修改其引用的 Variable 内部值。需要 InputAny 能够"反向写入"。
#### 3.5.9 节点对比总结
| 统一节点 | 取代旧节点 | 覆盖类型数 |
|---|---|---|
| `NodeConst` | `NodeConstFloat`, `NodeConstVector2`, `NodeConstVector3`, `NodeConstColor` | 6+ |
| `NodeMath` | `NodeMath`(扩展) | 5 (float, int, Vector2, Vector3, Color) |
| `NodeSplit` | `NodeSplitV2`, `NodeSplitV3` | 3 (Vector2, Vector3, Color) |
| `NodeCombine` | `NodeCombineV2`, `NodeCombineV3` | 3 (Vector2, Vector3, Color) |
| `NodeLerp` | (新) | 5 |
| `NodeCompare` | (新) | 3 |
| `NodeSelect` | (新) | 任意 |
| `NodeSet` | (新) | 任意 |
---
### 3.6 InputAny 类型锁定流程Manager 侧)
```
TryConnect(OutputAny/Output<T> src, InputAny dst):
1. 获取 src 的实际 DataType → T
2. 如果 dst.DataType == null → dst.LockType(T) → 传播到同节点其他端口
3. 如果 dst.DataType == T → OK
4. 如果 dst.DataType != T → 拒绝,类型不匹配
5. 调用 dst.ownerNode.OnTypePropagated() 通知节点刷新 UI
```
传播方法(在 `NodeBase` 上):
```csharp
/// <summary>当某个 InputAny 或 OutputAny 锁定了类型后,通知节点刷新其他端口</summary>
public virtual void OnTypePropagated(ConnectorSlot lockedSlot, Type lockedType)
{
// 默认遍历所有未锁定端口LockType(lockedType)
foreach (var slot in GetAnySlots())
if (slot.DataType == null)
slot.LockType(lockedType);
}
```
#### 3.6.1 fixed-type 端口标记
某些 InputAny 不接受类型传播(如 `NodeLerp.t` 必须是 float。新增标记
```csharp
public class InputAny : IInput
{
public bool IsFixedType { get; init; } // true = 不被传播覆盖,始终保持初始类型
}
```
`NodeLerp.t``new InputAny { IsFixedType = true, Name = "t" }`(已指定 float 意图时 LockType(float)
---
### 3.7 特殊节点:变量节点
```csharp
class NodeVariable<T> : NodeBase
{
Input<Signal> signal; // 通常不连,仅在循环内使用
Input<T> set;
Output<T> get;
}
```
- `Loop()` 立即返回当前值,不存在 Hang 状态
- 有 Signal 输入时(循环体内),等待 Signal 触发才更新
- 变量节点不参与 InputAny 类型传播——类型由泛型参数 T 固定
### 3.8 循环节点 & 子函数节点(新 UIRect 容器)
**UI 变更NodeUIBuilder 扩展):**
- 新增一个可手动拖拽缩放大小的 Rect 区域
- 将其他节点拖入此 Rect 即表示"在节点内部"
- 循环节点的 Index 和 Signal 输出点移到第一列(输入列),可拉线到 Rect 内部
**节点结构:**
```
NodeForLoop:
外部输入: exec(Signal), count(InputAny) ← count 支持 int/float
外部输出: completed(Signal)
内部 Rect 中:
子节点...(由主 Manager 调度,子控制器监控)
Index 输出(int) — 在第一列,拉入 Rect 内
LoopBody 输出(Signal) — 在第一列,拉入 Rect 内
```
**子控制器职责:**
- 检查循环体内所有节点是否 Complete
- 重置内部节点,开启下一步循环
- 向主 Manager 报告循环是否全部完成
**子函数定义节点:**
```
NodeSubFunctionDef:
输入: name(string)
Rect 外→Rect 内的变量节点 → 代表函数输入参数(变量类型即参数类型)
Rect 内的 set 节点 → Rect 外 → 代表函数输出
```
**子函数执行节点(配合 InputAny**
```
NodeSubFunctionCall:
— 根据已注册的子函数定义,动态生成 InputAny/OutputAny 端口
— 端口类型由子函数定义中变量节点的泛型参数决定
— Manager 扫描所有文件注册子函数定义
```
### 3.9 现有节点迁移计划
| 旧节点 | 处理方式 | 说明 |
|---|---|---|
| `NodeStart` | 迁移:`Evaluate()``Loop()` | 启动后 Complete |
| `NodeEntry` | 同上 | — |
| `NodeMath` | **扩展为多类型版本**3.5.2 | 旧版删除 |
| `NodeConstFloat/V2/V3/Color` | **合并为 `NodeConst`**3.5.1 | 四个节点合一,旧版全部删除 |
| `NodeSplitV2/V3` | **合并为 `NodeSplit`**3.5.3 | 两个节点合一 |
| `NodeCombineV2/V3` | **合并为 `NodeCombine`**3.5.4 | 两个节点合一 |
| `NodeForLoop` | 大改Rect 容器 + 子控制器 | count 改用 InputAny |
| `NodeForEach<T>` | 类似 ForLoop 改造 | — |
| `NodeBranch` | 迁移到 Loop()condition 改用 InputAny | 支持 float/int 条件 |
| `NodeGameElement` / `NodeSetTransform` / `NodeClone` 等 | 生命周期适配Signal 等待逻辑不变 | — |
| `NodeVariable<T>` | 特殊处理:立即 Complete不经过 Hang | 保持泛型不变 |
| `NodeDebugLog` / `NodeLog` | 迁移InputAny 支持任意显示类型 | — |
| `NodeList<T>` / `NodeListAdd<T>` / `NodeListGet<T>` | 保持泛型Loop() 适配 | — |
| `NodePositionStepper` | 迁移,参数改用 InputAny | — |
| **新增** `NodeLerp` | 全新 | — |
| **新增** `NodeCompare` | 全新 | — |
| **新增** `NodeSelect` | 全新 | — |
| **新增** `NodeSet` | 全新(需要反向写入能力) | — |
---
## 四、实施步骤建议
1. **Phase 1: 核心类型层** — 修改 `NodeCore.cs`
- 新增 `NodeStatus` 枚举
- `NodeBase` 新增 `Loop()``L``Status`
- `Input<T>` / `Output<T>` 改为拉取模式
- 实现 `InputAny``OutputAny``OnTypePropagated``IsFixedType`
2. **Phase 2: Manager 调度层** — 修改 `NodeManager.cs`
- 实现触发表/运行时表
- 实现 `RunCycle()` 循环调度
- 实现 L 值计算 + 连线验证
- 实现 `TryConnect` 中的 InputAny 类型锁定传播流程
3. **Phase 3: 统一节点实现** — 删除旧重复节点,实现新版
- 先实现 `NodeConst``NodeMath`(验证 InputAny 机制可用)
- 再实现 `NodeSplit``NodeCombine``NodeLerp`
- `NodeCompare``NodeSelect``NodeSet`(反向写入)
- 迁移保留的节点(`NodeStart``NodeBranch``NodeVariable<T>` 等)
4. **Phase 4: 循环/子函数** — Rect 容器 + 子控制器
- `NodeUIBuilder` 增加 Rect 容器支持
- 循环节点子控制器实现
- 子函数定义/执行节点 + Manager 注册扫描
5. **Phase 5: 测试 & 清理**
- 验证保存/加载兼容(含新旧类型映射)
- 删除旧版备份文件 `NodeBase.cs.bak`
- 删除旧节点类(`NodeConstFloat` 等)
- 验证所有节点类型覆盖无遗漏
---
## 五、风险点 & 注意事项
- **UI 不变原则**`NodeObject``ConnectorSlot``NodeUIBuilder` 的接口保持稳定,节点层改动不应破坏 UI 渲染ConnectorSlot 仅新增 `RefreshAppearance()`
- **InputAny 类型冲突**:同一节点两个已连线的 InputAny 类型不一致时Manager 拒绝并报错
- **反向写入**`NodeSet` 的 targetRef 需要能修改上游 Variable 内部值,这是 InputAny 设计的关键难点
- **向后兼容**:旧 JSON 中的 `NodeConstFloat` 等类型名加载时需映射到新的 `NodeConst`
- **循环嵌套**:子控制器设计需考虑循环内嵌套循环的递归情况
- **Performance**:大图时每帧遍历运行时表的开销,类型检查用 `Type` 引用比较(非字符串)
- **Signal 类型**:保留但不参与 InputAny 的类型传播

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 1091d96afa6e6e940a40f1345a5225f8
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,329 +0,0 @@
# NodeScript 系统总览
## 文件结构
```
Editor Tools/NodeScript/
├── NodeCore.cs # 核心类型:生命周期、连接器、动态类型
├── NodeManager.cs # 调度中心 + UI 交互
├── NodeObject.cs # MonoBehaviour节点 UI、插槽、选中、状态显示
├── ConnectorSlot.cs # 连接点交互 + 外观刷新
├── NodeUIBuilder.cs # UI 构建Dropdown / FloatField / Toggle / TypeDropdown
├── NodeCompoments/
│ ├── NodeCompoment.cs # 操作 / 控制流节点
│ └── NodeUtility.cs # 工具 / 常量 / 变量节点
├── Node重构.txt # 重构需求文档
├── Node重构大纲.md # 详细重构设计
└── 总览.md # 本文件
```
---
## 一、执行模型:拉取式 + 生命周期
### 1.1 周期调度
```
RunGraph():
1. ComputeLValues() — BFS 算所有节点到 Start 的最短距离 L
2. triggerTable = {全部节点}
3. while triggerTable runtimeTable 非空:
RunCycle()
RunCycle():
1. triggerTable 并入 runtimeTable清空 triggerTable
2. foreach node in runtimeTable:
result = node.Loop()
收集 triggers → triggerTable
if TriggerDownstream → 自动发现下游节点 → triggerTable
if RemoveFromRuntime → 标记移除
3. runtimeTable -= 已完成的节点
```
### 1.2 节点状态
| 状态 | 含义 |
|---|---|
| `Ready` | 等待本周期执行 |
| `Hang` | 前置节点未完成,挂起到下一周期 |
| `Complete` | 已完成,移出 runtimeTable |
### 1.3 LoopResult 返回类型
| 工厂方法 | RemoveFromRuntime | TriggerDownstream | 用途 |
|---|---|---|---|
| `Complete()` | true | true | 节点完成,触发下游 |
| `Hang(preceding)` | false | false | 等待前置节点 |
| `Repeat()` | false | true | 保持活跃+触发下游(循环迭代) |
| `Wait()` | false | false | 空转一周期(循环等下游消费) |
### 1.4 L 值
- BFS 从 `NodeStart`/`NodeEntry` 开始计算L 值显示在节点标题栏 `(L:N)`
- 仅显示,不约束连线
- L=-1 表示孤立节点(无路径可达)
### 1.5 防自循环
连线 A→B 时,从 B 出发沿现有边 DFS看能否回到 A。能回到则拒绝连线。
---
## 二、类型系统
### 2.1 连接器体系
```
IInput IOutput
├── Input<T> (泛型) ├── Output<T> (泛型)
└── InputAny (动态) └── OutputAny (动态)
```
### 2.2 InputAny / OutputAny
| 特性 | 说明 |
|---|---|
| 初始状态 | `DataType = null`,连接点显示**灰色** |
| 连线锁定 | 连线后自动 `LockType(T)`,连接点变为类型对应颜色 |
| 类型传播 | 锁定后 `OnTypePropagated(T)` → 同节点其他未锁定端口跟随 |
| `IsFixedType` | true 时阻止传播覆盖,如 `NodeLerp.t` 固定为 float |
| 可改类型 | `LockType` 允许覆盖(防止 `NodeConst` 切换类型后 DataType 不更新) |
### 2.3 Input<T> 拉取模型
- `Value` 优先读 `Output<T>._value`,回退读 `OutputAny`
- `ConnectAny(IOutput)` 支持 `OutputAny → Input<T>` 桥接(如 `NodeConst → NodeMath.a`
- `GetSourceNode()` 兼容两种源
### 2.4 Output<T> 反向写入
- `_writeBack` 回调:`NodeSet` 通过 targetRef 修改 `NodeVariable` 的内部值
### 2.5 类型兼容表
| from | to | 兼容 |
|---|---|---|
| null | * | ✓(未锁定端口) |
| T | T | ✓ |
| int | float | ✓ |
| float | int | ✓ |
| 其他 | 其他 | ✗ |
### 2.6 类型颜色
| 类型 | 颜色 |
|---|---|
| float | 蓝 |
| int | 青 |
| bool | 橙 |
| string | 紫 |
| Vector2 | 黄 |
| Vector3 | 绿 |
| Color | 粉 |
| GameElement | 深紫 |
| Signal | 白 |
| List\<GameElement\> | 暗紫 |
| null未锁定 | 灰 |
---
## 三、节点目录
### 3.1 入口节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeStart` | — | `exec(Signal)`, `element(GameElement)` | 图入口,绑定当前选中元素 |
| `NodeEntry` | — | `exec(Signal)` | 纯信号入口 |
### 3.2 运算节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeMath` | `a(Any)`, `b(Any, fixed)` | `result(Any)` | +-*/ 四则,支持 float/int/V2/V3/Color/string |
| `NodeLerp` | `a(Any)`, `b(Any)`, `t(float,fixed)` | `result(Any)` | 线性插值t 未连默认 0.5 |
| `NodeCompare` | `a(Any)`, `b(Any)` | `result(bool)` | == != > < >= <= |
### 3.3 向量节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeSplit` | `input(Any)` | `x/y/z/w(float)` | V2 输出 xyV3 输出 xyzColor 输出 rgba |
| `NodeCombine` | `x/y/z/w(float)` | `output(Any)` | UI 选类型后合并为 V2/V3/Color |
| `NodeGetTransform` | `element(GE)` | `pos/rot/scl(V3)` | 读取元素变换 |
| `NodeSetTransform` | `exec(Sig)`, `element(GE)`, `Pos/Rot/Scl(V3)` | — | 设置元素变换 |
### 3.4 数据节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeConst` | — | `value(Any)` | UI 选类型+填值6 种类型统一 |
| `NodeVariable<T>` | `signal(Sig)`, `set(T)` | `get(T)` | 变量存储;`signal` 连着时等触发才更新 |
| `NodeSet` | `targetRef(Any)`, `value(Any)` | — | 反向写入 targetRef 指向的变量 |
| `NodeSelect` | `cond(Any)`, `true(Any)`, `false(Any)` | `result(Any)` | 二选一cond 未连用 UI Toggle |
### 3.5 控制流节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeBranch` | `exec(Sig)`, `cond(Any)` | `true/false(Sig)` | cond>0 走 true否则 false |
| `NodeForLoop` | `exec(Sig)`, `count(Any)` | `loopBody(Sig)`, `index(int)`, `completed(Sig)` | 多周期交替输出→Wait→输出→Wait... |
### 3.6 集合节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeList<T>` | — | `output(List<T>)` | 空列表 |
| `NodeListAdd<T>` | `list(List<T>)`, `item(T)` | `output(List<T>)` | 追加元素 |
| `NodeListGet<T>` | `list(List<T>)`, idx(UI) | `element(T)` | 索引取值 |
| `NodeForEach` | `exec(Sig)`, `list(Any)` | `loopBody(Sig)`, `current(Any)`, `index(int)`, `completed(Sig)` | **非泛型**,连 `List<X>` 自动锁定 current 为 X |
### 3.7 GameElement 操作
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeGameElement` | `exec(Sig)`, `Root(GE)`, `Source(GE)` | `newElement(GE)`, `completed(Sig)` | 复制粘贴元素 |
| `NodeChildByIndex` | `parent(GE)`, idx(UI) | `child(GE)` | 子元素按索引 |
| `NodeChildCount` | `parent(GE)` | `count(int)` | 子元素个数 |
| `NodeClone` | `exec(Sig)`, `source(GE)` | `clone(GE)` | Instantiate 克隆 |
### 3.8 调试节点
| 节点 | 输入 | 输出 | 说明 |
|---|---|---|---|
| `NodeDebugLog` | `exec(Sig)`, `value(Any)` | — | 带 Signal 等待的日志 |
| `NodeLog` | `value(Any)` | — | 直接打印日志 |
---
## 四、所有节点统一规范
每个节点的 `Loop()` 遵循:
```csharp
public override LoopResult Loop()
{
// 1. 无输入直接 Complete
// 2. EnsureInputsReady() — 所有已连输入的上游必须 Complete
// 3. 取值 → 计算 → 设输出
// 4. 返回 Complete / Repeat / Wait
}
```
`EnsureInputsReady()``NodeBase` 上定义,自动检查 `GetPrecedingNodes()` 中所有上游的 Status。
---
## 五、UI 特性
### 5.1 节点外观
- 半透明背景 + 标题栏 `Name (L:N)`
- `statusImage`Ready 灰 / Hang 橙黄 / Complete 绿
- 选中节点蓝色高亮
### 5.2 连接线
- `UILineRenderer` 贝塞尔曲线
- 手动距离检测悬停(不依赖 Unity 射线)
- 悬停加粗 +3px选中加粗 ×2
- RectTransform 自动缩放到包围盒
- 拖线时 dragLine 不参与射线
### 5.3 交互
| 操作 | 功能 |
|---|---|
| 左键空白 | 取消所有选中 |
| 左键节点 | 选中节点Shift 多选) |
| 左键拖节点 | 移动节点 |
| 左键拖输出点→输入点 | 连线 |
| 左键线 | 选中线Shift 多选) |
| 中键拖面板 | 平移整个画布 |
| Ctrl+右键 | 右键菜单创建节点 |
| Delete | 删除选中 |
### 5.4 快捷键
| 快捷键 | 功能 |
|---|---|
| `F3` | 新建/销毁 NodeScript 编辑器 |
| `Enter` | 完整运行图 |
| `Shift+Enter` | 单步调试(首次初始化) |
| `Esc` | 退出调试模式 |
| `Ctrl+Enter` | 运行图(保留) |
| `F5` | 拓扑预览(打印分层执行计划) |
| `F1` | 保存 |
| `F2` | 加载 |
| `Ctrl+C/V` | 复制/粘贴节点 |
| `Delete` | 删除选中 |
### 5.5 控制台命令
| 命令 | 说明 |
|---|---|
| `newNode` | 新建/销毁 NodeScript 编辑器 |
| `saveNode` | 保存到默认 `graph.json` |
| `saveNode name` | 另存为 `{name}.json` |
| `loadNode name` | 从 `{name}.json` 加载 |
---
## 六、调试功能
### 6.1 单步调试 (Shift+Enter)
每步打印详细日志:
```
=== Step 3 ===
✓ NodeStart (L:0) → Complete
⏳ NodeMath (L:1) → Hang
▶ NodeBranch (L:2) → Ready
triggers pending: 2, still running: 3
```
`statusImage` 同步变色。
### 6.2 拓扑预览 (F5)
```
═══ Topological Order (BFS layers) ═══
Layer 0 (2 nodes, 3 downstream wires): Start(L:0), Entry(L:0)
Layer 1 (1 nodes, 2 downstream wires): NodeMath(L:1)
...
Unreachable (1 nodes): OrphanConst
Total: 6 nodes, 5 wires, 3 layers
```
---
## 七、NodeManager 关键 API
| 方法 | 说明 |
|---|---|
| `Init(GameElement)` | 绑定元素 + 创建 Start 节点 |
| `RunGraph()` | 完整执行(生命周期循环) |
| `ComputeLValues()` | 重算所有节点的 L 值 + 刷新标题 |
| `SaveToFile(string?)` | 保存图null 用默认路径 |
| `LoadFromFile(string?)` | 加载图 |
| `GetSavePath(string)` | 获取完整保存路径 |
---
## 八、文件存储
| 项目 | 路径 |
|---|---|
| 保存目录 | `Assets/StreamingAssets/NodeScript/` |
| 默认文件 | `graph.json` |
| 自定义文件 | `{name}.json` |
---
## 九、待完成 (Phase 4+)
- [ ] Rect 容器(循环 / 子函数体内嵌区域)
- [ ] 子控制器(循环体内节点独立调度)
- [ ] `NodeSubFunctionDef` / `NodeSubFunctionCall`
- [ ] Manager 扫描注册子函数定义

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 77e5a38d58b4a674ca00977979b575d5
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: