From 9e5f037d5df096eef36f2b72f67af64152fd1ea6 Mon Sep 17 00:00:00 2001
From: yhh <359807859@qq.com>
Date: Mon, 5 Jan 2026 18:35:42 +0800
Subject: [PATCH] docs(blueprint): improve graph visualization with auto-layout
- Add blueprint-graph.js for automatic node layout
- Use weighted graph algorithm for better node positioning
- Add drag-to-scroll functionality for large graphs
- Update CSS to support scrollable graph containers
- Sync Chinese and English docs for Example 2 (Health System)
- Add explanation for custom Event OnDamage node
---
docs/public/js/blueprint-graph.js | 462 +++++++++++++
.../docs/en/modules/blueprint/editor-guide.md | 266 +++++++-
.../docs/modules/blueprint/editor-guide.md | 618 +++++++++++++++---
docs/src/styles/custom.css | 254 +++++++
4 files changed, 1487 insertions(+), 113 deletions(-)
create mode 100644 docs/public/js/blueprint-graph.js
diff --git a/docs/public/js/blueprint-graph.js b/docs/public/js/blueprint-graph.js
new file mode 100644
index 00000000..a349d3ae
--- /dev/null
+++ b/docs/public/js/blueprint-graph.js
@@ -0,0 +1,462 @@
+/**
+ * Blueprint Graph Renderer
+ * Custom layout algorithm designed specifically for blueprint-style graphs
+ */
+(function() {
+ const PIN_COLORS = {
+ exec: '#ffffff',
+ entity: '#00a0e0',
+ component: '#7030c0',
+ float: '#7ecd32',
+ int: '#1cc4c4',
+ bool: '#8c0000',
+ string: '#e060e0',
+ any: '#707070'
+ };
+
+ const HEADER_CLASSES = {
+ event: 'event',
+ function: 'function',
+ pure: 'pure',
+ flow: 'flow',
+ math: 'math',
+ time: 'time',
+ debug: 'debug',
+ variable: 'variable'
+ };
+
+ const H_GAP = 50; // Horizontal gap between columns
+ const V_GAP = 25; // Vertical gap between nodes
+ const START_X = 20;
+ const START_Y = 20;
+
+ function estimateNodeSize(node) {
+ const headerHeight = 28; // Match CSS: min-height 28px
+ const pinRowHeight = 22; // Match CSS: pin row ~22px
+ const bodyPadding = 12; // Top + bottom padding
+
+ // Count all pins in body (each pin is its own row now)
+ const inputExecCount = node.inputs ? node.inputs.filter(p => p.type === 'exec').length : 0;
+ const inputDataCount = node.inputs ? node.inputs.filter(p => p.type !== 'exec').length : 0;
+ const outputExecCount = node.outputs ? node.outputs.filter(p => p.type === 'exec' && !p.inHeader).length : 0;
+ const outputDataCount = node.outputs ? node.outputs.filter(p => p.type !== 'exec' && !p.inHeader).length : 0;
+ const totalPins = inputExecCount + inputDataCount + outputExecCount + outputDataCount;
+
+ // Calculate height: header + body padding + all pin rows
+ const bodyHeight = totalPins > 0 ? bodyPadding + (totalPins * pinRowHeight) : 0;
+ const height = headerHeight + bodyHeight;
+
+ // Calculate width based on content
+ let maxLabelLen = node.title.length;
+ if (node.inputs) {
+ node.inputs.forEach(p => {
+ const len = (p.label || '').length + (p.value ? String(p.value).length + 3 : 0);
+ maxLabelLen = Math.max(maxLabelLen, len);
+ });
+ }
+ if (node.outputs) {
+ node.outputs.forEach(p => {
+ maxLabelLen = Math.max(maxLabelLen, (p.label || '').length);
+ });
+ }
+
+ const width = Math.max(110, Math.min(170, maxLabelLen * 8 + 40));
+ return { width, height };
+ }
+
+ /**
+ * Smart blueprint layout algorithm
+ *
+ * Uses weighted graph analysis:
+ * - All connections matter (exec has higher weight)
+ * - Topological sort for X ordering
+ * - Force-directed optimization for Y positions
+ */
+ function autoLayout(graphData, maxWidth) {
+ const nodes = graphData.nodes;
+ const connections = graphData.connections;
+
+ if (nodes.length === 0) return { positions: {}, sizes: {} };
+
+ // Calculate node sizes
+ const nodeSizes = {};
+ nodes.forEach(n => { nodeSizes[n.id] = estimateNodeSize(n); });
+
+ // Build maps
+ const pinToNode = {};
+ const nodeById = {};
+ nodes.forEach(n => {
+ nodeById[n.id] = n;
+ (n.inputs || []).forEach(p => { pinToNode[p.id] = n.id; });
+ (n.outputs || []).forEach(p => { pinToNode[p.id] = n.id; });
+ });
+
+ // Build weighted adjacency: outgoing[nodeId] = [{to, weight}]
+ const outgoing = {};
+ const incoming = {};
+ nodes.forEach(n => { outgoing[n.id] = []; incoming[n.id] = []; });
+
+ connections.forEach(c => {
+ const from = pinToNode[c.from];
+ const to = pinToNode[c.to];
+ if (!from || !to || from === to) return;
+
+ const weight = c.type === 'exec' ? 3 : 1;
+ outgoing[from].push({ to, weight });
+ incoming[to].push({ from, weight });
+ });
+
+ // Calculate node "depth" using weighted longest path
+ const nodeDepth = {};
+ const visited = new Set();
+ const inProcess = new Set();
+
+ function calcDepth(nodeId) {
+ if (visited.has(nodeId)) return nodeDepth[nodeId];
+ if (inProcess.has(nodeId)) return 0; // Cycle detected
+
+ inProcess.add(nodeId);
+
+ let maxPrevDepth = -1;
+ incoming[nodeId].forEach(({ from, weight }) => {
+ const prevDepth = calcDepth(from);
+ maxPrevDepth = Math.max(maxPrevDepth, prevDepth);
+ });
+
+ inProcess.delete(nodeId);
+ visited.add(nodeId);
+ nodeDepth[nodeId] = maxPrevDepth + 1;
+ return nodeDepth[nodeId];
+ }
+
+ // Calculate depth for all nodes
+ nodes.forEach(n => calcDepth(n.id));
+
+ // Group nodes by depth (column)
+ const columnNodes = {};
+ nodes.forEach(n => {
+ const depth = nodeDepth[n.id];
+ if (!columnNodes[depth]) columnNodes[depth] = [];
+ columnNodes[depth].push(n.id);
+ });
+
+ // Sort columns
+ const sortedColumns = Object.keys(columnNodes).map(Number).sort((a, b) => a - b);
+
+ // Calculate X positions
+ const columnX = {};
+ let currentX = START_X;
+ sortedColumns.forEach(col => {
+ columnX[col] = currentX;
+ let maxW = 0;
+ columnNodes[col].forEach(id => {
+ maxW = Math.max(maxW, nodeSizes[id].width);
+ });
+ currentX += maxW + H_GAP;
+ });
+
+ // Initialize Y positions - simple stacking first
+ const positions = {};
+ sortedColumns.forEach(col => {
+ let y = START_Y;
+ columnNodes[col].forEach(id => {
+ positions[id] = { x: columnX[col], y };
+ y += nodeSizes[id].height + V_GAP;
+ });
+ });
+
+ // Force-directed optimization for Y positions (few iterations)
+ for (let iter = 0; iter < 5; iter++) {
+ const forces = {};
+ nodes.forEach(n => { forces[n.id] = 0; });
+
+ // Calculate forces from connections
+ connections.forEach(c => {
+ const from = pinToNode[c.from];
+ const to = pinToNode[c.to];
+ if (!from || !to || from === to) return;
+
+ const weight = c.type === 'exec' ? 2 : 1;
+ const fromY = positions[from].y + nodeSizes[from].height / 2;
+ const toY = positions[to].y + nodeSizes[to].height / 2;
+ const diff = toY - fromY;
+
+ // Pull nodes toward each other
+ forces[from] += diff * 0.1 * weight;
+ forces[to] -= diff * 0.1 * weight;
+ });
+
+ // Apply forces
+ nodes.forEach(n => {
+ positions[n.id].y += forces[n.id];
+ positions[n.id].y = Math.max(START_Y, positions[n.id].y);
+ });
+
+ // Resolve overlaps within columns
+ sortedColumns.forEach(col => {
+ const nodesInCol = columnNodes[col];
+ nodesInCol.sort((a, b) => positions[a].y - positions[b].y);
+
+ for (let i = 1; i < nodesInCol.length; i++) {
+ const prevId = nodesInCol[i - 1];
+ const currId = nodesInCol[i];
+ const minY = positions[prevId].y + nodeSizes[prevId].height + V_GAP;
+ if (positions[currId].y < minY) {
+ positions[currId].y = minY;
+ }
+ }
+ });
+ }
+
+ return { positions, sizes: nodeSizes };
+ }
+
+ function renderPinSvg(type, filled = true) {
+ const color = PIN_COLORS[type] || PIN_COLORS.any;
+ if (type === 'exec') {
+ return ``;
+ }
+ return ``;
+ }
+
+ function renderNode(node, position, size) {
+ const isEvent = node.category === 'event';
+ const headerClass = HEADER_CLASSES[node.category] || 'function';
+
+ let html = `
`;
+ html += ``;
+
+ // Separate exec and data pins (matching node-editor order)
+ const inputExecPins = (node.inputs || []).filter(p => p.type === 'exec');
+ const inputDataPins = (node.inputs || []).filter(p => p.type !== 'exec');
+ const outputExecPins = (node.outputs || []).filter(p => p.type === 'exec' && !p.inHeader);
+ const outputDataPins = (node.outputs || []).filter(p => p.type !== 'exec' && !p.inHeader);
+
+ const hasBody = inputExecPins.length > 0 || inputDataPins.length > 0 ||
+ outputDataPins.length > 0 || outputExecPins.length > 0;
+
+ if (hasBody) {
+ html += `
`;
+
+ // Input exec pins first
+ inputExecPins.forEach(pin => {
+ const filled = pin.connected !== false;
+ html += `
`;
+ html += `${renderPinSvg(pin.type, filled)}`;
+ html += `${pin.label || ''}`;
+ html += `
`;
+ });
+
+ // Input data pins
+ inputDataPins.forEach(pin => {
+ const filled = pin.connected !== false;
+ html += `
`;
+ html += `${renderPinSvg(pin.type, filled)}`;
+ html += `${pin.label || ''}`;
+ if (pin.value !== undefined) html += `${pin.value}`;
+ html += `
`;
+ });
+
+ // Output data pins (pin first, then label - CSS row-reverse will flip them)
+ outputDataPins.forEach(pin => {
+ html += `
`;
+ html += `${renderPinSvg(pin.type)}`;
+ html += `${pin.label || ''}`;
+ html += `
`;
+ });
+
+ // Output exec pins
+ outputExecPins.forEach(pin => {
+ html += `
`;
+ html += `${renderPinSvg(pin.type)}`;
+ html += `${pin.label || ''}`;
+ html += `
`;
+ });
+
+ html += `
`;
+ }
+
+ html += `
`;
+ return html;
+ }
+
+ /**
+ * Setup drag-to-scroll for graph container
+ * Works with native overflow:auto scrolling
+ */
+ function setupDragScroll(container) {
+ let isDragging = false;
+ let startX = 0, startY = 0;
+ let scrollLeft = 0, scrollTop = 0;
+
+ container.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return;
+ isDragging = true;
+ startX = e.pageX - container.offsetLeft;
+ startY = e.pageY - container.offsetTop;
+ scrollLeft = container.scrollLeft;
+ scrollTop = container.scrollTop;
+ container.style.cursor = 'grabbing';
+ e.preventDefault();
+ });
+
+ container.addEventListener('mousemove', (e) => {
+ if (!isDragging) return;
+ const x = e.pageX - container.offsetLeft;
+ const y = e.pageY - container.offsetTop;
+ container.scrollLeft = scrollLeft - (x - startX);
+ container.scrollTop = scrollTop - (y - startY);
+ });
+
+ container.addEventListener('mouseup', () => {
+ isDragging = false;
+ container.style.cursor = 'grab';
+ });
+
+ container.addEventListener('mouseleave', () => {
+ isDragging = false;
+ container.style.cursor = 'grab';
+ });
+ }
+
+ function renderConnections(container, graphData) {
+ const svg = container.querySelector('.bp-connections');
+ if (!svg) return;
+
+ const content = container.querySelector('.bp-graph-content') || container;
+ const graphRect = content.getBoundingClientRect();
+
+ graphData.connections.forEach(c => {
+ const fromPin = container.querySelector(`[data-pin="${c.from}"]`);
+ const toPin = container.querySelector(`[data-pin="${c.to}"]`);
+ if (!fromPin || !toPin) return;
+
+ const fromRect = fromPin.getBoundingClientRect();
+ const toRect = toPin.getBoundingClientRect();
+
+ const x1 = fromRect.left - graphRect.left + fromRect.width / 2;
+ const y1 = fromRect.top - graphRect.top + fromRect.height / 2;
+ const x2 = toRect.left - graphRect.left + toRect.width / 2;
+ const y2 = toRect.top - graphRect.top + toRect.height / 2;
+
+ // Simple bezier curve
+ const dx = Math.abs(x2 - x1) * 0.5;
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ path.setAttribute('d', `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`);
+ path.setAttribute('class', `bp-conn ${c.type || 'exec'}`);
+ svg.appendChild(path);
+ });
+ }
+
+ function initBlueprintGraphs() {
+ document.querySelectorAll('.bp-graph[data-graph]').forEach(container => {
+ try {
+ const graphData = JSON.parse(container.dataset.graph);
+ if (!graphData.nodes || graphData.nodes.length === 0) {
+ console.warn('Blueprint graph has no nodes');
+ return;
+ }
+
+ // Get container width for layout calculation
+ let containerWidth = container.parentElement?.offsetWidth || 0;
+ if (containerWidth < 200) {
+ containerWidth = 650;
+ }
+
+ const { positions, sizes } = autoLayout(graphData, containerWidth - 30);
+
+ let maxX = 0, maxY = 0;
+ graphData.nodes.forEach(n => {
+ const pos = positions[n.id];
+ const size = sizes[n.id];
+ if (pos && size) {
+ maxX = Math.max(maxX, pos.x + size.width);
+ maxY = Math.max(maxY, pos.y + size.height);
+ }
+ });
+
+ // Add generous padding to ensure all nodes visible
+ maxX += 80;
+ maxY += 80;
+
+ // Set minimum height but allow natural expansion
+ const containerHeight = Math.max(maxY, 200);
+ container.style.minHeight = containerHeight + 'px';
+
+ let html = ``;
+ html += ``;
+ graphData.nodes.forEach(n => {
+ if (positions[n.id] && sizes[n.id]) {
+ html += renderNode(n, positions[n.id], sizes[n.id]);
+ }
+ });
+ html += `
`;
+ container.innerHTML = html;
+
+ // Setup drag-to-scroll
+ setupDragScroll(container);
+
+ requestAnimationFrame(() => renderConnections(container, graphData));
+ } catch (e) {
+ console.error('Blueprint graph error:', e);
+ }
+ });
+
+ // Legacy format
+ document.querySelectorAll('.bp-graph:not([data-graph])').forEach(graph => {
+ const nodes = graph.querySelectorAll('.bp-node');
+ let maxX = 0, maxY = 0;
+ nodes.forEach(node => {
+ const left = parseInt(node.style.left) || 0;
+ const top = parseInt(node.style.top) || 0;
+ const width = parseInt(node.style.width) || 150;
+ maxX = Math.max(maxX, left + width + 40);
+ maxY = Math.max(maxY, top + node.offsetHeight + 40);
+ });
+ // Don't set fixed width - let CSS handle it
+ graph.style.minHeight = Math.max(maxY, 120) + 'px';
+
+ const svg = graph.querySelector('.bp-connections');
+ if (!svg) return;
+ svg.setAttribute('width', maxX);
+ svg.setAttribute('height', Math.max(maxY, 120));
+
+ const conns = JSON.parse(graph.dataset.connections || '[]');
+ const graphRect = graph.getBoundingClientRect();
+
+ conns.forEach(c => {
+ const fromPin = graph.querySelector(`[data-pin="${c.from}"]`);
+ const toPin = graph.querySelector(`[data-pin="${c.to}"]`);
+ if (!fromPin || !toPin) return;
+
+ const fromRect = fromPin.getBoundingClientRect();
+ const toRect = toPin.getBoundingClientRect();
+
+ const x1 = fromRect.left - graphRect.left + fromRect.width / 2;
+ const y1 = fromRect.top - graphRect.top + fromRect.height / 2;
+ const x2 = toRect.left - graphRect.left + toRect.width / 2;
+ const y2 = toRect.top - graphRect.top + toRect.height / 2;
+
+ const dx = Math.abs(x2 - x1) * 0.5;
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ path.setAttribute('d', `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`);
+ path.setAttribute('class', `bp-conn ${c.type || 'exec'}`);
+ svg.appendChild(path);
+ });
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initBlueprintGraphs);
+ } else {
+ initBlueprintGraphs();
+ }
+})();
diff --git a/docs/src/content/docs/en/modules/blueprint/editor-guide.md b/docs/src/content/docs/en/modules/blueprint/editor-guide.md
index 856ca339..d6089de1 100644
--- a/docs/src/content/docs/en/modules/blueprint/editor-guide.md
+++ b/docs/src/content/docs/en/modules/blueprint/editor-guide.md
@@ -3,6 +3,8 @@ title: "Blueprint Editor User Guide"
description: "Complete guide for using the Cocos Creator Blueprint Visual Scripting Editor"
---
+
+
This guide covers how to use the Blueprint Visual Scripting Editor in Cocos Creator.
## Download & Installation
@@ -345,50 +347,244 @@ Blueprints are saved as JSON, compatible with `@esengine/blueprint` runtime:
Move entity every frame:
-```
-[Event Tick] ─Exec─→ [Get Self] ─Entity─→ [Get Component: Transform]
- │
- [Get Delta Time] ▼
- │ [Set Property: x]
- │ │
- [Multiply] ◄──────────────┘
- │
- └─ Speed: 100
-```
+
+
+
+
+
+
+
+
+ A
+
+
+
+ B (Speed)
+ 100
+
+
+
+ Result
+
+
+
+
+
+
+
+
+
+ Target
+
+
+
+ x
+
+
+
+
### Example 2: Health System
-Check death after taking damage:
+Check death after taking damage. `Event OnDamage` is a custom event node that can be triggered from code via `vm.triggerCustomEvent('OnDamage', { damage: 50 })`:
-```
-[On Damage Event] ─→ [Get Component: Health] ─→ [Get Property: current]
- │
- ▼
- [Subtract]
- │
- ▼
- [Set Property: current]
- │
- ▼
- ┌─ True ─→ [Destroy Self]
- [Branch]─┤
- └─ False ─→ (continue)
- ▲
- │
- [Less Or Equal]
- │
- current <= 0
-```
+
### Example 3: Delayed Spawning
Spawn an enemy every 2 seconds:
-```
-[Event BeginPlay] ─→ [Do N Times] ─Loop─→ [Delay: 2.0] ─→ [Create Entity: Enemy]
- │
- └─ N: 10
-```
+
+
+
+
+
+
+
+
+
+
+
+ N
+ 10
+
+
+
+
+ Index
+
+
+
+
+
+
+
+
+
+
+ Duration
+ 2.0
+
+
+
+
+
+
+
+
+
+
+ Name
+ "Enemy"
+
+
+
+
+ Entity
+
+
+
+
## Troubleshooting
diff --git a/docs/src/content/docs/modules/blueprint/editor-guide.md b/docs/src/content/docs/modules/blueprint/editor-guide.md
index 32ee2bc6..9ff713b3 100644
--- a/docs/src/content/docs/modules/blueprint/editor-guide.md
+++ b/docs/src/content/docs/modules/blueprint/editor-guide.md
@@ -3,6 +3,8 @@ title: "蓝图编辑器使用指南"
description: "Cocos Creator 蓝图可视化脚本编辑器完整使用教程"
---
+
+
本指南介绍如何在 Cocos Creator 中使用蓝图可视化脚本编辑器。
## 下载与安装
@@ -93,10 +95,37 @@ your-project/
| **Event EndPlay** | 蓝图停止时 | Exec |
**示例:游戏开始时打印消息**
-```
-[Event BeginPlay] ──Exec──→ [Print]
- └─ Message: "游戏开始!"
-```
+
+
+
+
+
+
+
+
+
+
+ Message
+ "游戏开始!"
+
+
+
+
### 实体节点 (Entity)
@@ -114,10 +143,52 @@ your-project/
| **Set Active** | 设置激活状态 | Exec, Entity, Active | Exec |
**示例:创建新实体**
-```
-[Event BeginPlay] ──→ [Create Entity] ──→ [Add Component]
- └─ Name: "Bullet" └─ Type: Transform
-```
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ "Bullet"
+
+
+
+
+ Entity
+
+
+
+
+
### 组件节点 (Component)
@@ -132,11 +203,50 @@ your-project/
| **Get/Set Property** | 获取/设置组件属性 |
**示例:修改 Transform 组件**
-```
-[Get Self] ─Entity─→ [Get Component: Transform] ─Component─→ [Set Property]
- ├─ Property: x
- └─ Value: 100
-```
+
+
+
+
+
+
+
+
+
+ Entity
+
+
+
+ Transform
+
+
+
+
+
+
+
+
+
+ Target
+
+
+
+ x
+ 100
+
+
+
+
### 流程控制节点 (Flow)
@@ -146,42 +256,161 @@ your-project/
条件判断,类似 if/else。
-```
- ┌─ True ──→ [DoSomething]
-[Branch]─┤
- └─ False ─→ [DoOtherThing]
-```
+
+
+
+
+
+
+
+
+ Condition
+
+
+
+
+
+
+
+
#### Sequence (序列)
按顺序执行多个分支。
-```
- ┌─ Then 0 ──→ [Step1]
-[Sequence]─┼─ Then 1 ──→ [Step2]
- └─ Then 2 ──→ [Step3]
-```
+
#### For Loop (循环)
循环执行指定次数。
-```
-[For Loop] ─Loop Body─→ [每次迭代执行]
- │
- └─ Completed ────→ [循环结束后执行]
-```
-
-| 输入 | 说明 |
-|------|------|
-| First Index | 起始索引 |
-| Last Index | 结束索引 |
-
-| 输出 | 说明 |
-|------|------|
-| Loop Body | 每次迭代执行 |
-| Index | 当前索引 |
-| Completed | 循环结束后执行 |
+
+
+
+
+
+
+
+
+ First Index
+ 0
+
+
+
+ Last Index
+ 10
+
+
+
+
+ Index
+
+
+
+
+
+
+
#### For Each (遍历)
@@ -212,10 +441,49 @@ your-project/
| **Get Time** | 获取运行总时间 | Number |
**示例:延迟 2 秒后执行**
-```
-[Event BeginPlay] ──→ [Delay] ──→ [Print]
- └─ Duration: 2.0 └─ "2秒后执行"
-```
+
+
+
+
+
+
+
+
+
+
+
+
+ Duration
+ 2.0
+
+
+
+
+
+
+
+
+
+
+ Msg
+ "2秒后执行"
+
+
+
+
### 数学节点 (Math)
@@ -345,50 +613,244 @@ your-project/
实现每帧移动实体:
-```
-[Event Tick] ─Exec─→ [Get Self] ─Entity─→ [Get Component: Transform]
- │
- [Get Delta Time] ▼
- │ [Set Property: x]
- │ │
- [Multiply] ◄──────────────┘
- │
- └─ Speed: 100
-```
+
+
+
+
+
+
+
+
+ A
+
+
+
+ B (Speed)
+ 100
+
+
+
+ Result
+
+
+
+
+
+
+
+
+
+ Target
+
+
+
+ x
+
+
+
+
### 示例 2:生命值系统
-受伤后检查死亡:
+受伤后检查死亡逻辑。`Event OnDamage` 是一个自定义事件节点,可以通过代码 `vm.triggerCustomEvent('OnDamage', { damage: 50 })` 触发:
-```
-[On Damage Event] ─→ [Get Component: Health] ─→ [Get Property: current]
- │
- ▼
- [Subtract]
- │
- ▼
- [Set Property: current]
- │
- ▼
- ┌─ True ─→ [Destroy Self]
- [Branch]─┤
- └─ False ─→ (继续)
- ▲
- │
- [Less Or Equal]
- │
- current <= 0
-```
+
### 示例 3:延迟生成
每 2 秒生成一个敌人:
-```
-[Event BeginPlay] ─→ [Do N Times] ─Loop─→ [Delay: 2.0] ─→ [Create Entity: Enemy]
- │
- └─ N: 10
-```
+
+
+
+
+
+
+
+
+
+
+
+ N
+ 10
+
+
+
+
+ Index
+
+
+
+
+
+
+
+
+
+
+ Duration
+ 2.0
+
+
+
+
+
+
+
+
+
+
+ Name
+ "Enemy"
+
+
+
+
+ Entity
+
+
+
+
## 常见问题
diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css
index 0c994a63..33416e10 100644
--- a/docs/src/styles/custom.css
+++ b/docs/src/styles/custom.css
@@ -236,3 +236,257 @@ nav.sidebar-content ul li a[aria-current="page"] {
font-size: 2rem;
}
}
+
+/* ==================== Blueprint Node Visualization ==================== */
+/* Matches the actual node-editor component styles */
+
+/* Graph Container - 使用固定像素坐标 */
+.bp-graph {
+ position: relative;
+ background: #141419;
+ background-image:
+ linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
+ background-size: 20px 20px;
+ border-radius: 8px;
+ margin: 1.5rem 0;
+ border: 1px solid #2a2a35;
+ overflow: auto;
+ cursor: grab;
+}
+
+.bp-graph:active {
+ cursor: grabbing;
+}
+
+/* Inner container for panning */
+.bp-graph-content {
+ position: relative;
+ transform-origin: 0 0;
+}
+
+/* SVG Connections Layer */
+.bp-connections {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ overflow: visible;
+}
+
+/* Node absolute positioning in graph */
+.bp-graph > .bp-node {
+ position: absolute;
+}
+
+/* Connection paths */
+.bp-conn {
+ fill: none;
+ stroke-width: 2px;
+ stroke-linecap: round;
+}
+
+.bp-conn.exec { stroke: #ffffff; stroke-width: 3px; }
+.bp-conn.float { stroke: #7ecd32; }
+.bp-conn.int { stroke: #1cc4c4; }
+.bp-conn.bool { stroke: #8c0000; }
+.bp-conn.string { stroke: #e060e0; }
+.bp-conn.object { stroke: #00a0e0; }
+.bp-conn.entity { stroke: #00a0e0; }
+.bp-conn.component { stroke: #7030c0; }
+.bp-conn.array { stroke: #7030c0; }
+.bp-conn.any { stroke: #707070; }
+
+/* ==================== Node Container ==================== */
+.bp-node {
+ position: relative;
+ min-width: 140px;
+ background: rgba(12, 12, 16, 0.95);
+ border: 1px solid rgba(40, 40, 50, 0.8);
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
+ font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
+ font-size: 11px;
+ flex-shrink: 0;
+ box-sizing: border-box;
+}
+
+/* ==================== Node Header ==================== */
+/* 与 node-editor 完全一致 */
+.bp-node-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 10px;
+ min-height: 28px;
+ border-radius: 5px 5px 0 0;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 500;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
+ position: relative;
+}
+
+/* Header icon - diamond for events */
+.bp-node-header-icon {
+ width: 8px;
+ height: 8px;
+ flex-shrink: 0;
+}
+
+.bp-node-header.event .bp-node-header-icon {
+ background: #fff;
+ transform: rotate(45deg);
+ border-radius: 1px;
+ box-shadow: 0 0 4px rgba(255, 255, 255, 0.5);
+}
+
+/* Header title */
+.bp-node-header-title {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Header exec pin (for event nodes) - at right edge */
+/* 与 node-editor 完全一致 */
+.bp-header-exec {
+ width: 12px;
+ height: 12px;
+ margin-left: auto;
+ flex-shrink: 0;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.bp-header-exec svg {
+ display: block;
+}
+
+/* Category Colors */
+.bp-node-header.event {
+ background: linear-gradient(180deg, #b81c1c 0%, #6b1010 100%);
+}
+
+.bp-node-header.function {
+ background: linear-gradient(180deg, #1b6eb5 0%, #0d3d66 100%);
+}
+
+.bp-node-header.pure {
+ background: linear-gradient(180deg, #3d8b3d 0%, #1f5a1f 100%);
+}
+
+.bp-node-header.flow {
+ background: linear-gradient(180deg, #4a4a4a 0%, #2a2a2a 100%);
+}
+
+.bp-node-header.variable {
+ background: linear-gradient(180deg, #7b3d9b 0%, #4a1f66 100%);
+}
+
+.bp-node-header.debug {
+ background: linear-gradient(180deg, #6a6a6a 0%, #3a3a3a 100%);
+}
+
+.bp-node-header.time {
+ background: linear-gradient(180deg, #1cc4c4 0%, #0d7070 100%);
+}
+
+.bp-node-header.math {
+ background: linear-gradient(180deg, #7ecd32 0%, #4a7a1e 100%);
+}
+
+/* ==================== Node Body ==================== */
+/* 与 node-editor 完全一致 */
+.bp-node-body {
+ padding: 6px 0;
+ background: rgba(8, 8, 12, 0.95);
+ border-radius: 0 0 5px 5px;
+}
+
+/* ==================== Pin Row ==================== */
+/* 与 node-editor 完全一致 */
+.bp-pin-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 20px;
+ padding: 1px 0;
+ position: relative;
+ box-sizing: border-box;
+}
+
+.bp-pin-row.input {
+ flex-direction: row;
+ justify-content: flex-start;
+ padding-left: 10px;
+ padding-right: 16px;
+}
+
+.bp-pin-row.output {
+ flex-direction: row-reverse;
+ justify-content: flex-start;
+ padding-right: 10px;
+ padding-left: 16px;
+}
+
+/* ==================== Pin (SVG-based) ==================== */
+.bp-pin {
+ width: 12px;
+ height: 12px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.bp-pin svg {
+ display: block;
+}
+
+/* Pin Label */
+.bp-pin-label {
+ color: #b0b0b0;
+ font-size: 11px;
+ white-space: nowrap;
+ line-height: 1;
+}
+
+.bp-pin-row.output .bp-pin-label {
+ text-align: right;
+}
+
+/* Pin Value */
+.bp-pin-value {
+ color: #7ecd32;
+ font-size: 10px;
+ margin-left: auto;
+ font-family: 'Consolas', 'Monaco', monospace;
+}
+
+.bp-pin-row.output .bp-pin-value {
+ margin-left: 0;
+ margin-right: auto;
+}
+
+/* ==================== Legend ==================== */
+.bp-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ padding: 10px;
+ background: rgba(20, 20, 25, 0.8);
+ border-radius: 6px;
+ margin: 1rem 0;
+}
+
+.bp-legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: #a0a0a0;
+}