@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 903817bad09ed754ca52d7ea5ba15e97
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d5e4efd89e8c6041bdcdeae97e94191
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36b55b766114d5d4bb9cfb9f673bbbae
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da71a524cc8687941840c85db8590b1c
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6d155dc132fc24448aa1896c024f9a6
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 335508d8cda287449ad8009a1c9e61a8
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6cdb8cc0b3fb21f49b1a761000205509
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc99ff1458babbf418cadaa025d9c4fc
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90b84ea9f6d5bbf4690ad415ef0bb1c9
|
||||
@@ -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输出。
|
||||
关于signal:Signal依然存在,仅用于触发操作,拥有signal并且signal输入已连接的节点会像上文一样等待signal。
|
||||
其他:节点拥有一个“到stars节点的最短距离”l
|
||||
举例:L等于零的输出可以接在l等于七的输入上,而l等于七的输出不能接在l等于二的输入上。确保单向逻辑。
|
||||
关于引用和动态类型:比如set节点,输入第一项为原值,比如一个变量int。第二项也为int,用于更改第一个项指向的值。引用节点没有输出,只有输入,并且要根据输入的第一项的类型更改输入第二项的类型,可能要为此实现一个input<any>之类的东西,
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8fec166f647d414c8d2fe21bfda2c86
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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"
|
||||
规则 3(OutputAny): 总是跟随同节点的首个已锁定 InputAny 的类型
|
||||
规则 4(无输入节点): 常量类节点无 InputAny,OutputAny 类型由节点内置字段/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 循环节点 & 子函数节点(新 UI:Rect 容器)
|
||||
|
||||
**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 的类型传播
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1091d96afa6e6e940a40f1345a5225f8
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 输出 xy,V3 输出 xyz,Color 输出 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 扫描注册子函数定义
|
||||
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77e5a38d58b4a674ca00977979b575d5
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user