Compare commits
33 Commits
@esengine/
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6aef13d93 | ||
|
|
470abb8750 | ||
|
|
2f95758911 | ||
|
|
2e9f36b656 | ||
|
|
c188a36f2b | ||
|
|
50681553b5 | ||
|
|
4e66bd8e2b | ||
|
|
7caa69a22e | ||
|
|
5a5daf7565 | ||
|
|
3415737fcc | ||
|
|
876312deb2 | ||
|
|
a790fc9e92 | ||
|
|
fa593a3c69 | ||
|
|
e7d95dfdaf | ||
|
|
bffe90b6a1 | ||
|
|
e90a42b1c9 | ||
|
|
30173f0764 | ||
|
|
12da6bd609 | ||
|
|
6b5b4efa72 | ||
|
|
51334dfc50 | ||
|
|
2035355e22 | ||
|
|
9e5f037d5d | ||
|
|
43be62b4cb | ||
|
|
c902dd7291 | ||
|
|
0d33cf0097 | ||
|
|
45de62e453 | ||
|
|
b983cbf87a | ||
|
|
34583b23af | ||
|
|
f2c3a24404 | ||
|
|
3bfb8a1c9b | ||
|
|
2ee8d87647 | ||
|
|
2d537dc10c | ||
|
|
c2acd14fce |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -63,9 +63,9 @@ jobs:
|
||||
- name: Build framework packages
|
||||
run: |
|
||||
pnpm --filter @esengine/ecs-framework build
|
||||
pnpm --filter @esengine/blueprint build
|
||||
pnpm --filter @esengine/ecs-framework-math build
|
||||
pnpm --filter @esengine/behavior-tree build
|
||||
pnpm --filter @esengine/blueprint build
|
||||
pnpm --filter @esengine/fsm build
|
||||
pnpm --filter @esengine/timer build
|
||||
pnpm --filter @esengine/spatial build
|
||||
|
||||
2
.github/workflows/release-changesets.yml
vendored
2
.github/workflows/release-changesets.yml
vendored
@@ -45,9 +45,9 @@ jobs:
|
||||
run: |
|
||||
# Only build packages managed by Changesets (not in ignore list)
|
||||
pnpm --filter "@esengine/ecs-framework" build
|
||||
pnpm --filter "@esengine/blueprint" build
|
||||
pnpm --filter "@esengine/ecs-framework-math" build
|
||||
pnpm --filter "@esengine/behavior-tree" build
|
||||
pnpm --filter "@esengine/blueprint" build
|
||||
pnpm --filter "@esengine/fsm" build
|
||||
pnpm --filter "@esengine/timer" build
|
||||
pnpm --filter "@esengine/spatial" build
|
||||
|
||||
@@ -3,8 +3,12 @@ import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import vue from '@astrojs/vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
rehypePlugins: [rehypeRaw],
|
||||
},
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'ESEngine',
|
||||
@@ -96,6 +100,8 @@ export default defineConfig({
|
||||
{ label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{ label: '场景管理器', slug: 'guide/scene-manager', translations: { en: 'SceneManager' } },
|
||||
{ label: '持久实体', slug: 'guide/persistent-entity', translations: { en: 'Persistent Entity' } },
|
||||
{
|
||||
label: '序列化',
|
||||
translations: { en: 'Serialization' },
|
||||
@@ -232,6 +238,8 @@ export default defineConfig({
|
||||
translations: { en: 'Blueprint' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
|
||||
{ label: '编辑器使用指南', slug: 'modules/blueprint/editor-guide', translations: { en: 'Editor Guide' } },
|
||||
{ label: 'Cocos Creator 编辑器', slug: 'modules/blueprint/cocos-editor', translations: { en: 'Cocos Creator Editor' } },
|
||||
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
|
||||
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
|
||||
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
|
||||
@@ -239,6 +247,14 @@ export default defineConfig({
|
||||
{ label: '实际示例', slug: 'modules/blueprint/examples', translations: { en: 'Examples' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数学库',
|
||||
translations: { en: 'Math' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/math', translations: { en: 'Overview' } },
|
||||
{ label: '蓝图节点', slug: 'modules/math/blueprint-nodes', translations: { en: 'Blueprint Nodes' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '程序生成',
|
||||
translations: { en: 'Procgen' },
|
||||
@@ -270,7 +286,9 @@ export default defineConfig({
|
||||
{ label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
|
||||
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
|
||||
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
|
||||
{ label: '分布式房间', slug: 'modules/network/distributed', translations: { en: 'Distributed Rooms' } },
|
||||
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
|
||||
{ label: '定点数同步', slug: 'modules/network/fixed-point', translations: { en: 'Fixed-Point Sync' } },
|
||||
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@astrojs/vue": "^5.1.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.6.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"sharp": "^0.34.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.26"
|
||||
|
||||
462
docs/public/js/blueprint-graph.js
Normal file
462
docs/public/js/blueprint-graph.js
Normal file
@@ -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 `<svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="${filled ? '#fff' : 'none'}" stroke="${filled ? 'none' : '#fff'}" stroke-width="2"/></svg>`;
|
||||
}
|
||||
return `<svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="${filled ? color : 'none'}" stroke="${filled ? 'none' : color}" stroke-width="2"/></svg>`;
|
||||
}
|
||||
|
||||
function renderNode(node, position, size) {
|
||||
const isEvent = node.category === 'event';
|
||||
const headerClass = HEADER_CLASSES[node.category] || 'function';
|
||||
|
||||
let html = `<div class="bp-node" style="left: ${position.x}px; top: ${position.y}px; width: ${size.width}px;">`;
|
||||
html += `<div class="bp-node-header ${headerClass}">`;
|
||||
if (isEvent) html += `<span class="bp-node-header-icon"></span>`;
|
||||
html += `<span class="bp-node-header-title">${node.title}</span>`;
|
||||
|
||||
const headerExec = node.outputs && node.outputs.find(p => p.type === 'exec' && p.inHeader);
|
||||
if (headerExec) {
|
||||
html += `<span class="bp-header-exec" data-pin="${headerExec.id}">${renderPinSvg('exec')}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
// 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 += `<div class="bp-node-body">`;
|
||||
|
||||
// Input exec pins first
|
||||
inputExecPins.forEach(pin => {
|
||||
const filled = pin.connected !== false;
|
||||
html += `<div class="bp-pin-row input">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type, filled)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// Input data pins
|
||||
inputDataPins.forEach(pin => {
|
||||
const filled = pin.connected !== false;
|
||||
html += `<div class="bp-pin-row input">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type, filled)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
if (pin.value !== undefined) html += `<span class="bp-pin-value">${pin.value}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// Output data pins (pin first, then label - CSS row-reverse will flip them)
|
||||
outputDataPins.forEach(pin => {
|
||||
html += `<div class="bp-pin-row output">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// Output exec pins
|
||||
outputExecPins.forEach(pin => {
|
||||
html += `<div class="bp-pin-row output">`;
|
||||
html += `<span class="bp-pin" data-pin="${pin.id}">${renderPinSvg(pin.type)}</span>`;
|
||||
html += `<span class="bp-pin-label">${pin.label || ''}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
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 = `<div class="bp-graph-content" style="width:${maxX}px;height:${maxY}px;position:relative;">`;
|
||||
html += `<svg class="bp-connections" width="${maxX}" height="${maxY}"></svg>`;
|
||||
graphData.nodes.forEach(n => {
|
||||
if (positions[n.id] && sizes[n.id]) {
|
||||
html += renderNode(n, positions[n.id], sizes[n.id]);
|
||||
}
|
||||
});
|
||||
html += `</div>`;
|
||||
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();
|
||||
}
|
||||
})();
|
||||
@@ -18,3 +18,6 @@ import Default from '@astrojs/starlight/components/Head.astro';
|
||||
document.documentElement.dataset.theme = 'dark';
|
||||
localStorage.setItem('starlight-theme', 'dark');
|
||||
</script>
|
||||
|
||||
<!-- Blueprint graph visualization -->
|
||||
<script is:inline src="/js/blueprint-graph.js"></script>
|
||||
|
||||
@@ -359,6 +359,5 @@ class GoodScene extends Scene {
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Scene](./scene) - Learn the basics of scenes
|
||||
- [SceneManager](./scene-manager) - Learn about scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about multi-world management
|
||||
- [Scene](/en/guide/scene/) - Learn the basics of scenes
|
||||
- [SceneManager](/en/guide/scene-manager/) - Learn about scene transitions
|
||||
|
||||
@@ -16,7 +16,7 @@ The ECS framework provides a platform adapter interface that allows users to imp
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
### [Browser Adapter](./platform-adapter/browser/)
|
||||
### [Browser Adapter](/en/guide/platform-adapter/browser/)
|
||||
|
||||
Supports all modern browser environments, including Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
@@ -30,7 +30,7 @@ Supports all modern browser environments, including Chrome, Firefox, Safari, Edg
|
||||
|
||||
---
|
||||
|
||||
### [WeChat Mini Game Adapter](./platform-adapter/wechat-minigame/)
|
||||
### [WeChat Mini Game Adapter](/en/guide/platform-adapter/wechat-minigame/)
|
||||
|
||||
Designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
@@ -44,7 +44,7 @@ Designed specifically for the WeChat Mini Game environment, handling special res
|
||||
|
||||
---
|
||||
|
||||
### [Node.js Adapter](./platform-adapter/nodejs/)
|
||||
### [Node.js Adapter](/en/guide/platform-adapter/nodejs/)
|
||||
|
||||
Provides support for Node.js server environments, suitable for game servers and compute servers.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ SceneManager is suitable for:
|
||||
- Automatic ECS fluent API management
|
||||
- Automatic scene lifecycle handling
|
||||
- Integrated with Core, auto-updated
|
||||
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+)
|
||||
- Supports [Persistent Entity](/en/guide/persistent-entity/) migration across scenes (v2.3.0+)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
@@ -434,7 +434,6 @@ Core (Global Services)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
|
||||
- [Persistent Entity](/en/guide/persistent-entity/) - Learn how to keep entities across scene transitions
|
||||
|
||||
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.
|
||||
|
||||
@@ -100,6 +100,6 @@ console.log('Current state:', runtime.state);
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Core Concepts](./core-concepts/) - Understand nodes and execution
|
||||
- [Custom Actions](./custom-actions/) - Create your own nodes
|
||||
- [Editor Guide](./editor-guide/) - Visual tree creation
|
||||
- [Core Concepts](/en/modules/behavior-tree/core-concepts/) - Understand nodes and execution
|
||||
- [Custom Actions](/en/modules/behavior-tree/custom-actions/) - Create your own nodes
|
||||
- [Editor Guide](/en/modules/behavior-tree/editor-guide/) - Visual tree creation
|
||||
|
||||
315
docs/src/content/docs/en/modules/blueprint/cocos-editor.md
Normal file
315
docs/src/content/docs/en/modules/blueprint/cocos-editor.md
Normal file
@@ -0,0 +1,315 @@
|
||||
---
|
||||
title: "Cocos Creator Blueprint Editor"
|
||||
description: "Using the blueprint visual scripting system in Cocos Creator"
|
||||
---
|
||||
|
||||
This document explains how to install and use the blueprint visual scripting editor extension in Cocos Creator projects.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Download Extension
|
||||
|
||||
Download the `cocos-node-editor.zip` extension package from the release page.
|
||||
|
||||
### 2. Import Extension
|
||||
|
||||
1. Open Cocos Creator
|
||||
2. Go to **Extensions → Extension Manager**
|
||||
3. Click the **Import Extension** button
|
||||
4. Select the downloaded `cocos-node-editor.zip` file
|
||||
5. Enable the extension after importing
|
||||
|
||||
## Opening the Blueprint Editor
|
||||
|
||||
Open the blueprint editor panel via menu **Panel → Node Editor**.
|
||||
|
||||
### First Launch - Install Dependencies
|
||||
|
||||
When opening the panel for the first time, the plugin will check if `@esengine/blueprint` is installed in your project. If not installed, it will display **"Missing required dependencies"** prompt. Click the **"Install Dependencies"** button to install automatically.
|
||||
|
||||
## Editor Interface
|
||||
|
||||
### Toolbar
|
||||
|
||||
| Button | Shortcut | Function |
|
||||
|--------|----------|----------|
|
||||
| New | - | Create empty blueprint |
|
||||
| Load | - | Load blueprint from file |
|
||||
| Save | `Ctrl+S` | Save blueprint to file |
|
||||
| Undo | `Ctrl+Z` | Undo last operation |
|
||||
| Redo | `Ctrl+Shift+Z` | Redo operation |
|
||||
| Cut | `Ctrl+X` | Cut selected nodes |
|
||||
| Copy | `Ctrl+C` | Copy selected nodes |
|
||||
| Paste | `Ctrl+V` | Paste nodes |
|
||||
| Delete | `Delete` | Delete selected items |
|
||||
| Rescan | - | Rescan project for blueprint nodes |
|
||||
|
||||
### Canvas Operations
|
||||
|
||||
- **Right-click on canvas**: Open node addition menu
|
||||
- **Drag nodes**: Move node position
|
||||
- **Click node**: Select node
|
||||
- **Ctrl+Click**: Multi-select nodes
|
||||
- **Drag pin to pin**: Create connection
|
||||
- **Scroll wheel**: Zoom canvas
|
||||
- **Middle-click drag**: Pan canvas
|
||||
|
||||
### Node Menu
|
||||
|
||||
Right-clicking on canvas shows the node menu:
|
||||
|
||||
- Search box at top for quick node search
|
||||
- Nodes grouped by category
|
||||
- Press `Enter` to quickly add first search result
|
||||
- Press `Esc` to close menu
|
||||
|
||||
## Blueprint File Format
|
||||
|
||||
Blueprints are saved as `.blueprint.json` files, fully compatible with runtime:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "blueprint",
|
||||
"metadata": {
|
||||
"name": "My Blueprint",
|
||||
"createdAt": 1704307200000,
|
||||
"modifiedAt": 1704307200000
|
||||
},
|
||||
"variables": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-1",
|
||||
"type": "PrintString",
|
||||
"position": { "x": 100, "y": 200 },
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "conn-1",
|
||||
"fromNodeId": "node-1",
|
||||
"fromPin": "exec",
|
||||
"toNodeId": "node-2",
|
||||
"toPin": "exec"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Running Blueprints in Game
|
||||
|
||||
The `@esengine/blueprint` package provides complete ECS integration, including `BlueprintComponent` and `BlueprintSystem` ready to use.
|
||||
|
||||
### 1. Add Blueprint System to Scene
|
||||
|
||||
```typescript
|
||||
import { BlueprintSystem } from '@esengine/blueprint';
|
||||
|
||||
// Add blueprint system during scene initialization
|
||||
scene.addSystem(new BlueprintSystem());
|
||||
```
|
||||
|
||||
### 2. Load Blueprint and Add to Entity
|
||||
|
||||
```typescript
|
||||
import { resources, JsonAsset } from 'cc';
|
||||
import { BlueprintComponent, validateBlueprintAsset, BlueprintAsset } from '@esengine/blueprint';
|
||||
|
||||
// Load blueprint asset
|
||||
async function loadBlueprint(path: string): Promise<BlueprintAsset | null> {
|
||||
return new Promise((resolve) => {
|
||||
resources.load(path, JsonAsset, (err, asset) => {
|
||||
if (err || !asset) {
|
||||
console.error('Failed to load blueprint:', err);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = asset.json;
|
||||
if (validateBlueprintAsset(data)) {
|
||||
resolve(data as BlueprintAsset);
|
||||
} else {
|
||||
console.error('Invalid blueprint format');
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create entity with blueprint
|
||||
async function createBlueprintEntity(scene: IScene, blueprintPath: string): Promise<Entity> {
|
||||
const entity = scene.createEntity('BlueprintEntity');
|
||||
|
||||
const bpComponent = entity.addComponent(BlueprintComponent);
|
||||
bpComponent.blueprintPath = blueprintPath;
|
||||
bpComponent.blueprintAsset = await loadBlueprint(blueprintPath);
|
||||
|
||||
return entity;
|
||||
}
|
||||
```
|
||||
|
||||
### BlueprintComponent Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `blueprintAsset` | `BlueprintAsset \| null` | Blueprint asset data |
|
||||
| `blueprintPath` | `string` | Blueprint asset path (for serialization) |
|
||||
| `autoStart` | `boolean` | Auto-start execution (default `true`) |
|
||||
| `debug` | `boolean` | Enable debug mode |
|
||||
|
||||
### BlueprintComponent Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `start()` | Manually start blueprint execution |
|
||||
| `stop()` | Stop blueprint execution |
|
||||
| `cleanup()` | Cleanup blueprint resources |
|
||||
|
||||
## Creating Custom Nodes
|
||||
|
||||
### Using Decorators for Components
|
||||
|
||||
Use decorators to automatically generate blueprint nodes from components:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
|
||||
|
||||
@ECSComponent('Health')
|
||||
@BlueprintExpose({ displayName: 'Health Component' })
|
||||
export class HealthComponent extends Component {
|
||||
@BlueprintProperty({ displayName: 'Current Health', category: 'number' })
|
||||
current: number = 100;
|
||||
|
||||
@BlueprintProperty({ displayName: 'Max Health', category: 'number' })
|
||||
max: number = 100;
|
||||
|
||||
@BlueprintMethod({ displayName: 'Heal', isExec: true })
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.current + amount, this.max);
|
||||
}
|
||||
|
||||
@BlueprintMethod({ displayName: 'Take Damage', isExec: true })
|
||||
takeDamage(amount: number): void {
|
||||
this.current = Math.max(this.current - amount, 0);
|
||||
}
|
||||
|
||||
@BlueprintMethod({ displayName: 'Is Dead' })
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Register Component Nodes
|
||||
|
||||
```typescript
|
||||
import { registerAllComponentNodes } from '@esengine/blueprint';
|
||||
|
||||
// Register all decorated components at application startup
|
||||
registerAllComponentNodes();
|
||||
```
|
||||
|
||||
### Manual Node Definition (Advanced)
|
||||
|
||||
For fully custom node logic:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BlueprintNodeTemplate,
|
||||
INodeExecutor,
|
||||
RegisterNode,
|
||||
ExecutionContext,
|
||||
ExecutionResult
|
||||
} from '@esengine/blueprint';
|
||||
|
||||
const MyNodeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'MyCustomNode',
|
||||
title: 'My Custom Node',
|
||||
category: 'custom',
|
||||
description: 'Custom node example',
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
|
||||
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
|
||||
{ name: 'result', type: 'number', direction: 'output' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = context.getInput<number>(node.id, 'value');
|
||||
return {
|
||||
outputs: { result: value * 2 },
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Node Categories
|
||||
|
||||
| Category | Description | Color |
|
||||
|----------|-------------|-------|
|
||||
| `event` | Event nodes | Red |
|
||||
| `flow` | Flow control | Gray |
|
||||
| `entity` | Entity operations | Blue |
|
||||
| `component` | Component access | Cyan |
|
||||
| `math` | Math operations | Green |
|
||||
| `logic` | Logic operations | Red |
|
||||
| `variable` | Variable access | Purple |
|
||||
| `time` | Time utilities | Cyan |
|
||||
| `debug` | Debug utilities | Gray |
|
||||
| `custom` | Custom nodes | Blue-gray |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **File Organization**
|
||||
- Place blueprint files in `assets/blueprints/` directory
|
||||
- Use meaningful file names like `player-controller.blueprint.json`
|
||||
|
||||
2. **Component Design**
|
||||
- Use `@BlueprintExpose` to mark components that should be exposed to blueprints
|
||||
- Provide clear `displayName` for properties and methods
|
||||
- Mark execution methods with `isExec: true`
|
||||
|
||||
3. **Performance Considerations**
|
||||
- Avoid heavy computation in Tick events
|
||||
- Use variables to cache intermediate results
|
||||
- Pure function nodes automatically cache outputs
|
||||
|
||||
4. **Debugging Tips**
|
||||
- Use Print nodes to output intermediate values
|
||||
- Enable `vm.debug = true` to view execution logs
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: Node menu is empty?
|
||||
|
||||
A: Click the **Rescan** button to scan for blueprint node classes in your project. Make sure you have called `registerAllComponentNodes()`.
|
||||
|
||||
### Q: Blueprint doesn't execute?
|
||||
|
||||
A: Check:
|
||||
1. Entity has `BlueprintComponent` added
|
||||
2. `BlueprintExecutionSystem` is registered to scene
|
||||
3. `blueprintAsset` is correctly loaded
|
||||
4. `autoStart` is `true`
|
||||
|
||||
### Q: How to trigger custom events?
|
||||
|
||||
A: Trigger through VM:
|
||||
```typescript
|
||||
const bp = entity.getComponent(BlueprintComponent);
|
||||
bp.vm?.triggerCustomEvent('OnPickup', { item: itemEntity });
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Blueprint Runtime API](/en/modules/blueprint/) - BlueprintVM and core API
|
||||
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Detailed node creation guide
|
||||
- [Built-in Nodes](/en/modules/blueprint/nodes) - Built-in node reference
|
||||
@@ -3,6 +3,164 @@ title: "Custom Nodes"
|
||||
description: "Creating custom blueprint nodes"
|
||||
---
|
||||
|
||||
## Blueprint Decorators
|
||||
|
||||
Use decorators to quickly expose ECS components as blueprint nodes.
|
||||
|
||||
### @BlueprintComponent
|
||||
|
||||
Mark a component class as blueprint-enabled:
|
||||
|
||||
```typescript
|
||||
import { BlueprintComponent, BlueprintProperty } from '@esengine/blueprint';
|
||||
|
||||
@BlueprintComponent({
|
||||
title: 'Player Controller',
|
||||
category: 'gameplay',
|
||||
color: '#4a90d9',
|
||||
description: 'Controls player movement and interaction'
|
||||
})
|
||||
class PlayerController extends Component {
|
||||
@BlueprintProperty({ displayName: 'Move Speed' })
|
||||
speed: number = 100;
|
||||
|
||||
@BlueprintProperty({ displayName: 'Jump Height' })
|
||||
jumpHeight: number = 200;
|
||||
}
|
||||
```
|
||||
|
||||
### @BlueprintProperty
|
||||
|
||||
Expose component properties as node inputs:
|
||||
|
||||
```typescript
|
||||
@BlueprintProperty({
|
||||
displayName: 'Health',
|
||||
description: 'Current health value',
|
||||
isInput: true,
|
||||
isOutput: true
|
||||
})
|
||||
health: number = 100;
|
||||
```
|
||||
|
||||
### @BlueprintArray
|
||||
|
||||
For array type properties, supports editing complex object arrays:
|
||||
|
||||
```typescript
|
||||
import { BlueprintArray, Schema } from '@esengine/blueprint';
|
||||
|
||||
interface Waypoint {
|
||||
position: { x: number; y: number };
|
||||
waitTime: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
@BlueprintComponent({
|
||||
title: 'Patrol Path',
|
||||
category: 'ai'
|
||||
})
|
||||
class PatrolPath extends Component {
|
||||
@BlueprintArray({
|
||||
displayName: 'Waypoints',
|
||||
description: 'Points along the patrol path',
|
||||
itemSchema: Schema.object({
|
||||
position: Schema.vector2({ defaultValue: { x: 0, y: 0 } }),
|
||||
waitTime: Schema.float({ min: 0, max: 10, defaultValue: 1.0 }),
|
||||
speed: Schema.float({ min: 0, max: 500, defaultValue: 100 })
|
||||
}),
|
||||
reorderable: true,
|
||||
exposeElementPorts: true,
|
||||
portNameTemplate: 'Waypoint {index1}'
|
||||
})
|
||||
waypoints: Waypoint[] = [];
|
||||
}
|
||||
```
|
||||
|
||||
## Schema Type System
|
||||
|
||||
Schema defines type information for complex data structures, enabling the editor to automatically generate corresponding UI.
|
||||
|
||||
### Primitive Types
|
||||
|
||||
```typescript
|
||||
import { Schema } from '@esengine/blueprint';
|
||||
|
||||
// Number types
|
||||
Schema.float({ min: 0, max: 100, defaultValue: 50, step: 0.1 })
|
||||
Schema.int({ min: 0, max: 10, defaultValue: 5 })
|
||||
|
||||
// String
|
||||
Schema.string({ defaultValue: 'Hello', multiline: false, placeholder: 'Enter text...' })
|
||||
|
||||
// Boolean
|
||||
Schema.boolean({ defaultValue: true })
|
||||
|
||||
// Vectors
|
||||
Schema.vector2({ defaultValue: { x: 0, y: 0 } })
|
||||
Schema.vector3({ defaultValue: { x: 0, y: 0, z: 0 } })
|
||||
```
|
||||
|
||||
### Composite Types
|
||||
|
||||
```typescript
|
||||
// Object
|
||||
Schema.object({
|
||||
name: Schema.string({ defaultValue: '' }),
|
||||
health: Schema.float({ min: 0, max: 100 }),
|
||||
position: Schema.vector2()
|
||||
})
|
||||
|
||||
// Array
|
||||
Schema.array({
|
||||
items: Schema.float(),
|
||||
minItems: 0,
|
||||
maxItems: 10
|
||||
})
|
||||
|
||||
// Enum
|
||||
Schema.enum({
|
||||
options: ['idle', 'walk', 'run', 'jump'],
|
||||
defaultValue: 'idle'
|
||||
})
|
||||
|
||||
// Reference
|
||||
Schema.ref({ refType: 'entity' })
|
||||
Schema.ref({ refType: 'asset', assetType: 'texture' })
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
|
||||
```typescript
|
||||
@BlueprintComponent({ title: 'Enemy Config', category: 'ai' })
|
||||
class EnemyConfig extends Component {
|
||||
@BlueprintArray({
|
||||
displayName: 'Attack Patterns',
|
||||
itemSchema: Schema.object({
|
||||
name: Schema.string({ defaultValue: 'Basic Attack' }),
|
||||
damage: Schema.float({ min: 0, max: 100, defaultValue: 10 }),
|
||||
cooldown: Schema.float({ min: 0, max: 10, defaultValue: 1 }),
|
||||
range: Schema.float({ min: 0, max: 500, defaultValue: 50 }),
|
||||
animation: Schema.string({ defaultValue: 'attack_01' })
|
||||
}),
|
||||
reorderable: true
|
||||
})
|
||||
attackPatterns: AttackPattern[] = [];
|
||||
|
||||
@BlueprintProperty({
|
||||
displayName: 'Patrol Area',
|
||||
schema: Schema.object({
|
||||
center: Schema.vector2(),
|
||||
radius: Schema.float({ min: 0, defaultValue: 100 })
|
||||
})
|
||||
})
|
||||
patrolArea: { center: { x: number; y: number }; radius: number } = {
|
||||
center: { x: 0, y: 0 },
|
||||
radius: 100
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Defining Node Template
|
||||
|
||||
```typescript
|
||||
@@ -33,16 +191,11 @@ import { INodeExecutor, RegisterNode, BlueprintNode, ExecutionContext, Execution
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
// Get input (using evaluateInput)
|
||||
const value = context.evaluateInput(node.id, 'value', 0) as number;
|
||||
|
||||
// Execute logic
|
||||
const result = value * 2;
|
||||
|
||||
// Return result
|
||||
return {
|
||||
outputs: { result },
|
||||
nextExec: 'exec' // Continue execution
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -64,19 +217,10 @@ NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
|
||||
```typescript
|
||||
import { NodeRegistry } from '@esengine/blueprint';
|
||||
|
||||
// Get singleton
|
||||
const registry = NodeRegistry.instance;
|
||||
|
||||
// Get all templates
|
||||
const allTemplates = registry.getAllTemplates();
|
||||
|
||||
// Get by category
|
||||
const mathNodes = registry.getTemplatesByCategory('math');
|
||||
|
||||
// Search nodes
|
||||
const results = registry.searchTemplates('add');
|
||||
|
||||
// Check existence
|
||||
if (registry.has('MyCustomNode')) { ... }
|
||||
```
|
||||
|
||||
@@ -89,7 +233,7 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDistance',
|
||||
title: 'Get Distance',
|
||||
category: 'math',
|
||||
isPure: true, // Mark as pure node
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'vector2', direction: 'input' },
|
||||
{ name: 'b', type: 'vector2', direction: 'input' }
|
||||
@@ -99,59 +243,3 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## Example: ECS Component Operation Node
|
||||
|
||||
```typescript
|
||||
import type { Entity } from '@esengine/ecs-framework';
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
|
||||
import { ExecutionContext, ExecutionResult } from '@esengine/blueprint';
|
||||
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
|
||||
|
||||
// Custom heal node
|
||||
const HealEntityTemplate: BlueprintNodeTemplate = {
|
||||
type: 'HealEntity',
|
||||
title: 'Heal Entity',
|
||||
category: 'gameplay',
|
||||
color: '#22aa22',
|
||||
description: 'Heal an entity with HealthComponent',
|
||||
keywords: ['heal', 'health', 'restore'],
|
||||
menuPath: ['Gameplay', 'Combat', 'Heal Entity'],
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', displayName: '' },
|
||||
{ name: 'entity', type: 'entity', displayName: 'Target' },
|
||||
{ name: 'amount', type: 'float', displayName: 'Amount', defaultValue: 10 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', displayName: '' },
|
||||
{ name: 'newHealth', type: 'float', displayName: 'New Health' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(HealEntityTemplate)
|
||||
class HealEntityExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
|
||||
const amount = context.evaluateInput(node.id, 'amount', 10) as number;
|
||||
|
||||
if (!entity || entity.isDestroyed) {
|
||||
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
|
||||
}
|
||||
|
||||
// Get HealthComponent
|
||||
const health = entity.components.find(c =>
|
||||
(c.constructor as any).__componentName__ === 'Health'
|
||||
) as any;
|
||||
|
||||
if (health) {
|
||||
health.current = Math.min(health.current + amount, health.max);
|
||||
return {
|
||||
outputs: { newHealth: health.current },
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
|
||||
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
610
docs/src/content/docs/en/modules/blueprint/editor-guide.md
Normal file
610
docs/src/content/docs/en/modules/blueprint/editor-guide.md
Normal file
@@ -0,0 +1,610 @@
|
||||
---
|
||||
title: "Blueprint Editor User Guide"
|
||||
description: "Complete guide for using the Cocos Creator Blueprint Visual Scripting Editor"
|
||||
---
|
||||
|
||||
<script src="/js/blueprint-graph.js"></script>
|
||||
|
||||
This guide covers how to use the Blueprint Visual Scripting Editor in Cocos Creator.
|
||||
|
||||
## Download & Installation
|
||||
|
||||
### Download
|
||||
|
||||
Download the latest version from GitHub Release (Free):
|
||||
|
||||
**[Download Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
|
||||
|
||||
> QQ Group: **481923584** | Website: [esengine.cn](https://esengine.cn/)
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. Extract `cocos-node-editor.zip` to your project's `extensions` directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── assets/
|
||||
├── extensions/
|
||||
│ └── cocos-node-editor/ ← Extract here
|
||||
└── ...
|
||||
```
|
||||
|
||||
2. Restart Cocos Creator
|
||||
|
||||
3. Confirm the plugin is enabled via **Extensions → Extension Manager**
|
||||
|
||||
4. Open the editor via **Panel → Node Editor**
|
||||
|
||||
## Interface Overview
|
||||
|
||||
- **Toolbar** - Located at the top, contains New, Open, Save, Undo, Redo operations
|
||||
- **Variables Panel** - Located at the top-left, for defining and managing blueprint variables
|
||||
- **Canvas Area** - Main area for placing and connecting nodes
|
||||
- **Node Menu** - Right-click on empty canvas to open, lists all available nodes by category
|
||||
|
||||
## Canvas Operations
|
||||
|
||||
| Operation | Method |
|
||||
|-----------|--------|
|
||||
| Pan canvas | Middle-click drag / Alt + Left-click drag |
|
||||
| Zoom canvas | Mouse wheel |
|
||||
| Open node menu | Right-click on empty space |
|
||||
| Box select nodes | Drag on empty canvas |
|
||||
| Additive select | Ctrl + Drag |
|
||||
| Delete selected | Delete key |
|
||||
|
||||
## Node Operations
|
||||
|
||||
### Adding Nodes
|
||||
|
||||
1. **Drag from Node Panel** - Drag nodes from the left panel onto the canvas
|
||||
2. **Right-click Menu** - Right-click on empty canvas space to select nodes
|
||||
|
||||
### Connecting Nodes
|
||||
|
||||
1. Drag from an output pin to an input pin
|
||||
2. Compatible pins will highlight
|
||||
3. Release to complete the connection
|
||||
|
||||
**Pin Type Reference:**
|
||||
|
||||
| Pin Color | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| White ▶ | Exec | Execution flow (controls order) |
|
||||
| Cyan ◆ | Entity | Entity reference |
|
||||
| Purple ◆ | Component | Component reference |
|
||||
| Light Blue ◆ | String | String value |
|
||||
| Green ◆ | Number | Numeric value |
|
||||
| Red ◆ | Boolean | Boolean value |
|
||||
| Gray ◆ | Any | Any type |
|
||||
|
||||
### Deleting Connections
|
||||
|
||||
Click a connection line to select it, then press Delete.
|
||||
|
||||
## Node Types Reference
|
||||
|
||||
### Event Nodes
|
||||
|
||||
Event nodes are entry points for blueprint execution, triggered when specific events occur.
|
||||
|
||||
| Node | Trigger | Outputs |
|
||||
|------|---------|---------|
|
||||
| **Event BeginPlay** | When blueprint starts | Exec, Self (Entity) |
|
||||
| **Event Tick** | Every frame | Exec, Delta Time |
|
||||
| **Event EndPlay** | When blueprint stops | Exec |
|
||||
|
||||
**Example: Print message on game start**
|
||||
```
|
||||
[Event BeginPlay] ──Exec──→ [Print]
|
||||
└─ Message: "Game Started!"
|
||||
```
|
||||
|
||||
### Entity Nodes
|
||||
|
||||
Nodes for operating on ECS entities.
|
||||
|
||||
| Node | Function | Inputs | Outputs |
|
||||
|------|----------|--------|---------|
|
||||
| **Get Self** | Get current entity | - | Entity |
|
||||
| **Create Entity** | Create new entity | Exec, Name | Exec, Entity |
|
||||
| **Destroy Entity** | Destroy entity | Exec, Entity | Exec |
|
||||
| **Find Entity By Name** | Find by name | Name | Entity |
|
||||
| **Find Entities By Tag** | Find by tag | Tag | Entity[] |
|
||||
| **Is Valid** | Check entity validity | Entity | Boolean |
|
||||
| **Get/Set Entity Name** | Get/Set name | Entity | String |
|
||||
| **Set Active** | Set active state | Exec, Entity, Active | Exec |
|
||||
|
||||
**Example: Create new entity**
|
||||
```
|
||||
[Event BeginPlay] ──→ [Create Entity] ──→ [Add Component]
|
||||
└─ Name: "Bullet" └─ Type: Transform
|
||||
```
|
||||
|
||||
### Component Nodes
|
||||
|
||||
Access and manipulate ECS components.
|
||||
|
||||
| Node | Function |
|
||||
|------|----------|
|
||||
| **Has Component** | Check if entity has component |
|
||||
| **Get Component** | Get component instance |
|
||||
| **Add Component** | Add component to entity |
|
||||
| **Remove Component** | Remove component |
|
||||
| **Get/Set Property** | Get/Set component property |
|
||||
|
||||
**Example: Modify Transform component**
|
||||
```
|
||||
[Get Self] ─Entity─→ [Get Component: Transform] ─Component─→ [Set Property]
|
||||
├─ Property: x
|
||||
└─ Value: 100
|
||||
```
|
||||
|
||||
### Flow Control Nodes
|
||||
|
||||
Nodes that control execution flow.
|
||||
|
||||
#### Branch
|
||||
|
||||
Conditional branching, similar to if/else.
|
||||
|
||||
```
|
||||
┌─ True ──→ [DoSomething]
|
||||
[Branch]─┤
|
||||
└─ False ─→ [DoOtherThing]
|
||||
```
|
||||
|
||||
#### Sequence
|
||||
|
||||
Execute multiple branches in order.
|
||||
|
||||
```
|
||||
┌─ Then 0 ──→ [Step1]
|
||||
[Sequence]─┼─ Then 1 ──→ [Step2]
|
||||
└─ Then 2 ──→ [Step3]
|
||||
```
|
||||
|
||||
#### For Loop
|
||||
|
||||
Execute a specified number of times.
|
||||
|
||||
```
|
||||
[For Loop] ─Loop Body─→ [Execute each iteration]
|
||||
│
|
||||
└─ Completed ────→ [Execute after loop ends]
|
||||
```
|
||||
|
||||
| Input | Description |
|
||||
|-------|-------------|
|
||||
| First Index | Starting index |
|
||||
| Last Index | Ending index |
|
||||
|
||||
| Output | Description |
|
||||
|--------|-------------|
|
||||
| Loop Body | Execute each iteration |
|
||||
| Index | Current index |
|
||||
| Completed | Execute after loop ends |
|
||||
|
||||
#### For Each
|
||||
|
||||
Iterate over array elements.
|
||||
|
||||
#### While Loop
|
||||
|
||||
Loop while condition is true.
|
||||
|
||||
#### Do Once
|
||||
|
||||
Execute only once, skip afterwards.
|
||||
|
||||
#### Flip Flop
|
||||
|
||||
Alternate between A and B outputs each execution.
|
||||
|
||||
#### Gate
|
||||
|
||||
Control whether execution passes through via Open/Close/Toggle.
|
||||
|
||||
### Time Nodes
|
||||
|
||||
| Node | Function | Output Type |
|
||||
|------|----------|-------------|
|
||||
| **Delay** | Delay execution by specified time | Exec |
|
||||
| **Get Delta Time** | Get frame delta time | Number |
|
||||
| **Get Time** | Get total runtime | Number |
|
||||
|
||||
**Example: Execute after 2 second delay**
|
||||
```
|
||||
[Event BeginPlay] ──→ [Delay] ──→ [Print]
|
||||
└─ Duration: 2.0 └─ "Executed after 2s"
|
||||
```
|
||||
|
||||
### Math Nodes
|
||||
|
||||
| Node | Function |
|
||||
|------|----------|
|
||||
| **Add / Subtract / Multiply / Divide** | Arithmetic operations |
|
||||
| **Abs** | Absolute value |
|
||||
| **Clamp** | Clamp to range |
|
||||
| **Lerp** | Linear interpolation |
|
||||
| **Min / Max** | Minimum/Maximum value |
|
||||
| **Random Range** | Random number |
|
||||
| **Sin / Cos / Tan** | Trigonometric functions |
|
||||
|
||||
### Debug Nodes
|
||||
|
||||
| Node | Function |
|
||||
|------|----------|
|
||||
| **Print** | Output to console |
|
||||
|
||||
## Variable System
|
||||
|
||||
Variables store and share data within a blueprint.
|
||||
|
||||
### Creating Variables
|
||||
|
||||
1. Click the **+** button in the Variables panel
|
||||
2. Enter variable name
|
||||
3. Select variable type
|
||||
4. Set default value (optional)
|
||||
|
||||
### Using Variables
|
||||
|
||||
- **Drag to canvas** - Creates Get or Set node
|
||||
- **Get Node** - Read variable value
|
||||
- **Set Node** - Write variable value
|
||||
|
||||
### Variable Types
|
||||
|
||||
| Type | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| Boolean | Boolean value | false |
|
||||
| Number | Numeric value | 0 |
|
||||
| String | String value | "" |
|
||||
| Entity | Entity reference | null |
|
||||
| Vector2 | 2D vector | (0, 0) |
|
||||
| Vector3 | 3D vector | (0, 0, 0) |
|
||||
|
||||
### Variable Node Error State
|
||||
|
||||
If you delete a variable but nodes still reference it:
|
||||
- Nodes display a **red border** and **warning icon**
|
||||
- You need to recreate the variable or delete these nodes
|
||||
|
||||
## Node Grouping
|
||||
|
||||
You can organize multiple nodes into a visual group box to help manage complex blueprints.
|
||||
|
||||
### Creating a Group
|
||||
|
||||
1. Box-select or Ctrl+click to select multiple nodes (at least 2)
|
||||
2. Right-click on the selected nodes
|
||||
3. Choose **Create Group**
|
||||
4. A group box will automatically wrap all selected nodes
|
||||
|
||||
### Group Operations
|
||||
|
||||
| Action | Method |
|
||||
|--------|--------|
|
||||
| Move group | Drag the group header, all nodes move together |
|
||||
| Ungroup | Right-click on group box → **Ungroup** |
|
||||
|
||||
### Features
|
||||
|
||||
- **Dynamic sizing**: Group box automatically resizes to wrap all nodes
|
||||
- **Independent movement**: You can move nodes within the group individually, and the box adjusts
|
||||
- **Editor only**: Groups are purely visual organization, no runtime impact
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Function |
|
||||
|----------|----------|
|
||||
| `Ctrl + S` | Save blueprint |
|
||||
| `Ctrl + Z` | Undo |
|
||||
| `Ctrl + Shift + Z` | Redo |
|
||||
| `Ctrl + C` | Copy selected nodes |
|
||||
| `Ctrl + X` | Cut selected nodes |
|
||||
| `Ctrl + V` | Paste nodes |
|
||||
| `Delete` | Delete selected items |
|
||||
| `Ctrl + A` | Select all |
|
||||
|
||||
## Save & Load
|
||||
|
||||
### Saving Blueprints
|
||||
|
||||
1. Click the **Save** button in the toolbar
|
||||
2. Choose save location (**must be saved in `assets/resources` directory**, otherwise Cocos Creator cannot load dynamically)
|
||||
3. File extension is `.blueprint.json`
|
||||
|
||||
> **Important**: Blueprint files must be placed in the `resources` directory for runtime loading via `cc.resources.load()`.
|
||||
|
||||
### Loading Blueprints
|
||||
|
||||
1. Click the **Open** button in the toolbar
|
||||
2. Select a `.blueprint.json` file
|
||||
|
||||
### Blueprint File Format
|
||||
|
||||
Blueprints are saved as JSON, compatible with `@esengine/blueprint` runtime:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "blueprint",
|
||||
"metadata": {
|
||||
"name": "PlayerController",
|
||||
"description": "Player control logic"
|
||||
},
|
||||
"variables": [],
|
||||
"nodes": [],
|
||||
"connections": []
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: Movement Control
|
||||
|
||||
Move entity every frame:
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"ex1-exec","to":"ex1-setprop","type":"exec"},{"from":"ex1-delta","to":"ex1-mul-a","type":"float"},{"from":"ex1-mul-result","to":"ex1-x","type":"float"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event Tick</span>
|
||||
<span class="bp-header-exec" data-pin="ex1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex1-delta"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Delta Time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 110px; width: 120px;">
|
||||
<div class="bp-node-header math">Multiply</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex1-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">B (Speed)</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex1-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 380px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Set Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex1-setprop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7030c0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex1-x"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Example 2: Health System
|
||||
|
||||
Check death after taking damage. `Event OnDamage` is a custom event node that can be triggered from code via `vm.triggerCustomEvent('OnDamage', { damage: 50 })`:
|
||||
|
||||
<div class="bp-graph" data-graph='{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "event", "title": "Event OnDamage", "category": "event",
|
||||
"outputs": [
|
||||
{"id": "event-exec", "type": "exec", "inHeader": true},
|
||||
{"id": "event-self", "type": "entity", "label": "Self"},
|
||||
{"id": "event-damage", "type": "float", "label": "Damage"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "getcomp", "title": "Get Component", "category": "function",
|
||||
"inputs": [
|
||||
{"id": "getcomp-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "getcomp-entity", "type": "entity", "label": "Entity"},
|
||||
{"id": "getcomp-type", "type": "string", "label": "Type", "value": "Health", "connected": false}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "getcomp-out", "type": "exec"},
|
||||
{"id": "getcomp-comp", "type": "component", "label": "Component"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "getprop", "title": "Get Property", "category": "pure",
|
||||
"inputs": [
|
||||
{"id": "getprop-target", "type": "component", "label": "Target"},
|
||||
{"id": "getprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "getprop-val", "type": "float", "label": "Value"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sub", "title": "Subtract", "category": "math",
|
||||
"inputs": [
|
||||
{"id": "sub-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "sub-a", "type": "float", "label": "A"},
|
||||
{"id": "sub-b", "type": "float", "label": "B"}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "sub-out", "type": "exec"},
|
||||
{"id": "sub-result", "type": "float", "label": "Result"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "setprop", "title": "Set Property", "category": "function",
|
||||
"inputs": [
|
||||
{"id": "setprop-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "setprop-target", "type": "component", "label": "Target"},
|
||||
{"id": "setprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false},
|
||||
{"id": "setprop-val", "type": "float", "label": "Value"}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "setprop-out", "type": "exec"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lte", "title": "Less Or Equal", "category": "pure",
|
||||
"inputs": [
|
||||
{"id": "lte-a", "type": "float", "label": "A"},
|
||||
{"id": "lte-b", "type": "float", "label": "B", "value": "0", "connected": false}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "lte-result", "type": "bool", "label": "Result"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "branch", "title": "Branch", "category": "flow",
|
||||
"inputs": [
|
||||
{"id": "branch-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "branch-cond", "type": "bool", "label": "Condition"}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "branch-true", "type": "exec", "label": "True"},
|
||||
{"id": "branch-false", "type": "exec", "label": "False"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "destroy", "title": "Destroy Entity", "category": "function",
|
||||
"inputs": [
|
||||
{"id": "destroy-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "destroy-entity", "type": "entity", "label": "Entity"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{"from": "event-exec", "to": "getcomp-exec", "type": "exec"},
|
||||
{"from": "getcomp-out", "to": "sub-exec", "type": "exec"},
|
||||
{"from": "sub-out", "to": "setprop-exec", "type": "exec"},
|
||||
{"from": "setprop-out", "to": "branch-exec", "type": "exec"},
|
||||
{"from": "branch-true", "to": "destroy-exec", "type": "exec"},
|
||||
{"from": "event-self", "to": "getcomp-entity", "type": "entity"},
|
||||
{"from": "event-self", "to": "destroy-entity", "type": "entity"},
|
||||
{"from": "getcomp-comp", "to": "getprop-target", "type": "component"},
|
||||
{"from": "getcomp-comp", "to": "setprop-target", "type": "component"},
|
||||
{"from": "getprop-val", "to": "sub-a", "type": "float"},
|
||||
{"from": "event-damage", "to": "sub-b", "type": "float"},
|
||||
{"from": "sub-result", "to": "setprop-val", "type": "float"},
|
||||
{"from": "sub-result", "to": "lte-a", "type": "float"},
|
||||
{"from": "lte-result", "to": "branch-cond", "type": "bool"}
|
||||
]
|
||||
}'></div>
|
||||
|
||||
### Example 3: Delayed Spawning
|
||||
|
||||
Spawn an enemy every 2 seconds:
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"ex3-begin-exec","to":"ex3-loop","type":"exec"},{"from":"ex3-loop-body","to":"ex3-delay","type":"exec"},{"from":"ex3-delay-done","to":"ex3-create","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="ex3-begin-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header flow">Do N Times</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex3-loop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">N</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex3-loop-body"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Loop Body</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Index</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 430px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header time">Delay</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex3-delay"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Duration</span>
|
||||
<span class="bp-pin-value">2.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex3-delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 610px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header function">Create Entity</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex3-create"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Name</span>
|
||||
<span class="bp-pin-value">"Enemy"</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Q: Nodes won't connect?
|
||||
|
||||
Check if pin types are compatible. Execution pins (white) can only connect to execution pins. Data pins need matching types.
|
||||
|
||||
### Q: Blueprint not executing?
|
||||
|
||||
1. Ensure entity has `BlueprintComponent` attached
|
||||
2. Ensure scene has `BlueprintSystem` added
|
||||
3. Check if `autoStart` is `true`
|
||||
|
||||
### Q: How to debug?
|
||||
|
||||
Use **Print** nodes to output variable values to the console.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [ECS Node Reference](/en/modules/blueprint/nodes) - Complete node list
|
||||
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
|
||||
- [Runtime Integration](/en/modules/blueprint/vm) - Blueprint VM API
|
||||
- [Examples](/en/modules/blueprint/examples) - More game logic examples
|
||||
@@ -5,7 +5,17 @@ description: "Visual scripting system deeply integrated with ECS framework"
|
||||
|
||||
`@esengine/blueprint` provides a visual scripting system deeply integrated with the ECS framework, supporting node-based programming to control entity behavior.
|
||||
|
||||
## Installation
|
||||
## Editor Download
|
||||
|
||||
Blueprint Editor Plugin for Cocos Creator (Free):
|
||||
|
||||
**[Download Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
|
||||
|
||||
> QQ Group: **481923584** | Website: [esengine.cn](https://esengine.cn/)
|
||||
|
||||
For detailed usage instructions, see [Editor User Guide](./editor-guide).
|
||||
|
||||
## Runtime Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/blueprint
|
||||
@@ -144,6 +154,7 @@ interface BlueprintAsset {
|
||||
|
||||
## Documentation Navigation
|
||||
|
||||
- [Editor User Guide](./editor-guide) - Cocos Creator Blueprint Editor tutorial
|
||||
- [Virtual Machine API](./vm) - BlueprintVM and ECS integration
|
||||
- [ECS Node Reference](./nodes) - Built-in ECS operation nodes
|
||||
- [Custom Nodes](./custom-nodes) - Create custom ECS nodes
|
||||
|
||||
@@ -1,118 +1,616 @@
|
||||
---
|
||||
title: "ECS Node Reference"
|
||||
description: "Blueprint built-in ECS operation nodes"
|
||||
description: "Blueprint built-in ECS operation nodes - complete reference with visual examples"
|
||||
---
|
||||
|
||||
This document provides a complete reference for all built-in blueprint nodes with visual examples.
|
||||
|
||||
<script src="/js/blueprint-graph.js"></script>
|
||||
|
||||
## Pin Type Legend
|
||||
|
||||
<div class="bp-legend">
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff" stroke="#fff" stroke-width="1"/></svg> Execution Flow</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#00a0e0" stroke-width="2"/></svg> Entity</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#7030c0" stroke-width="2"/></svg> Component</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#7ecd32" stroke-width="2"/></svg> Number</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#e060e0" stroke-width="2"/></svg> String</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#8c0000" stroke-width="2"/></svg> Boolean</div>
|
||||
</div>
|
||||
|
||||
## Event Nodes
|
||||
|
||||
Lifecycle events as blueprint entry points:
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `EventBeginPlay` | Triggered when blueprint starts |
|
||||
| `EventTick` | Triggered each frame, receives deltaTime |
|
||||
| `EventEndPlay` | Triggered when blueprint stops |
|
||||
| Node | Description | Outputs |
|
||||
|------|-------------|---------|
|
||||
| `EventBeginPlay` | Triggered when blueprint starts | Exec, Self (Entity) |
|
||||
| `EventTick` | Triggered each frame | Exec, Delta Time |
|
||||
| `EventEndPlay` | Triggered when blueprint stops | Exec |
|
||||
|
||||
### Example: Game Initialization
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-beginplay-exec","to":"en-print-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="en-beginplay-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Self</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-print-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Message</span>
|
||||
<span class="bp-pin-value">"Game Started!"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Example: Per-Frame Movement
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-tick-exec","to":"en-setprop-exec","type":"exec"},{"from":"en-tick-delta","to":"en-mul-a","type":"float"},{"from":"en-mul-result","to":"en-setprop-x","type":"float"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event Tick</span>
|
||||
<span class="bp-header-exec" data-pin="en-tick-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-tick-delta"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Delta Time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 110px; width: 120px;">
|
||||
<div class="bp-node-header math">Multiply</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 380px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Set Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-setprop-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7030c0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-setprop-x"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Entity Nodes
|
||||
|
||||
ECS entity operations:
|
||||
Manipulate ECS entities:
|
||||
|
||||
| Node | Description | Type |
|
||||
|------|-------------|------|
|
||||
| `Get Self` | Get entity owning this blueprint | Pure |
|
||||
| `Create Entity` | Create new entity in scene | Execution |
|
||||
| `Destroy Entity` | Destroy specified entity | Execution |
|
||||
| `Destroy Self` | Destroy self entity | Execution |
|
||||
| `Get Self` | Get the entity owning this blueprint | Pure |
|
||||
| `Create Entity` | Create a new entity in the scene | Exec |
|
||||
| `Destroy Entity` | Destroy specified entity | Exec |
|
||||
| `Destroy Self` | Destroy the owning entity | Exec |
|
||||
| `Is Valid` | Check if entity is valid | Pure |
|
||||
| `Get Entity Name` | Get entity name | Pure |
|
||||
| `Set Entity Name` | Set entity name | Execution |
|
||||
| `Get Entity Tag` | Get entity tag | Pure |
|
||||
| `Set Entity Tag` | Set entity tag | Execution |
|
||||
| `Set Active` | Set entity active state | Execution |
|
||||
| `Is Active` | Check if entity is active | Pure |
|
||||
| `Set Entity Name` | Set entity name | Exec |
|
||||
| `Find Entity By Name` | Find entity by name | Pure |
|
||||
| `Find Entities By Tag` | Find all entities by tag | Pure |
|
||||
| `Get Entity ID` | Get entity unique ID | Pure |
|
||||
| `Find Entity By ID` | Find entity by ID | Pure |
|
||||
| `Find Entities By Tag` | Find all entities with tag | Pure |
|
||||
|
||||
### Example: Create Bullet
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-bp-exec","to":"en-create-exec","type":"exec"},{"from":"en-create-exec-out","to":"en-add-exec","type":"exec"},{"from":"en-create-entity","to":"en-add-entity","type":"entity"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="en-bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Self</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Create Entity</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-create-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-create-exec-out"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-create-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 520px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Add Transform</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-add-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-add-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Component Nodes
|
||||
|
||||
ECS component operations:
|
||||
Read and write component properties:
|
||||
|
||||
| Node | Description | Type |
|
||||
|------|-------------|------|
|
||||
| `Get Component` | Get component of specified type from entity | Pure |
|
||||
| `Has Component` | Check if entity has specified component | Pure |
|
||||
| `Get Component` | Get component from entity | Pure |
|
||||
| `Get All Components` | Get all components from entity | Pure |
|
||||
| `Remove Component` | Remove component | Execution |
|
||||
| `Get Component Property` | Get component property value | Pure |
|
||||
| `Set Component Property` | Set component property value | Execution |
|
||||
| `Get Component Type` | Get component type name | Pure |
|
||||
| `Get Owner Entity` | Get owning entity from component | Pure |
|
||||
| `Add Component` | Add component to entity | Exec |
|
||||
| `Remove Component` | Remove component from entity | Exec |
|
||||
| `Get Property` | Get component property value | Pure |
|
||||
| `Set Property` | Set component property value | Exec |
|
||||
|
||||
### Example: Modify Position
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-self-entity","to":"en-getcomp-entity","type":"entity"},{"from":"en-getcomp-transform","to":"en-getprop-target","type":"component"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 100px;">
|
||||
<div class="bp-node-header pure">Get Self</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-self-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header pure">Get Component</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-getcomp-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-getcomp-transform"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
|
||||
<span class="bp-pin-label">Transform</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 430px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header pure">Get Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-getprop-target"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Flow Control Nodes
|
||||
|
||||
Control execution flow:
|
||||
Control blueprint execution flow:
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `Branch` | Conditional branch (if/else) |
|
||||
| `Sequence` | Execute multiple outputs in sequence |
|
||||
| `For Loop` | Loop execution |
|
||||
| `For Each` | Iterate array |
|
||||
| `While Loop` | Conditional loop |
|
||||
| `Branch` | Conditional branching (if/else) |
|
||||
| `Sequence` | Execute multiple branches in order |
|
||||
| `For Loop` | Loop specified number of times |
|
||||
| `For Each` | Iterate over array elements |
|
||||
| `While Loop` | Loop while condition is true |
|
||||
| `Do Once` | Execute only once |
|
||||
| `Flip Flop` | Alternate between two branches |
|
||||
| `Gate` | Toggleable execution gate |
|
||||
| `Flip Flop` | Alternate between A and B |
|
||||
| `Gate` | Gate switch control |
|
||||
|
||||
### Example: Conditional Branch
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-cond-exec","to":"en-branch-exec","type":"exec"},{"from":"en-cond-result","to":"en-branch-cond","type":"bool"},{"from":"en-branch-true","to":"en-print1-exec","type":"exec"},{"from":"en-branch-false","to":"en-print2-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 60px; width: 120px;">
|
||||
<div class="bp-node-header pure">Condition</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-cond-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-cond-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#8c0000"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 60px; width: 110px;">
|
||||
<div class="bp-node-header flow">Branch</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-branch-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-branch-cond"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#8c0000"/></svg></span>
|
||||
<span class="bp-pin-label">Cond</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-branch-true"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">True</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-branch-false"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">False</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 420px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-print1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"Yes"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 420px; top: 130px; width: 120px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-print2-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"No"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Example: For Loop
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-forloop-bp-exec","to":"en-forloop-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="en-forloop-bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header flow">For Loop</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-forloop-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">First</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Last</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Body</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Index</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Time Nodes
|
||||
|
||||
| Node | Description | Type |
|
||||
|------|-------------|------|
|
||||
| `Delay` | Delay execution | Execution |
|
||||
| `Get Delta Time` | Get frame delta time | Pure |
|
||||
| `Get Time` | Get total runtime | Pure |
|
||||
| Node | Description | Output |
|
||||
|------|-------------|--------|
|
||||
| `Delay` | Delay execution by specified seconds | Exec |
|
||||
| `Get Delta Time` | Get frame delta time | Float |
|
||||
| `Get Time` | Get total runtime | Float |
|
||||
|
||||
### Example: Delayed Execution
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-delay-bp-exec","to":"en-delay-exec","type":"exec"},{"from":"en-delay-done","to":"en-delay-print-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="en-delay-bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header time">Delay</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-delay-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Duration</span>
|
||||
<span class="bp-pin-value">2.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 490px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-delay-print-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"After 2s"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Math Nodes
|
||||
|
||||
### Basic Operations
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Add` | Addition | A, B | A + B |
|
||||
| `Subtract` | Subtraction | A, B | A - B |
|
||||
| `Multiply` | Multiplication | A, B | A × B |
|
||||
| `Divide` | Division | A, B | A / B |
|
||||
| `Modulo` | Modulo | A, B | A % B |
|
||||
|
||||
### Math Functions
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Abs` | Absolute value | Value | \|Value\| |
|
||||
| `Sqrt` | Square root | Value | √Value |
|
||||
| `Pow` | Power | Base, Exp | Base^Exp |
|
||||
| `Floor` | Floor | Value | ⌊Value⌋ |
|
||||
| `Ceil` | Ceiling | Value | ⌈Value⌉ |
|
||||
| `Round` | Round | Value | round(Value) |
|
||||
| `Clamp` | Clamp to range | Value, Min, Max | min(max(V, Min), Max) |
|
||||
| `Lerp` | Linear interpolation | A, B, Alpha | A + (B-A) × Alpha |
|
||||
| `Min` | Minimum | A, B | min(A, B) |
|
||||
| `Max` | Maximum | A, B | max(A, B) |
|
||||
|
||||
### Trigonometric Functions
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `Add` / `Subtract` / `Multiply` / `Divide` | Basic operations |
|
||||
| `Abs` | Absolute value |
|
||||
| `Clamp` | Clamp to range |
|
||||
| `Lerp` | Linear interpolation |
|
||||
| `Min` / `Max` | Minimum/Maximum |
|
||||
| `Sin` | Sine |
|
||||
| `Cos` | Cosine |
|
||||
| `Tan` | Tangent |
|
||||
| `Asin` | Arc sine |
|
||||
| `Acos` | Arc cosine |
|
||||
| `Atan` | Arc tangent |
|
||||
| `Atan2` | Two-argument arc tangent |
|
||||
|
||||
### Random Numbers
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Random` | Random float [0, 1) | - | Float |
|
||||
| `Random Range` | Random in range | Min, Max | Float |
|
||||
| `Random Int` | Random integer | Min, Max | Int |
|
||||
|
||||
### Comparison Nodes
|
||||
|
||||
| Node | Description | Output |
|
||||
|------|-------------|--------|
|
||||
| `Equal` | A == B | Boolean |
|
||||
| `Not Equal` | A != B | Boolean |
|
||||
| `Greater` | A > B | Boolean |
|
||||
| `Greater Or Equal` | A >= B | Boolean |
|
||||
| `Less` | A < B | Boolean |
|
||||
| `Less Or Equal` | A <= B | Boolean |
|
||||
|
||||
### Extended Math Nodes
|
||||
|
||||
> **Vector2, Fixed32, FixedVector2, Color** and other advanced math nodes are provided by the `@esengine/ecs-framework-math` module.
|
||||
>
|
||||
> See: [Math Blueprint Nodes](/en/modules/math/blueprint-nodes)
|
||||
|
||||
### Example: Clamp Value
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-rand-result","to":"en-clamp-value","type":"float"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header math">Random Range</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Min</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Max</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-rand-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header math">Clamp</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-clamp-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Min</span>
|
||||
<span class="bp-pin-value">20</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Max</span>
|
||||
<span class="bp-pin-value">80</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Variable Nodes
|
||||
|
||||
Blueprint-defined variables automatically generate Get and Set nodes:
|
||||
|
||||
| Node | Description | Type |
|
||||
|------|-------------|------|
|
||||
| `Get <varname>` | Read variable value | Pure |
|
||||
| `Set <varname>` | Set variable value | Exec |
|
||||
|
||||
### Example: Counter
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"en-cnt-tick-exec","to":"en-cnt-add-exec","type":"exec"},{"from":"en-cnt-get-value","to":"en-cnt-add-a","type":"int"},{"from":"en-cnt-add-result","to":"en-cnt-set-value","type":"int"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event Tick</span>
|
||||
<span class="bp-header-exec" data-pin="en-cnt-tick-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Delta</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 20px; top: 120px; width: 110px;">
|
||||
<div class="bp-node-header variable">Get Count</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-cnt-get-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 240px; top: 20px; width: 110px;">
|
||||
<div class="bp-node-header math">Add</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-cnt-add-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-cnt-add-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
<span class="bp-pin-value">1</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="en-cnt-add-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 430px; top: 20px; width: 110px;">
|
||||
<div class="bp-node-header variable">Set Count</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="en-cnt-set-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Debug Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `Print` | Print to console |
|
||||
| `Print` | Output message to console |
|
||||
|
||||
## Auto-generated Component Nodes
|
||||
## Related Documentation
|
||||
|
||||
Components marked with `@BlueprintExpose` decorator auto-generate nodes:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
@BlueprintExpose({ displayName: 'Transform', category: 'core' })
|
||||
export class TransformComponent extends Component {
|
||||
@BlueprintProperty({ displayName: 'X Position' })
|
||||
x: number = 0;
|
||||
|
||||
@BlueprintProperty({ displayName: 'Y Position' })
|
||||
y: number = 0;
|
||||
|
||||
@BlueprintMethod({ displayName: 'Translate' })
|
||||
translate(dx: number, dy: number): void {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Generated nodes:
|
||||
- **Get Transform** - Get Transform component
|
||||
- **Get X Position** / **Set X Position** - Access x property
|
||||
- **Get Y Position** / **Set Y Position** - Access y property
|
||||
- **Translate** - Call translate method
|
||||
- [Math Blueprint Nodes](/en/modules/math/blueprint-nodes) - Vector2, Fixed32, Color and other math nodes
|
||||
- [Blueprint Editor Guide](/en/modules/blueprint/editor-guide) - Learn how to use the editor
|
||||
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
|
||||
- [Blueprint VM](/en/modules/blueprint/vm) - Runtime API
|
||||
|
||||
489
docs/src/content/docs/en/modules/math/blueprint-nodes.md
Normal file
489
docs/src/content/docs/en/modules/math/blueprint-nodes.md
Normal file
@@ -0,0 +1,489 @@
|
||||
---
|
||||
title: "Math Blueprint Nodes"
|
||||
description: "Blueprint nodes provided by the Math module - Vector2, Fixed32, FixedVector2, Color"
|
||||
---
|
||||
|
||||
This document describes the blueprint nodes provided by the `@esengine/ecs-framework-math` module.
|
||||
|
||||
> **Note**: These nodes require the math module to be installed.
|
||||
|
||||
<script src="/js/blueprint-graph.js"></script>
|
||||
|
||||
## Pin Type Legend
|
||||
|
||||
<div class="bp-legend">
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#7ecd32" stroke-width="2"/></svg> Float</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#2196F3" stroke-width="2"/></svg> Vector2</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#9C27B0" stroke-width="2"/></svg> Fixed32</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#673AB7" stroke-width="2"/></svg> FixedVector2</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#FF9800" stroke-width="2"/></svg> Color</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Vector2 Nodes
|
||||
|
||||
2D vector operations for position, velocity, and direction calculations.
|
||||
|
||||
### Node List
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Make Vector2` | Create Vector2 from X, Y | X, Y | Vector2 |
|
||||
| `Break Vector2` | Decompose Vector2 to X, Y | Vector | X, Y |
|
||||
| `Vector2 +` | Vector addition | A, B | Vector2 |
|
||||
| `Vector2 -` | Vector subtraction | A, B | Vector2 |
|
||||
| `Vector2 *` | Vector scaling | Vector, Scalar | Vector2 |
|
||||
| `Vector2 Length` | Get vector length | Vector | Float |
|
||||
| `Vector2 Normalize` | Normalize to unit vector | Vector | Vector2 |
|
||||
| `Vector2 Dot` | Dot product | A, B | Float |
|
||||
| `Vector2 Cross` | 2D cross product | A, B | Float |
|
||||
| `Vector2 Distance` | Distance between two points | A, B | Float |
|
||||
| `Vector2 Lerp` | Linear interpolation | A, B, T | Vector2 |
|
||||
| `Vector2 Rotate` | Rotate by angle (radians) | Vector, Angle | Vector2 |
|
||||
| `Vector2 From Angle` | Create unit vector from angle | Angle | Vector2 |
|
||||
|
||||
### Example: Calculate Movement Direction
|
||||
|
||||
Direction vector from start to end point:
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"v2-start","to":"v2-sub-a","type":"vector2"},{"from":"v2-end","to":"v2-sub-b","type":"vector2"},{"from":"v2-sub-result","to":"v2-norm-in","type":"vector2"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Make Vector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-start"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 180px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Make Vector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">50</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-end"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 220px; top: 90px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 -</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-sub-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-sub-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-sub-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 400px; top: 55px; width: 140px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 Normalize</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-norm-in"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Example: Circular Motion
|
||||
|
||||
Calculate circular position using angle and radius:
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"v2-angle-out","to":"v2-scale-vec","type":"vector2"},{"from":"v2-scale-result","to":"v2-add-b","type":"vector2"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 40px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 From Angle</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Angle</span>
|
||||
<span class="bp-pin-value">1.57</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-angle-out"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 230px; top: 40px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 *</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-scale-vec"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Scalar</span>
|
||||
<span class="bp-pin-value">50</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-scale-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 420px; top: 40px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 +</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#2196F3" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">A (Center)</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-add-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Position</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Fixed32 Nodes (Fixed-Point Numbers)
|
||||
|
||||
Q16.16 fixed-point number operations for lockstep networking games, ensuring cross-platform calculation consistency.
|
||||
|
||||
### Node List
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Fixed32 From Float` | Create from float | Float | Fixed32 |
|
||||
| `Fixed32 From Int` | Create from integer | Int | Fixed32 |
|
||||
| `Fixed32 To Float` | Convert to float | Fixed32 | Float |
|
||||
| `Fixed32 To Int` | Convert to integer | Fixed32 | Int |
|
||||
| `Fixed32 +` | Addition | A, B | Fixed32 |
|
||||
| `Fixed32 -` | Subtraction | A, B | Fixed32 |
|
||||
| `Fixed32 *` | Multiplication | A, B | Fixed32 |
|
||||
| `Fixed32 /` | Division | A, B | Fixed32 |
|
||||
| `Fixed32 Abs` | Absolute value | Value | Fixed32 |
|
||||
| `Fixed32 Sqrt` | Square root | Value | Fixed32 |
|
||||
| `Fixed32 Floor` | Floor | Value | Fixed32 |
|
||||
| `Fixed32 Ceil` | Ceiling | Value | Fixed32 |
|
||||
| `Fixed32 Round` | Round | Value | Fixed32 |
|
||||
| `Fixed32 Sign` | Sign (-1, 0, 1) | Value | Fixed32 |
|
||||
| `Fixed32 Min` | Minimum | A, B | Fixed32 |
|
||||
| `Fixed32 Max` | Maximum | A, B | Fixed32 |
|
||||
| `Fixed32 Clamp` | Clamp to range | Value, Min, Max | Fixed32 |
|
||||
| `Fixed32 Lerp` | Linear interpolation | A, B, T | Fixed32 |
|
||||
|
||||
### Example: Lockstep Movement Speed Calculation
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"f32-speed","to":"f32-mul-a","type":"fixed32"},{"from":"f32-dt","to":"f32-mul-b","type":"fixed32"},{"from":"f32-mul-result","to":"f32-tofloat","type":"fixed32"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 From Float</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
<span class="bp-pin-value">5.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="f32-speed"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">Speed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 160px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 From Float</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
<span class="bp-pin-value">0.016</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="f32-dt"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">DeltaTime</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 240px; top: 75px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 *</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="f32-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="f32-mul-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="f32-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 430px; top: 75px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 To Float</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="f32-tofloat"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">Fixed</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Float</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## FixedVector2 Nodes (Fixed-Point Vectors)
|
||||
|
||||
Fixed-point vector operations for deterministic physics calculations, suitable for lockstep networking.
|
||||
|
||||
### Node List
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Make FixedVector2` | Create from X, Y floats | X, Y | FixedVector2 |
|
||||
| `Break FixedVector2` | Decompose to X, Y floats | Vector | X, Y |
|
||||
| `FixedVector2 +` | Vector addition | A, B | FixedVector2 |
|
||||
| `FixedVector2 -` | Vector subtraction | A, B | FixedVector2 |
|
||||
| `FixedVector2 *` | Scale by Fixed32 | Vector, Scalar | FixedVector2 |
|
||||
| `FixedVector2 Negate` | Negate vector | Vector | FixedVector2 |
|
||||
| `FixedVector2 Length` | Get length | Vector | Fixed32 |
|
||||
| `FixedVector2 Normalize` | Normalize | Vector | FixedVector2 |
|
||||
| `FixedVector2 Dot` | Dot product | A, B | Fixed32 |
|
||||
| `FixedVector2 Cross` | 2D cross product | A, B | Fixed32 |
|
||||
| `FixedVector2 Distance` | Distance between points | A, B | Fixed32 |
|
||||
| `FixedVector2 Lerp` | Linear interpolation | A, B, T | FixedVector2 |
|
||||
|
||||
### Example: Deterministic Position Update
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"fv2-pos","to":"fv2-add-a","type":"fixedvector2"},{"from":"fv2-vel","to":"fv2-add-b","type":"fixedvector2"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #673AB7;">Make FixedVector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">20</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="fv2-pos"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">Position</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 180px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #673AB7;">Make FixedVector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">1</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="fv2-vel"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">Velocity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 250px; top: 90px; width: 140px;">
|
||||
<div class="bp-node-header math" style="background: #673AB7;">FixedVector2 +</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="fv2-add-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="fv2-add-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">New Position</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Color Nodes
|
||||
|
||||
Color creation and manipulation nodes.
|
||||
|
||||
### Node List
|
||||
|
||||
| Node | Description | Inputs | Output |
|
||||
|------|-------------|--------|--------|
|
||||
| `Make Color` | Create from RGBA | R, G, B, A | Color |
|
||||
| `Break Color` | Decompose to RGBA | Color | R, G, B, A |
|
||||
| `Color From Hex` | Create from hex string | Hex | Color |
|
||||
| `Color To Hex` | Convert to hex string | Color | String |
|
||||
| `Color From HSL` | Create from HSL | H, S, L | Color |
|
||||
| `Color To HSL` | Convert to HSL | Color | H, S, L |
|
||||
| `Color Lerp` | Color interpolation | A, B, T | Color |
|
||||
| `Color Lighten` | Lighten | Color, Amount | Color |
|
||||
| `Color Darken` | Darken | Color, Amount | Color |
|
||||
| `Color Saturate` | Increase saturation | Color, Amount | Color |
|
||||
| `Color Desaturate` | Decrease saturation | Color, Amount | Color |
|
||||
| `Color Invert` | Invert | Color | Color |
|
||||
| `Color Grayscale` | Convert to grayscale | Color | Color |
|
||||
| `Color Luminance` | Get luminance | Color | Float |
|
||||
|
||||
### Color Constants
|
||||
|
||||
| Node | Value |
|
||||
|------|-------|
|
||||
| `Color White` | (1, 1, 1, 1) |
|
||||
| `Color Black` | (0, 0, 0, 1) |
|
||||
| `Color Red` | (1, 0, 0, 1) |
|
||||
| `Color Green` | (0, 1, 0, 1) |
|
||||
| `Color Blue` | (0, 0, 1, 1) |
|
||||
| `Color Transparent` | (0, 0, 0, 0) |
|
||||
|
||||
### Example: Color Transition Animation
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"color-a","to":"color-lerp-a","type":"color"},{"from":"color-b","to":"color-lerp-b","type":"color"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color Red</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="color-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 130px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color Blue</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="color-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 220px; top: 50px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color Lerp</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="color-lerp-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="color-lerp-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">T</span>
|
||||
<span class="bp-pin-value">0.5</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Example: Create Color from Hex
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"hex-color","to":"break-color","type":"color"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 30px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color From Hex</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Hex</span>
|
||||
<span class="bp-pin-value">"#FF5722"</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="hex-color"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 250px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Break Color</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="break-color"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">R</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">G</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Blueprint Node Reference](/en/modules/blueprint/nodes) - Core blueprint nodes
|
||||
- [Blueprint Editor Guide](/en/modules/blueprint/editor-guide) - Editor usage
|
||||
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
|
||||
79
docs/src/content/docs/en/modules/math/index.md
Normal file
79
docs/src/content/docs/en/modules/math/index.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Math Library"
|
||||
description: "ESEngine Math Library - Vector2, Fixed32, FixedVector2, Color and other math types"
|
||||
---
|
||||
|
||||
The `@esengine/ecs-framework-math` module provides common math types and operations for game development.
|
||||
|
||||
## Core Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `Vector2` | 2D floating-point vector for position, velocity, direction |
|
||||
| `Fixed32` | Q16.16 fixed-point number for deterministic lockstep calculations |
|
||||
| `FixedVector2` | 2D fixed-point vector for deterministic physics |
|
||||
| `Color` | RGBA color |
|
||||
|
||||
## Features
|
||||
|
||||
### Vector2
|
||||
|
||||
- Addition, subtraction, scaling
|
||||
- Dot product, cross product
|
||||
- Length, normalization
|
||||
- Distance, interpolation
|
||||
- Rotation, angle conversion
|
||||
|
||||
### Fixed32 Fixed-Point Numbers
|
||||
|
||||
Designed for lockstep networking games, ensuring cross-platform calculation consistency:
|
||||
|
||||
- Basic operations: add, subtract, multiply, divide
|
||||
- Math functions: absolute value, square root, rounding
|
||||
- Comparison, clamping, interpolation
|
||||
- Constants: 0, 1, 0.5, PI, 2*PI
|
||||
|
||||
### Color
|
||||
|
||||
- RGB/RGBA creation and decomposition
|
||||
- Hex string conversion
|
||||
- HSL color space conversion
|
||||
- Color operations: lighten, darken, saturation adjustment
|
||||
- Color blending and interpolation
|
||||
|
||||
## Blueprint Support
|
||||
|
||||
The math library provides rich blueprint nodes, see:
|
||||
|
||||
- [Math Blueprint Nodes](/en/modules/math/blueprint-nodes)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add @esengine/ecs-framework-math
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Vector2, Fixed32, FixedVector2, Color } from '@esengine/ecs-framework-math';
|
||||
|
||||
// Vector2
|
||||
const pos = new Vector2(10, 20);
|
||||
const dir = pos.normalized();
|
||||
|
||||
// Fixed32 (lockstep)
|
||||
const speed = Fixed32.from(5.0);
|
||||
const dt = Fixed32.from(0.016);
|
||||
const distance = speed.mul(dt);
|
||||
|
||||
// FixedVector2
|
||||
const fixedPos = FixedVector2.from(10, 20);
|
||||
const fixedVel = FixedVector2.from(1, 0);
|
||||
const newPos = fixedPos.add(fixedVel);
|
||||
|
||||
// Color
|
||||
const red = Color.RED;
|
||||
const blue = Color.BLUE;
|
||||
const purple = Color.lerp(red, blue, 0.5);
|
||||
```
|
||||
326
docs/src/content/docs/en/modules/network/fixed-point.md
Normal file
326
docs/src/content/docs/en/modules/network/fixed-point.md
Normal file
@@ -0,0 +1,326 @@
|
||||
---
|
||||
title: "Fixed-Point Numbers"
|
||||
description: "Deterministic fixed-point math library for lockstep games"
|
||||
---
|
||||
|
||||
`@esengine/ecs-framework-math` provides deterministic fixed-point calculations designed for **Lockstep** architecture. Fixed-point numbers guarantee identical results across all platforms.
|
||||
|
||||
## Why Fixed-Point?
|
||||
|
||||
Floating-point numbers may produce different rounding results on different platforms:
|
||||
|
||||
```typescript
|
||||
// Floating-point: may differ across platforms
|
||||
const a = 0.1 + 0.2; // 0.30000000000000004 (some platforms)
|
||||
// 0.3 (other platforms)
|
||||
|
||||
// Fixed-point: consistent everywhere
|
||||
const x = Fixed32.from(0.1);
|
||||
const y = Fixed32.from(0.2);
|
||||
const z = x.add(y); // raw = 19661 (all platforms)
|
||||
```
|
||||
|
||||
| Feature | Floating-Point | Fixed-Point |
|
||||
|---------|----------------|-------------|
|
||||
| Cross-platform consistency | ❌ May differ | ✅ Identical |
|
||||
| Network sync mode | State sync | Lockstep |
|
||||
| Game types | FPS, RPG | RTS, MOBA, Fighting |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework-math
|
||||
```
|
||||
|
||||
## Fixed32 Fixed-Point Number
|
||||
|
||||
Q16.16 format: 16-bit integer + 16-bit fraction, range ±32767.99998.
|
||||
|
||||
### Creating Fixed-Point Numbers
|
||||
|
||||
```typescript
|
||||
import { Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// From floating-point
|
||||
const speed = Fixed32.from(5.5);
|
||||
|
||||
// From integer (no precision loss)
|
||||
const count = Fixed32.fromInt(10);
|
||||
|
||||
// From raw value (after network receive)
|
||||
const received = Fixed32.fromRaw(360448); // equals 5.5
|
||||
|
||||
// Predefined constants
|
||||
Fixed32.ZERO // 0
|
||||
Fixed32.ONE // 1
|
||||
Fixed32.HALF // 0.5
|
||||
Fixed32.PI // π
|
||||
Fixed32.TWO_PI // 2π
|
||||
Fixed32.HALF_PI // π/2
|
||||
```
|
||||
|
||||
### Basic Operations
|
||||
|
||||
```typescript
|
||||
const a = Fixed32.from(10);
|
||||
const b = Fixed32.from(3);
|
||||
|
||||
const sum = a.add(b); // 13
|
||||
const diff = a.sub(b); // 7
|
||||
const prod = a.mul(b); // 30
|
||||
const quot = a.div(b); // 3.333...
|
||||
const mod = a.mod(b); // 1
|
||||
const neg = a.neg(); // -10
|
||||
const abs = neg.abs(); // 10
|
||||
```
|
||||
|
||||
### Comparison Operations
|
||||
|
||||
```typescript
|
||||
const x = Fixed32.from(5);
|
||||
const y = Fixed32.from(3);
|
||||
|
||||
x.eq(y) // false - equal
|
||||
x.ne(y) // true - not equal
|
||||
x.lt(y) // false - less than
|
||||
x.le(y) // false - less or equal
|
||||
x.gt(y) // true - greater than
|
||||
x.ge(y) // true - greater or equal
|
||||
|
||||
x.isZero() // false
|
||||
x.isPositive() // true
|
||||
x.isNegative() // false
|
||||
```
|
||||
|
||||
### Math Functions
|
||||
|
||||
```typescript
|
||||
// Square root (Newton's method, deterministic)
|
||||
const sqrt = Fixed32.sqrt(Fixed32.from(16)); // 4
|
||||
|
||||
// Rounding
|
||||
Fixed32.floor(Fixed32.from(3.7)) // 3
|
||||
Fixed32.ceil(Fixed32.from(3.2)) // 4
|
||||
Fixed32.round(Fixed32.from(3.5)) // 4
|
||||
|
||||
// Clamping
|
||||
Fixed32.clamp(value, min, max)
|
||||
|
||||
// Linear interpolation
|
||||
Fixed32.lerp(from, to, t)
|
||||
|
||||
// Min/Max
|
||||
Fixed32.min(a, b)
|
||||
Fixed32.max(a, b)
|
||||
```
|
||||
|
||||
### Type Conversion
|
||||
|
||||
```typescript
|
||||
const value = Fixed32.from(3.14159);
|
||||
|
||||
// To float (for rendering)
|
||||
const float = value.toNumber(); // 3.14159
|
||||
|
||||
// Get raw value (for network)
|
||||
const raw = value.toRaw(); // 205887
|
||||
|
||||
// To integer (floor)
|
||||
const int = value.toInt(); // 3
|
||||
```
|
||||
|
||||
## FixedVector2 Fixed-Point Vector
|
||||
|
||||
Immutable 2D vector, all operations return new instances.
|
||||
|
||||
### Creating Vectors
|
||||
|
||||
```typescript
|
||||
import { FixedVector2, Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// From floating-point
|
||||
const pos = FixedVector2.from(100, 200);
|
||||
|
||||
// From raw values (after network receive)
|
||||
const received = FixedVector2.fromRaw(6553600, 13107200);
|
||||
|
||||
// From Fixed32
|
||||
const vec = new FixedVector2(Fixed32.from(10), Fixed32.from(20));
|
||||
|
||||
// Predefined constants
|
||||
FixedVector2.ZERO // (0, 0)
|
||||
FixedVector2.ONE // (1, 1)
|
||||
FixedVector2.RIGHT // (1, 0)
|
||||
FixedVector2.LEFT // (-1, 0)
|
||||
FixedVector2.UP // (0, 1)
|
||||
FixedVector2.DOWN // (0, -1)
|
||||
```
|
||||
|
||||
### Vector Operations
|
||||
|
||||
```typescript
|
||||
const a = FixedVector2.from(3, 4);
|
||||
const b = FixedVector2.from(1, 2);
|
||||
|
||||
// Basic operations
|
||||
const sum = a.add(b); // (4, 6)
|
||||
const diff = a.sub(b); // (2, 2)
|
||||
const scaled = a.mul(Fixed32.from(2)); // (6, 8)
|
||||
const divided = a.div(Fixed32.from(2)); // (1.5, 2)
|
||||
|
||||
// Vector products
|
||||
const dot = a.dot(b); // 3*1 + 4*2 = 11
|
||||
const cross = a.cross(b); // 3*2 - 4*1 = 2
|
||||
|
||||
// Length
|
||||
const lenSq = a.lengthSquared(); // 25
|
||||
const len = a.length(); // 5
|
||||
|
||||
// Normalize
|
||||
const norm = a.normalize(); // (0.6, 0.8)
|
||||
|
||||
// Distance
|
||||
const dist = a.distanceTo(b); // sqrt((3-1)² + (4-2)²)
|
||||
```
|
||||
|
||||
### Rotation and Angles
|
||||
|
||||
```typescript
|
||||
import { FixedMath } from '@esengine/ecs-framework-math';
|
||||
|
||||
const vec = FixedVector2.from(1, 0);
|
||||
const angle = Fixed32.from(Math.PI / 2); // 90 degrees
|
||||
|
||||
// Rotate vector
|
||||
const rotated = vec.rotate(angle); // (0, 1)
|
||||
|
||||
// Rotate around point
|
||||
const center = FixedVector2.from(5, 5);
|
||||
const around = vec.rotateAround(center, angle);
|
||||
|
||||
// Get vector angle
|
||||
const vecAngle = vec.angle();
|
||||
|
||||
// Angle between vectors
|
||||
const between = vec.angleTo(other);
|
||||
|
||||
// Create unit vector from angle
|
||||
const dir = FixedVector2.fromAngle(angle);
|
||||
|
||||
// From polar coordinates
|
||||
const polar = FixedVector2.fromPolar(length, angle);
|
||||
```
|
||||
|
||||
### Type Conversion
|
||||
|
||||
```typescript
|
||||
const pos = FixedVector2.from(100.5, 200.5);
|
||||
|
||||
// To float object (for rendering)
|
||||
const obj = pos.toObject(); // { x: 100.5, y: 200.5 }
|
||||
|
||||
// To array
|
||||
const arr = pos.toArray(); // [100.5, 200.5]
|
||||
|
||||
// Get raw values (for network)
|
||||
const raw = pos.toRawObject(); // { x: 6586368, y: 13140992 }
|
||||
```
|
||||
|
||||
## FixedMath Trigonometric Functions
|
||||
|
||||
Deterministic trigonometric functions using lookup tables.
|
||||
|
||||
```typescript
|
||||
import { FixedMath, Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
const angle = Fixed32.from(Math.PI / 6); // 30 degrees
|
||||
|
||||
// Trigonometric functions
|
||||
const sin = FixedMath.sin(angle); // 0.5
|
||||
const cos = FixedMath.cos(angle); // 0.866
|
||||
const tan = FixedMath.tan(angle); // 0.577
|
||||
|
||||
// Inverse trigonometric
|
||||
const atan = FixedMath.atan2(y, x);
|
||||
const asin = FixedMath.asin(value);
|
||||
const acos = FixedMath.acos(value);
|
||||
|
||||
// Normalize angle to [-π, π]
|
||||
const normalized = FixedMath.normalizeAngle(angle);
|
||||
|
||||
// Angle difference (shortest path)
|
||||
const delta = FixedMath.angleDelta(from, to);
|
||||
|
||||
// Angle interpolation (handles 360° wrap)
|
||||
const lerped = FixedMath.lerpAngle(from, to, t);
|
||||
|
||||
// Radian/degree conversion
|
||||
const deg = FixedMath.radToDeg(rad);
|
||||
const rad = FixedMath.degToRad(deg);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Fixed-Point Throughout
|
||||
|
||||
```typescript
|
||||
// ✅ Correct: all game logic uses fixed-point
|
||||
function calculateDamage(baseDamage: Fixed32, multiplier: Fixed32): Fixed32 {
|
||||
return baseDamage.mul(multiplier);
|
||||
}
|
||||
|
||||
// ❌ Wrong: mixing floating-point
|
||||
function calculateDamage(baseDamage: number, multiplier: number): number {
|
||||
return baseDamage * multiplier; // may be inconsistent
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Only Convert to Float for Rendering
|
||||
|
||||
```typescript
|
||||
// Game logic
|
||||
const position: FixedVector2 = calculatePosition(input);
|
||||
|
||||
// Rendering
|
||||
const { x, y } = position.toObject();
|
||||
sprite.position.set(x, y);
|
||||
```
|
||||
|
||||
### 3. Use Raw Values for Network
|
||||
|
||||
```typescript
|
||||
// ✅ Correct: transmit raw integers
|
||||
const raw = position.toRawObject();
|
||||
send(JSON.stringify(raw));
|
||||
|
||||
// ❌ Wrong: transmit floats
|
||||
const float = position.toObject();
|
||||
send(JSON.stringify(float)); // may lose precision
|
||||
```
|
||||
|
||||
### 4. Use FixedMath for Trigonometry
|
||||
|
||||
```typescript
|
||||
// ✅ Correct: use lookup tables
|
||||
const direction = FixedVector2.fromAngle(FixedMath.atan2(dy, dx));
|
||||
|
||||
// ❌ Wrong: use Math library
|
||||
const angle = Math.atan2(dy.toNumber(), dx.toNumber()); // non-deterministic
|
||||
```
|
||||
|
||||
## API Exports
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Fixed32,
|
||||
FixedVector2,
|
||||
FixedMath,
|
||||
type IFixed32,
|
||||
type IFixedVector2
|
||||
} from '@esengine/ecs-framework-math';
|
||||
```
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [State Sync](/en/modules/network/sync) - Fixed-point snapshot buffer
|
||||
- [Client Prediction](/en/modules/network/prediction) - Fixed-point client prediction
|
||||
@@ -252,3 +252,145 @@ if (predictionSystem) {
|
||||
console.log('Current sequence:', predictionSystem.inputSequence);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fixed-Point Client Prediction (Lockstep)
|
||||
|
||||
Deterministic client prediction for **Lockstep** architecture.
|
||||
|
||||
> See [Fixed-Point Numbers](/en/modules/network/fixed-point) for math basics
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedClientPrediction,
|
||||
createFixedClientPrediction,
|
||||
type IFixedPredictor,
|
||||
type IFixedStatePositionExtractor
|
||||
} from '@esengine/network';
|
||||
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// Define game state
|
||||
interface GameState {
|
||||
position: FixedVector2;
|
||||
velocity: FixedVector2;
|
||||
}
|
||||
|
||||
// Implement predictor (must use fixed-point arithmetic)
|
||||
const predictor: IFixedPredictor<GameState, PlayerInput> = {
|
||||
predict(state: GameState, input: PlayerInput, deltaTime: Fixed32): GameState {
|
||||
const speed = Fixed32.from(100);
|
||||
const inputVec = FixedVector2.from(input.dx, input.dy);
|
||||
const velocity = inputVec.normalize().mul(speed);
|
||||
const displacement = velocity.mul(deltaTime);
|
||||
|
||||
return {
|
||||
position: state.position.add(displacement),
|
||||
velocity
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Create prediction
|
||||
const prediction = createFixedClientPrediction(predictor, {
|
||||
maxUnacknowledgedInputs: 60,
|
||||
fixedDeltaTime: Fixed32.from(1 / 60),
|
||||
reconciliationThreshold: Fixed32.from(0.001),
|
||||
enableSmoothReconciliation: false // Usually disabled for lockstep
|
||||
});
|
||||
```
|
||||
|
||||
### Record Input
|
||||
|
||||
```typescript
|
||||
function onUpdate(input: PlayerInput, currentState: GameState) {
|
||||
// Record input and get predicted state
|
||||
const predicted = prediction.recordInput(input, currentState);
|
||||
|
||||
// Render predicted state
|
||||
const pos = predicted.position.toObject();
|
||||
sprite.position.set(pos.x, pos.y);
|
||||
|
||||
// Send input
|
||||
socket.send(JSON.stringify({
|
||||
frame: prediction.currentFrame,
|
||||
input
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Server Reconciliation
|
||||
|
||||
```typescript
|
||||
// Position extractor
|
||||
const posExtractor: IFixedStatePositionExtractor<GameState> = {
|
||||
getPosition(state: GameState): FixedVector2 {
|
||||
return state.position;
|
||||
}
|
||||
};
|
||||
|
||||
// When receiving server state
|
||||
function onServerState(serverState: GameState, serverFrame: number) {
|
||||
const reconciled = prediction.reconcile(
|
||||
serverState,
|
||||
serverFrame,
|
||||
posExtractor
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Rollback and Replay
|
||||
|
||||
```typescript
|
||||
// Rollback when desync detected
|
||||
const correctedState = prediction.rollbackAndResimulate(
|
||||
serverFrame,
|
||||
authoritativeState
|
||||
);
|
||||
|
||||
// View historical state
|
||||
const historicalState = prediction.getStateAtFrame(100);
|
||||
```
|
||||
|
||||
### Preset Movement Predictor
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createFixedMovementPredictor,
|
||||
createFixedMovementPositionExtractor,
|
||||
type IFixedMovementInput,
|
||||
type IFixedMovementState
|
||||
} from '@esengine/network';
|
||||
|
||||
// Create movement predictor (speed 100 units/sec)
|
||||
const movePredictor = createFixedMovementPredictor(Fixed32.from(100));
|
||||
const posExtractor = createFixedMovementPositionExtractor();
|
||||
|
||||
const prediction = createFixedClientPrediction<IFixedMovementState, IFixedMovementInput>(
|
||||
movePredictor,
|
||||
{ fixedDeltaTime: Fixed32.from(1 / 60) }
|
||||
);
|
||||
|
||||
// Input format
|
||||
const input: IFixedMovementInput = { dx: 1, dy: 0 };
|
||||
```
|
||||
|
||||
### API Exports
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedClientPrediction,
|
||||
createFixedClientPrediction,
|
||||
createFixedMovementPredictor,
|
||||
createFixedMovementPositionExtractor,
|
||||
type IFixedInputSnapshot,
|
||||
type IFixedPredictedState,
|
||||
type IFixedPredictor,
|
||||
type IFixedStatePositionExtractor,
|
||||
type FixedClientPredictionConfig,
|
||||
type IFixedMovementInput,
|
||||
type IFixedMovementState
|
||||
} from '@esengine/network';
|
||||
```
|
||||
|
||||
@@ -235,3 +235,139 @@ const corrected = prediction.reconcile(serverState, serverSeq, applyInput);
|
||||
1. **Interpolation delay**: 100-150ms for typical networks
|
||||
2. **Prediction**: Use only for local player, interpolate remote players
|
||||
3. **Snapshot count**: Keep enough snapshots to handle network jitter
|
||||
|
||||
---
|
||||
|
||||
## Fixed-Point Sync (Lockstep)
|
||||
|
||||
For **Lockstep** architecture, use fixed-point numbers to ensure cross-platform determinism.
|
||||
|
||||
> See [Fixed-Point Numbers](/en/modules/network/fixed-point) for math basics
|
||||
|
||||
### FixedTransformState
|
||||
|
||||
Fixed-point transform state for network transmission:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
type IFixedTransformStateRaw
|
||||
} from '@esengine/network';
|
||||
|
||||
// Create state
|
||||
const state = FixedTransformState.from(100, 200, Math.PI / 4);
|
||||
|
||||
// Serialize (sender)
|
||||
const raw: IFixedTransformStateRaw = state.toRaw();
|
||||
socket.send(JSON.stringify({ type: 'sync', state: raw }));
|
||||
|
||||
// Deserialize (receiver)
|
||||
const received = FixedTransformState.fromRaw(message.state);
|
||||
|
||||
// Use for rendering
|
||||
const { x, y, rotation } = received.toFloat();
|
||||
sprite.position.set(x, y);
|
||||
```
|
||||
|
||||
State with velocity (for extrapolation):
|
||||
|
||||
```typescript
|
||||
const state = FixedTransformStateWithVelocity.from(
|
||||
100, 200, // position
|
||||
0, // rotation
|
||||
5, 3, // velocity
|
||||
0.1 // angular velocity
|
||||
);
|
||||
```
|
||||
|
||||
### Fixed-Point Interpolators
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator
|
||||
} from '@esengine/network';
|
||||
import { Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// Linear interpolator
|
||||
const interpolator = createFixedTransformInterpolator();
|
||||
|
||||
const from = FixedTransformState.from(0, 0, 0);
|
||||
const to = FixedTransformState.from(100, 50, Math.PI);
|
||||
const t = Fixed32.from(0.5);
|
||||
|
||||
const result = interpolator.interpolate(from, to, t);
|
||||
|
||||
// Hermite interpolator (smoother)
|
||||
const hermite = createFixedHermiteTransformInterpolator(100);
|
||||
```
|
||||
|
||||
### Fixed-Point Snapshot Buffer
|
||||
|
||||
Manages fixed-point state history for lockstep replay:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedSnapshotBuffer,
|
||||
createFixedSnapshotBuffer
|
||||
} from '@esengine/network';
|
||||
|
||||
// Create buffer (max 30 snapshots, 2 frame delay)
|
||||
const buffer = createFixedSnapshotBuffer<FixedTransformState>(30, 2);
|
||||
|
||||
// Add snapshots
|
||||
buffer.push({
|
||||
frame: 100,
|
||||
state: FixedTransformState.from(100, 200, 0)
|
||||
});
|
||||
|
||||
// Get interpolation snapshots
|
||||
const result = buffer.getInterpolationSnapshots(103);
|
||||
if (result) {
|
||||
const { from, to, t } = result;
|
||||
const interpolated = interpolator.interpolate(from.state, to.state, t);
|
||||
}
|
||||
|
||||
// Get latest/specific frame
|
||||
const latest = buffer.getLatest();
|
||||
const atFrame = buffer.getAtFrame(100);
|
||||
|
||||
// Rollback replay
|
||||
const snapshotsToReplay = buffer.getSnapshotsAfter(98);
|
||||
|
||||
// Clean up old snapshots
|
||||
buffer.removeSnapshotsBefore(95);
|
||||
```
|
||||
|
||||
Sub-frame interpolation:
|
||||
|
||||
```typescript
|
||||
// Use Fixed32 frame time (supports fractional frames)
|
||||
const frameTime = Fixed32.from(102.5);
|
||||
const result = buffer.getInterpolationSnapshotsFixed(frameTime);
|
||||
```
|
||||
|
||||
### API Exports
|
||||
|
||||
```typescript
|
||||
import {
|
||||
// State classes
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
type IFixedTransformStateRaw,
|
||||
type IFixedTransformStateWithVelocityRaw,
|
||||
|
||||
// Interpolators
|
||||
FixedTransformInterpolator,
|
||||
FixedHermiteTransformInterpolator,
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator,
|
||||
|
||||
// Snapshot buffer
|
||||
FixedSnapshotBuffer,
|
||||
createFixedSnapshotBuffer,
|
||||
type IFixedStateSnapshot,
|
||||
type IFixedInterpolationResult
|
||||
} from '@esengine/network';
|
||||
```
|
||||
|
||||
@@ -434,6 +434,6 @@ const found = hierarchySystem.findChild(parent, "Child");
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [实体类](./entity/) 的其他功能
|
||||
- 了解 [场景管理](./scene/) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component/) 如何定义和使用组件
|
||||
- 了解 [实体类](/guide/entity/) 的其他功能
|
||||
- 了解 [场景管理](/guide/scene/) 如何组织实体和系统
|
||||
- 了解 [组件系统](/guide/component/) 如何定义和使用组件
|
||||
|
||||
363
docs/src/content/docs/guide/persistent-entity.md
Normal file
363
docs/src/content/docs/guide/persistent-entity.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
title: "持久实体"
|
||||
---
|
||||
|
||||
# 持久实体
|
||||
|
||||
> **版本**: v2.3.0+
|
||||
|
||||
持久实体是一种特殊类型的实体,在场景切换时会自动迁移到新场景。适用于需要跨场景保持状态的游戏对象,如玩家、游戏管理器、音频管理器等。
|
||||
|
||||
## 基本概念
|
||||
|
||||
在 ECS 框架中,实体有两种生命周期策略:
|
||||
|
||||
| 策略 | 描述 | 默认 |
|
||||
|------|------|------|
|
||||
| `SceneLocal` | 场景局部实体,场景切换时销毁 | ✓ |
|
||||
| `Persistent` | 持久实体,场景切换时自动迁移 | |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建持久实体
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建持久玩家实体
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new PlayerData('Hero', 500));
|
||||
|
||||
// 创建普通敌人实体(场景切换时销毁)
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(300, 200));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换时的行为
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始场景
|
||||
class Level1Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 玩家 - 持久实体,将迁移到下一个场景
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 敌人 - 场景局部实体,场景切换时销毁
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// 目标场景
|
||||
class Level2Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 新敌人
|
||||
const enemy = this.createEntity('Boss');
|
||||
enemy.addComponent(new Position(200, 200));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 玩家已自动迁移到此场景
|
||||
const player = this.findEntity('Player');
|
||||
console.log(player !== null); // true
|
||||
|
||||
// 位置和生命值数据完整保留
|
||||
const position = player?.getComponent(Position);
|
||||
const health = player?.getComponent(Health);
|
||||
console.log(position?.x, position?.y); // 0, 0
|
||||
console.log(health?.value); // 100
|
||||
}
|
||||
}
|
||||
|
||||
// 切换场景
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(new Level1Scene());
|
||||
|
||||
// 稍后切换到 Level2
|
||||
Core.loadScene(new Level2Scene());
|
||||
// 玩家实体自动迁移,敌人实体被销毁
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 实体方法
|
||||
|
||||
#### setPersistent()
|
||||
|
||||
将实体标记为持久实体,防止在场景切换时被销毁。
|
||||
|
||||
```typescript
|
||||
public setPersistent(): this
|
||||
```
|
||||
|
||||
**返回值**: 返回实体本身,支持链式调用
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
player.addComponent(new Position(100, 200));
|
||||
```
|
||||
|
||||
#### setSceneLocal()
|
||||
|
||||
将实体恢复为场景局部策略(默认)。
|
||||
|
||||
```typescript
|
||||
public setSceneLocal(): this
|
||||
```
|
||||
|
||||
**返回值**: 返回实体本身,支持链式调用
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
// 动态取消持久性
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
#### isPersistent
|
||||
|
||||
检查实体是否为持久实体。
|
||||
|
||||
```typescript
|
||||
public get isPersistent(): boolean
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
if (entity.isPersistent) {
|
||||
console.log('这是一个持久实体');
|
||||
}
|
||||
```
|
||||
|
||||
#### lifecyclePolicy
|
||||
|
||||
获取实体的生命周期策略。
|
||||
|
||||
```typescript
|
||||
public get lifecyclePolicy(): EEntityLifecyclePolicy
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
|
||||
console.log('持久实体');
|
||||
}
|
||||
```
|
||||
|
||||
### 场景方法
|
||||
|
||||
#### findPersistentEntities()
|
||||
|
||||
查找场景中所有持久实体。
|
||||
|
||||
```typescript
|
||||
public findPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**返回值**: 持久实体数组
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const persistentEntities = scene.findPersistentEntities();
|
||||
console.log(`场景中有 ${persistentEntities.length} 个持久实体`);
|
||||
```
|
||||
|
||||
#### extractPersistentEntities()
|
||||
|
||||
提取并移除场景中所有持久实体(通常由框架内部调用)。
|
||||
|
||||
```typescript
|
||||
public extractPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**返回值**: 被提取的持久实体数组
|
||||
|
||||
#### receiveMigratedEntities()
|
||||
|
||||
接收迁移的实体(通常由框架内部调用)。
|
||||
|
||||
```typescript
|
||||
public receiveMigratedEntities(entities: Entity[]): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `entities` - 要接收的实体数组
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 跨关卡的玩家实体
|
||||
|
||||
```typescript
|
||||
class PlayerSetupScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 玩家在所有关卡中保持状态
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Transform(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new Inventory());
|
||||
player.addComponent(new PlayerStats());
|
||||
}
|
||||
}
|
||||
|
||||
class Level1 extends Scene { /* ... */ }
|
||||
class Level2 extends Scene { /* ... */ }
|
||||
class Level3 extends Scene { /* ... */ }
|
||||
|
||||
// 玩家实体在所有关卡之间自动迁移
|
||||
Core.setScene(new PlayerSetupScene());
|
||||
// ... 游戏进行
|
||||
Core.loadScene(new Level1());
|
||||
// ... 关卡完成
|
||||
Core.loadScene(new Level2());
|
||||
// 玩家数据(生命值、背包、属性)完整保留
|
||||
```
|
||||
|
||||
### 2. 全局管理器
|
||||
|
||||
```typescript
|
||||
class BootstrapScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 音频管理器 - 跨场景持久
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent();
|
||||
audioManager.addComponent(new AudioController());
|
||||
|
||||
// 成就管理器 - 跨场景持久
|
||||
const achievementManager = this.createEntity('AchievementManager').setPersistent();
|
||||
achievementManager.addComponent(new AchievementTracker());
|
||||
|
||||
// 游戏设置 - 跨场景持久
|
||||
const settings = this.createEntity('GameSettings').setPersistent();
|
||||
settings.addComponent(new SettingsData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动态切换持久性
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 初始创建为普通实体
|
||||
const companion = this.createEntity('Companion');
|
||||
companion.addComponent(new Transform(0, 0));
|
||||
companion.addComponent(new CompanionAI());
|
||||
|
||||
// 监听招募事件
|
||||
this.eventSystem.on('companion:recruited', () => {
|
||||
// 招募后变为持久实体
|
||||
companion.setPersistent();
|
||||
console.log('同伴加入队伍,将跟随玩家跨场景');
|
||||
});
|
||||
|
||||
// 监听解散事件
|
||||
this.eventSystem.on('companion:dismissed', () => {
|
||||
// 解散后恢复为场景局部实体
|
||||
companion.setSceneLocal();
|
||||
console.log('同伴离开队伍,不再跨场景持久');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确标识持久实体
|
||||
|
||||
```typescript
|
||||
// 推荐:创建时立即标记
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
|
||||
// 不推荐:创建后再标记(容易遗忘)
|
||||
const player = this.createEntity('Player');
|
||||
// ... 大量代码 ...
|
||||
player.setPersistent(); // 容易忘记
|
||||
```
|
||||
|
||||
### 2. 合理使用持久性
|
||||
|
||||
```typescript
|
||||
// ✓ 适合持久化的实体
|
||||
const player = this.createEntity('Player').setPersistent(); // 玩家
|
||||
const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统
|
||||
|
||||
// ✗ 不应该持久化的实体
|
||||
const bullet = this.createEntity('Bullet'); // 临时对象
|
||||
const enemy = this.createEntity('Enemy'); // 关卡特定敌人
|
||||
const particle = this.createEntity('Particle'); // 特效粒子
|
||||
```
|
||||
|
||||
### 3. 检查迁移的实体
|
||||
|
||||
```typescript
|
||||
class NewScene extends Scene {
|
||||
public onStart(): void {
|
||||
// 检查预期的持久实体是否存在
|
||||
const player = this.findEntity('Player');
|
||||
if (!player) {
|
||||
console.error('玩家实体未正确迁移!');
|
||||
// 处理错误情况
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// ✗ 避免:持久实体引用场景局部实体
|
||||
class BadScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// 危险:player 是持久的但 enemy 不是
|
||||
// 场景切换后,enemy 被销毁,引用变为无效
|
||||
player.addComponent(new TargetComponent(enemy));
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ 推荐:使用 ID 引用或事件系统
|
||||
class GoodScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// 存储 ID 而非直接引用
|
||||
player.addComponent(new TargetComponent(enemy.id));
|
||||
|
||||
// 或使用事件系统通信
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 重要说明
|
||||
|
||||
1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久也不会迁移。
|
||||
|
||||
2. **组件数据完整保留**:迁移过程中所有组件及其状态都会被保留。
|
||||
|
||||
3. **场景引用会更新**:迁移后,实体的 `scene` 属性将指向新场景。
|
||||
|
||||
4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。
|
||||
|
||||
5. **延迟切换同样有效**:使用 `Core.loadScene()` 进行延迟切换时,持久实体同样会迁移。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景](/guide/scene/) - 了解场景基础知识
|
||||
- [场景管理器](/guide/scene-manager/) - 了解场景切换
|
||||
@@ -16,7 +16,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
## 支持的平台
|
||||
|
||||
### 🌐 [浏览器适配器](./platform-adapter/browser/)
|
||||
### 🌐 [浏览器适配器](/guide/platform-adapter/browser/)
|
||||
|
||||
支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。
|
||||
|
||||
@@ -30,7 +30,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
---
|
||||
|
||||
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame/)
|
||||
### 📱 [微信小游戏适配器](/guide/platform-adapter/wechat-minigame/)
|
||||
|
||||
专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
@@ -44,7 +44,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ [Node.js适配器](./platform-adapter/nodejs/)
|
||||
### 🖥️ [Node.js适配器](/guide/platform-adapter/nodejs/)
|
||||
|
||||
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
|
||||
|
||||
|
||||
439
docs/src/content/docs/guide/scene-manager.md
Normal file
439
docs/src/content/docs/guide/scene-manager.md
Normal file
@@ -0,0 +1,439 @@
|
||||
---
|
||||
title: "场景管理器"
|
||||
---
|
||||
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
## 适用场景
|
||||
|
||||
SceneManager 适用于:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要场景切换的游戏(菜单、游戏、暂停等)
|
||||
- 不需要多 World 隔离的项目
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 轻量级,零额外开销
|
||||
- 简单直观的 API
|
||||
- 支持延迟场景切换(避免在帧中途切换)
|
||||
- 自动 ECS 流式 API 管理
|
||||
- 自动场景生命周期处理
|
||||
- 与 Core 集成,自动更新
|
||||
- 支持 [持久实体](/guide/persistent-entity/) 跨场景迁移(v2.3.0+)
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 推荐:使用 Core 的静态方法
|
||||
|
||||
这是最简单且推荐的方式,适用于大多数应用:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. 创建并设置场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 创建初始实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置场景
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. 游戏循环(Core.update 自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// Laya 引擎集成
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator 集成
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 进阶:直接使用 SceneManager
|
||||
|
||||
如果需要更多控制,可以直接使用 SceneManager:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 获取 SceneManager(已由 Core 自动创建并注册)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环(仍然使用 Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core 自动调用 sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**重要提示**:无论使用哪种方式,在游戏循环中只需调用 `Core.update()`。它会自动更新 SceneManager 和场景。无需手动调用 `sceneManager.update()`。
|
||||
|
||||
## 场景切换
|
||||
|
||||
### 立即切换
|
||||
|
||||
使用 `Core.setScene()` 或 `sceneManager.setScene()` 立即切换场景:
|
||||
|
||||
```typescript
|
||||
// 方法 1:使用 Core(推荐)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 方法 2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### 延迟切换
|
||||
|
||||
使用 `Core.loadScene()` 或 `sceneManager.loadScene()` 进行延迟场景切换,在下一帧生效:
|
||||
|
||||
```typescript
|
||||
// 方法 1:使用 Core(推荐)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// 方法 2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
在 System 中切换场景时,使用延迟切换:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// 延迟切换到游戏结束场景(下一帧生效)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续执行,不会中断当前系统处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法(推荐)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要设置的场景实例
|
||||
|
||||
**返回值**:
|
||||
- 返回设置的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
延迟场景加载(下一帧切换)。
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要加载的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
获取当前活动场景。
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**返回值**:
|
||||
- 当前场景实例,如果没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`当前场景: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager 方法(进阶)
|
||||
|
||||
如果需要直接使用 SceneManager,通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
延迟场景加载。
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
获取当前场景。
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
检查是否有活动场景。
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
检查是否有待处理的场景切换。
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 Core 的静态方法
|
||||
|
||||
```typescript
|
||||
// 推荐:使用 Core 的静态方法
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 不推荐:除非有特殊需求,否则不要直接使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. 只调用 Core.update()
|
||||
|
||||
```typescript
|
||||
// 正确:只调用 Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// 错误:不要手动调用 sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // 重复更新,会导致问题!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用延迟切换避免问题
|
||||
|
||||
在 System 中切换场景时,使用 `loadScene()` 而不是 `setScene()`:
|
||||
|
||||
```typescript
|
||||
// 推荐:延迟切换
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续处理其他实体
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:立即切换可能导致问题
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// 场景立即切换,当前帧其他实体可能无法正确处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 场景职责分离
|
||||
|
||||
每个场景应该只负责一个特定的游戏状态:
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏逻辑
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// 只处理暂停界面逻辑
|
||||
}
|
||||
|
||||
// 避免这种设计 - 职责混杂
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、暂停和所有其他逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 资源管理
|
||||
|
||||
在场景的 `unload()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 事件驱动的场景切换
|
||||
|
||||
使用事件系统触发场景切换,保持代码解耦:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听场景切换事件
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中触发事件
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构概览
|
||||
|
||||
SceneManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core(全局服务)
|
||||
└── SceneManager(场景管理,自动更新)
|
||||
└── Scene(当前场景)
|
||||
├── EntitySystem(系统)
|
||||
├── Entity(实体)
|
||||
└── Component(组件)
|
||||
```
|
||||
|
||||
## 与 WorldManager 的比较
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多个 World,每个包含多个场景 |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要切换但不需要同时运行的场景
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [持久实体](/guide/persistent-entity/) - 了解如何在场景切换时保持实体
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松管理场景切换。
|
||||
@@ -305,11 +305,11 @@ const tree = BehaviorTreeBuilder.create('Timeout')
|
||||
|
||||
### Cocos Creator集成
|
||||
|
||||
参见[Cocos Creator集成指南](./cocos-integration/)
|
||||
参见[Cocos Creator集成指南](/modules/behavior-tree/cocos-integration/)
|
||||
|
||||
### LayaAir集成
|
||||
|
||||
参见[LayaAir集成指南](./laya-integration/)
|
||||
参见[LayaAir集成指南](/modules/behavior-tree/laya-integration/)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
@@ -389,6 +389,6 @@ const tree = BehaviorTreeBuilder.create('AI')
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](./best-practices/)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](./editor-guide/)学习可视化编辑
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](/modules/behavior-tree/editor-guide/)学习可视化编辑
|
||||
|
||||
@@ -503,6 +503,6 @@ console.log(json);
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[Cocos Creator 集成](./cocos-integration/)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices/)优化你的行为树设计
|
||||
- 学习[Cocos Creator 集成](/modules/behavior-tree/cocos-integration/)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的行为树设计
|
||||
|
||||
@@ -26,7 +26,7 @@ Root Selector
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)。
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 使用内置节点
|
||||
@@ -465,6 +465,6 @@ export class SmartUpdate implements INodeExecutor {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[自定义节点执行器](./custom-actions/)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage/)了解更多技巧
|
||||
- 参考[核心概念](./core-concepts/)深入理解原理
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)扩展行为树功能
|
||||
- 探索[高级用法](/modules/behavior-tree/advanced-usage/)了解更多技巧
|
||||
- 参考[核心概念](/modules/behavior-tree/core-concepts/)深入理解原理
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "Cocos Creator 集成"
|
||||
|
||||
- Cocos Creator 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started/)教程
|
||||
- 已完成[快速开始](/modules/behavior-tree/getting-started/)教程
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -679,7 +679,7 @@ const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management/)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](./advanced-usage/)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](./best-practices/)优化你的 AI
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 查看[资产管理](/modules/behavior-tree/asset-management/)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](/modules/behavior-tree/advanced-usage/)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的 AI
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
|
||||
|
||||
@@ -192,7 +192,7 @@ const tree = BehaviorTreeBuilder.create('Actions')
|
||||
.build();
|
||||
```
|
||||
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)。
|
||||
|
||||
#### Condition(条件)
|
||||
|
||||
@@ -487,7 +487,7 @@ NodeRuntimeState
|
||||
|
||||
现在你已经理解了行为树的核心概念,接下来可以:
|
||||
|
||||
- 查看[快速开始](./getting-started/)创建第一个行为树
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义节点
|
||||
- 探索[高级用法](./advanced-usage/)了解更多功能
|
||||
- 阅读[最佳实践](./best-practices/)学习设计模式
|
||||
- 查看[快速开始](/modules/behavior-tree/getting-started/)创建第一个行为树
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义节点
|
||||
- 探索[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)学习设计模式
|
||||
|
||||
@@ -1123,6 +1123,6 @@ execute(context: NodeExecutionContext): TaskStatus {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[编辑器工作流](./editor-workflow/)了解如何在编辑器中使用自定义节点
|
||||
- 阅读[最佳实践](./best-practices/)学习行为树设计模式
|
||||
- 查看[高级用法](./advanced-usage/)了解更多功能
|
||||
- 学习[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解如何在编辑器中使用自定义节点
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)学习行为树设计模式
|
||||
- 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
|
||||
|
||||
@@ -117,5 +117,5 @@ BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[编辑器工作流](./editor-workflow/)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何扩展节点
|
||||
- 查看[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何扩展节点
|
||||
|
||||
@@ -112,7 +112,7 @@ setInterval(() => {
|
||||
|
||||
## 实现自定义执行器
|
||||
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions/)):
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](/modules/behavior-tree/custom-actions/)):
|
||||
|
||||
```typescript
|
||||
import {
|
||||
@@ -250,6 +250,6 @@ setInterval(() => {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
- 查看[高级用法](./advanced-usage/)了解性能优化等高级特性
|
||||
- 查看[最佳实践](./best-practices/)优化你的AI设计
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
|
||||
- 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解性能优化等高级特性
|
||||
- 查看[最佳实践](/modules/behavior-tree/best-practices/)优化你的AI设计
|
||||
|
||||
@@ -333,11 +333,11 @@ BehaviorTreeStarter.restart(entity);
|
||||
|
||||
现在你已经创建了第一个行为树,接下来可以:
|
||||
|
||||
1. 学习[核心概念](./core-concepts/)深入理解行为树原理
|
||||
2. 学习[资产管理](./asset-management/)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration/) 或 [Node.js](./nodejs-usage.md)
|
||||
5. 查看[高级用法](./advanced-usage/)了解更多功能
|
||||
1. 学习[核心概念](/modules/behavior-tree/core-concepts/)深入理解行为树原理
|
||||
2. 学习[资产管理](/modules/behavior-tree/asset-management/)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](/modules/behavior-tree/cocos-integration/) 或 [Node.js](/modules/behavior-tree/nodejs-usage/)
|
||||
5. 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
|
||||
|
||||
## 常见问题
|
||||
|
||||
@@ -384,4 +384,4 @@ console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
|
||||
内置的`executeAction`和`executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
|
||||
|
||||
参见[自定义节点执行器](./custom-actions/)学习如何创建。
|
||||
参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建。
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "Laya 引擎集成"
|
||||
|
||||
- LayaAir 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started/)教程
|
||||
- 已完成[快速开始](/modules/behavior-tree/getting-started/)教程
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -311,5 +311,5 @@ class AIManager {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[高级用法](./advanced-usage/)
|
||||
- 学习[最佳实践](./best-practices/)
|
||||
- 查看[高级用法](/modules/behavior-tree/advanced-usage/)
|
||||
- 学习[最佳实践](/modules/behavior-tree/best-practices/)
|
||||
|
||||
@@ -577,6 +577,6 @@ function loadAIState(entity: Entity, savedState: any) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management/)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices/)优化你的服务端AI
|
||||
- 查看[资产管理](/modules/behavior-tree/asset-management/)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的服务端AI
|
||||
|
||||
315
docs/src/content/docs/modules/blueprint/cocos-editor.md
Normal file
315
docs/src/content/docs/modules/blueprint/cocos-editor.md
Normal file
@@ -0,0 +1,315 @@
|
||||
---
|
||||
title: "Cocos Creator 蓝图编辑器"
|
||||
description: "在 Cocos Creator 中使用蓝图可视化脚本系统"
|
||||
---
|
||||
|
||||
本文档介绍如何在 Cocos Creator 项目中安装和使用蓝图可视化脚本编辑器扩展。
|
||||
|
||||
## 安装扩展
|
||||
|
||||
### 1. 下载扩展
|
||||
|
||||
从发布页面下载 `cocos-node-editor.zip` 扩展包。
|
||||
|
||||
### 2. 导入扩展
|
||||
|
||||
1. 打开 Cocos Creator
|
||||
2. 进入 **扩展 → 扩展管理器**
|
||||
3. 点击 **导入扩展包** 按钮
|
||||
4. 选择下载的 `cocos-node-editor.zip` 文件
|
||||
5. 导入后启用扩展
|
||||
|
||||
## 打开蓝图编辑器
|
||||
|
||||
通过菜单 **面板 → Node Editor** 打开蓝图编辑器面板。
|
||||
|
||||
### 首次打开 - 安装依赖
|
||||
|
||||
首次打开面板时,插件会检测项目中是否安装了 `@esengine/blueprint` 依赖包。如果未安装,会显示 **"缺少必要的依赖包"** 提示,点击 **"安装依赖"** 按钮即可自动安装。
|
||||
|
||||
## 编辑器界面
|
||||
|
||||
### 工具栏
|
||||
|
||||
| 按钮 | 快捷键 | 功能 |
|
||||
|------|--------|------|
|
||||
| 新建 | - | 创建空白蓝图 |
|
||||
| 加载 | - | 从文件加载蓝图 |
|
||||
| 保存 | `Ctrl+S` | 保存蓝图到文件 |
|
||||
| 撤销 | `Ctrl+Z` | 撤销上一步操作 |
|
||||
| 重做 | `Ctrl+Shift+Z` | 重做操作 |
|
||||
| 剪切 | `Ctrl+X` | 剪切选中节点 |
|
||||
| 复制 | `Ctrl+C` | 复制选中节点 |
|
||||
| 粘贴 | `Ctrl+V` | 粘贴节点 |
|
||||
| 删除 | `Delete` | 删除选中项 |
|
||||
| 重新扫描 | - | 重新扫描项目中的蓝图节点 |
|
||||
|
||||
### 画布操作
|
||||
|
||||
- **右键单击画布**:打开节点添加菜单
|
||||
- **拖拽节点**:移动节点位置
|
||||
- **点击节点**:选中节点
|
||||
- **Ctrl+点击**:多选节点
|
||||
- **拖拽引脚到引脚**:创建连接
|
||||
- **滚轮**:缩放画布
|
||||
- **中键拖拽**:平移画布
|
||||
|
||||
### 节点菜单
|
||||
|
||||
右键单击画布后会显示节点菜单:
|
||||
|
||||
- 顶部搜索框可以快速搜索节点
|
||||
- 节点按类别分组显示
|
||||
- 按 `Enter` 快速添加第一个搜索结果
|
||||
- 按 `Esc` 关闭菜单
|
||||
|
||||
## 蓝图文件格式
|
||||
|
||||
蓝图保存为 `.blueprint.json` 文件,格式与运行时完全兼容:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "blueprint",
|
||||
"metadata": {
|
||||
"name": "My Blueprint",
|
||||
"createdAt": 1704307200000,
|
||||
"modifiedAt": 1704307200000
|
||||
},
|
||||
"variables": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node-1",
|
||||
"type": "PrintString",
|
||||
"position": { "x": 100, "y": 200 },
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "conn-1",
|
||||
"fromNodeId": "node-1",
|
||||
"fromPin": "exec",
|
||||
"toNodeId": "node-2",
|
||||
"toPin": "exec"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 在游戏中运行蓝图
|
||||
|
||||
`@esengine/blueprint` 包已提供完整的 ECS 集成,包括 `BlueprintComponent` 和 `BlueprintSystem`,可以直接使用。
|
||||
|
||||
### 1. 添加蓝图系统到场景
|
||||
|
||||
```typescript
|
||||
import { BlueprintSystem } from '@esengine/blueprint';
|
||||
|
||||
// 在场景初始化时添加蓝图系统
|
||||
scene.addSystem(new BlueprintSystem());
|
||||
```
|
||||
|
||||
### 2. 加载蓝图并添加到实体
|
||||
|
||||
```typescript
|
||||
import { resources, JsonAsset } from 'cc';
|
||||
import { BlueprintComponent, validateBlueprintAsset, BlueprintAsset } from '@esengine/blueprint';
|
||||
|
||||
// 加载蓝图资产
|
||||
async function loadBlueprint(path: string): Promise<BlueprintAsset | null> {
|
||||
return new Promise((resolve) => {
|
||||
resources.load(path, JsonAsset, (err, asset) => {
|
||||
if (err || !asset) {
|
||||
console.error('Failed to load blueprint:', err);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = asset.json;
|
||||
if (validateBlueprintAsset(data)) {
|
||||
resolve(data as BlueprintAsset);
|
||||
} else {
|
||||
console.error('Invalid blueprint format');
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 创建带蓝图的实体
|
||||
async function createBlueprintEntity(scene: IScene, blueprintPath: string): Promise<Entity> {
|
||||
const entity = scene.createEntity('BlueprintEntity');
|
||||
|
||||
const bpComponent = entity.addComponent(BlueprintComponent);
|
||||
bpComponent.blueprintPath = blueprintPath;
|
||||
bpComponent.blueprintAsset = await loadBlueprint(blueprintPath);
|
||||
|
||||
return entity;
|
||||
}
|
||||
```
|
||||
|
||||
### BlueprintComponent 属性
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `blueprintAsset` | `BlueprintAsset \| null` | 蓝图资产数据 |
|
||||
| `blueprintPath` | `string` | 蓝图资产路径(用于序列化) |
|
||||
| `autoStart` | `boolean` | 是否自动开始执行(默认 `true`) |
|
||||
| `debug` | `boolean` | 是否启用调试模式 |
|
||||
|
||||
### BlueprintComponent 方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `start()` | 手动开始执行蓝图 |
|
||||
| `stop()` | 停止蓝图执行 |
|
||||
| `cleanup()` | 清理蓝图资源 |
|
||||
|
||||
## 创建自定义节点
|
||||
|
||||
### 使用装饰器标记组件
|
||||
|
||||
推荐使用装饰器让组件自动生成蓝图节点:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
|
||||
|
||||
@ECSComponent('Health')
|
||||
@BlueprintExpose({ displayName: '生命值组件' })
|
||||
export class HealthComponent extends Component {
|
||||
@BlueprintProperty({ displayName: '当前生命值', category: 'number' })
|
||||
current: number = 100;
|
||||
|
||||
@BlueprintProperty({ displayName: '最大生命值', category: 'number' })
|
||||
max: number = 100;
|
||||
|
||||
@BlueprintMethod({ displayName: '治疗', isExec: true })
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.current + amount, this.max);
|
||||
}
|
||||
|
||||
@BlueprintMethod({ displayName: '受伤', isExec: true })
|
||||
takeDamage(amount: number): void {
|
||||
this.current = Math.max(this.current - amount, 0);
|
||||
}
|
||||
|
||||
@BlueprintMethod({ displayName: '是否死亡' })
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册组件节点
|
||||
|
||||
```typescript
|
||||
import { registerAllComponentNodes } from '@esengine/blueprint';
|
||||
|
||||
// 在应用启动时注册所有标记的组件
|
||||
registerAllComponentNodes();
|
||||
```
|
||||
|
||||
### 手动定义节点(高级)
|
||||
|
||||
如需完全自定义节点逻辑:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BlueprintNodeTemplate,
|
||||
INodeExecutor,
|
||||
RegisterNode,
|
||||
ExecutionContext,
|
||||
ExecutionResult
|
||||
} from '@esengine/blueprint';
|
||||
|
||||
const MyNodeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'MyCustomNode',
|
||||
title: '我的自定义节点',
|
||||
category: 'custom',
|
||||
description: '自定义节点示例',
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
|
||||
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
|
||||
{ name: 'result', type: 'number', direction: 'output' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = context.getInput<number>(node.id, 'value');
|
||||
return {
|
||||
outputs: { result: value * 2 },
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 节点类别
|
||||
|
||||
| 类别 | 说明 | 颜色 |
|
||||
|------|------|------|
|
||||
| `event` | 事件节点 | 红色 |
|
||||
| `flow` | 流程控制 | 灰色 |
|
||||
| `entity` | 实体操作 | 蓝色 |
|
||||
| `component` | 组件访问 | 青色 |
|
||||
| `math` | 数学运算 | 绿色 |
|
||||
| `logic` | 逻辑运算 | 红色 |
|
||||
| `variable` | 变量访问 | 紫色 |
|
||||
| `time` | 时间工具 | 青色 |
|
||||
| `debug` | 调试工具 | 灰色 |
|
||||
| `custom` | 自定义节点 | 蓝灰色 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **文件组织**
|
||||
- 将蓝图文件放在 `assets/blueprints/` 目录下
|
||||
- 使用有意义的文件名,如 `player-controller.blueprint.json`
|
||||
|
||||
2. **组件设计**
|
||||
- 使用 `@BlueprintExpose` 标记需要暴露给蓝图的组件
|
||||
- 为属性和方法提供清晰的 `displayName`
|
||||
- 将执行方法标记为 `isExec: true`
|
||||
|
||||
3. **性能考虑**
|
||||
- 避免在 Tick 事件中执行重计算
|
||||
- 使用变量缓存中间结果
|
||||
- 纯函数节点会自动缓存输出
|
||||
|
||||
4. **调试技巧**
|
||||
- 使用 Print 节点输出中间值
|
||||
- 启用 `vm.debug = true` 查看执行日志
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 节点菜单是空的?
|
||||
|
||||
A: 点击 **重新扫描** 按钮扫描项目中的蓝图节点类。确保已调用 `registerAllComponentNodes()`。
|
||||
|
||||
### Q: 蓝图不执行?
|
||||
|
||||
A: 检查:
|
||||
1. 实体是否添加了 `BlueprintComponent`
|
||||
2. `BlueprintExecutionSystem` 是否注册到场景
|
||||
3. `blueprintAsset` 是否正确加载
|
||||
4. `autoStart` 是否为 `true`
|
||||
|
||||
### Q: 如何触发自定义事件?
|
||||
|
||||
A: 通过 VM 触发:
|
||||
```typescript
|
||||
const bp = entity.getComponent(BlueprintComponent);
|
||||
bp.vm?.triggerCustomEvent('OnPickup', { item: itemEntity });
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [蓝图运行时 API](/modules/blueprint/) - BlueprintVM 和核心 API
|
||||
- [自定义节点](/modules/blueprint/custom-nodes) - 详细的节点创建指南
|
||||
- [内置节点](/modules/blueprint/nodes) - 内置节点参考
|
||||
@@ -3,6 +3,164 @@ title: "自定义节点"
|
||||
description: "创建自定义蓝图节点"
|
||||
---
|
||||
|
||||
## 蓝图装饰器
|
||||
|
||||
使用装饰器可以快速将 ECS 组件暴露为蓝图节点。
|
||||
|
||||
### @BlueprintComponent
|
||||
|
||||
将组件类标记为蓝图可用:
|
||||
|
||||
```typescript
|
||||
import { BlueprintComponent, BlueprintProperty } from '@esengine/blueprint';
|
||||
|
||||
@BlueprintComponent({
|
||||
title: '玩家控制器',
|
||||
category: 'gameplay',
|
||||
color: '#4a90d9',
|
||||
description: '控制玩家移动和交互'
|
||||
})
|
||||
class PlayerController extends Component {
|
||||
@BlueprintProperty({ displayName: '移动速度' })
|
||||
speed: number = 100;
|
||||
|
||||
@BlueprintProperty({ displayName: '跳跃高度' })
|
||||
jumpHeight: number = 200;
|
||||
}
|
||||
```
|
||||
|
||||
### @BlueprintProperty
|
||||
|
||||
将组件属性暴露为节点输入:
|
||||
|
||||
```typescript
|
||||
@BlueprintProperty({
|
||||
displayName: '生命值',
|
||||
description: '当前生命值',
|
||||
isInput: true,
|
||||
isOutput: true
|
||||
})
|
||||
health: number = 100;
|
||||
```
|
||||
|
||||
### @BlueprintArray
|
||||
|
||||
用于数组类型属性,支持复杂对象数组的编辑:
|
||||
|
||||
```typescript
|
||||
import { BlueprintArray, Schema } from '@esengine/blueprint';
|
||||
|
||||
interface Waypoint {
|
||||
position: { x: number; y: number };
|
||||
waitTime: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
@BlueprintComponent({
|
||||
title: '巡逻路径',
|
||||
category: 'ai'
|
||||
})
|
||||
class PatrolPath extends Component {
|
||||
@BlueprintArray({
|
||||
displayName: '路径点',
|
||||
description: '巡逻路径的各个点',
|
||||
itemSchema: Schema.object({
|
||||
position: Schema.vector2({ defaultValue: { x: 0, y: 0 } }),
|
||||
waitTime: Schema.float({ min: 0, max: 10, defaultValue: 1.0 }),
|
||||
speed: Schema.float({ min: 0, max: 500, defaultValue: 100 })
|
||||
}),
|
||||
reorderable: true,
|
||||
exposeElementPorts: true,
|
||||
portNameTemplate: '路径点 {index1}'
|
||||
})
|
||||
waypoints: Waypoint[] = [];
|
||||
}
|
||||
```
|
||||
|
||||
## Schema 类型系统
|
||||
|
||||
Schema 用于定义复杂数据结构的类型信息,支持编辑器自动生成对应的 UI。
|
||||
|
||||
### 基础类型
|
||||
|
||||
```typescript
|
||||
import { Schema } from '@esengine/blueprint';
|
||||
|
||||
// 数字类型
|
||||
Schema.float({ min: 0, max: 100, defaultValue: 50, step: 0.1 })
|
||||
Schema.int({ min: 0, max: 10, defaultValue: 5 })
|
||||
|
||||
// 字符串
|
||||
Schema.string({ defaultValue: 'Hello', multiline: false, placeholder: '输入文本...' })
|
||||
|
||||
// 布尔
|
||||
Schema.boolean({ defaultValue: true })
|
||||
|
||||
// 向量
|
||||
Schema.vector2({ defaultValue: { x: 0, y: 0 } })
|
||||
Schema.vector3({ defaultValue: { x: 0, y: 0, z: 0 } })
|
||||
```
|
||||
|
||||
### 复合类型
|
||||
|
||||
```typescript
|
||||
// 对象
|
||||
Schema.object({
|
||||
name: Schema.string({ defaultValue: '' }),
|
||||
health: Schema.float({ min: 0, max: 100 }),
|
||||
position: Schema.vector2()
|
||||
})
|
||||
|
||||
// 数组
|
||||
Schema.array({
|
||||
items: Schema.float(),
|
||||
minItems: 0,
|
||||
maxItems: 10
|
||||
})
|
||||
|
||||
// 枚举
|
||||
Schema.enum({
|
||||
options: ['idle', 'walk', 'run', 'jump'],
|
||||
defaultValue: 'idle'
|
||||
})
|
||||
|
||||
// 引用
|
||||
Schema.ref({ refType: 'entity' })
|
||||
Schema.ref({ refType: 'asset', assetType: 'texture' })
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```typescript
|
||||
@BlueprintComponent({ title: '敌人配置', category: 'ai' })
|
||||
class EnemyConfig extends Component {
|
||||
@BlueprintArray({
|
||||
displayName: '攻击模式',
|
||||
itemSchema: Schema.object({
|
||||
name: Schema.string({ defaultValue: '普通攻击' }),
|
||||
damage: Schema.float({ min: 0, max: 100, defaultValue: 10 }),
|
||||
cooldown: Schema.float({ min: 0, max: 10, defaultValue: 1 }),
|
||||
range: Schema.float({ min: 0, max: 500, defaultValue: 50 }),
|
||||
animation: Schema.string({ defaultValue: 'attack_01' })
|
||||
}),
|
||||
reorderable: true
|
||||
})
|
||||
attackPatterns: AttackPattern[] = [];
|
||||
|
||||
@BlueprintProperty({
|
||||
displayName: '巡逻区域',
|
||||
schema: Schema.object({
|
||||
center: Schema.vector2(),
|
||||
radius: Schema.float({ min: 0, defaultValue: 100 })
|
||||
})
|
||||
})
|
||||
patrolArea: { center: { x: number; y: number }; radius: number } = {
|
||||
center: { x: 0, y: 0 },
|
||||
radius: 100
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 定义节点模板
|
||||
|
||||
```typescript
|
||||
@@ -33,16 +191,11 @@ import { INodeExecutor, RegisterNode, BlueprintNode, ExecutionContext, Execution
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
// 获取输入(使用 evaluateInput)
|
||||
const value = context.evaluateInput(node.id, 'value', 0) as number;
|
||||
|
||||
// 执行逻辑
|
||||
const result = value * 2;
|
||||
|
||||
// 返回结果
|
||||
return {
|
||||
outputs: { result },
|
||||
nextExec: 'exec' // 继续执行
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -64,19 +217,10 @@ NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
|
||||
```typescript
|
||||
import { NodeRegistry } from '@esengine/blueprint';
|
||||
|
||||
// 获取单例
|
||||
const registry = NodeRegistry.instance;
|
||||
|
||||
// 获取所有模板
|
||||
const allTemplates = registry.getAllTemplates();
|
||||
|
||||
// 按类别获取
|
||||
const mathNodes = registry.getTemplatesByCategory('math');
|
||||
|
||||
// 搜索节点
|
||||
const results = registry.searchTemplates('add');
|
||||
|
||||
// 检查是否存在
|
||||
if (registry.has('MyCustomNode')) { ... }
|
||||
```
|
||||
|
||||
@@ -89,7 +233,7 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDistance',
|
||||
title: 'Get Distance',
|
||||
category: 'math',
|
||||
isPure: true, // 标记为纯节点
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'vector2', direction: 'input' },
|
||||
{ name: 'b', type: 'vector2', direction: 'input' }
|
||||
@@ -99,59 +243,3 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## 实际示例:ECS 组件操作节点
|
||||
|
||||
```typescript
|
||||
import type { Entity } from '@esengine/ecs-framework';
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
|
||||
import { ExecutionContext, ExecutionResult } from '@esengine/blueprint';
|
||||
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
|
||||
|
||||
// 自定义治疗节点
|
||||
const HealEntityTemplate: BlueprintNodeTemplate = {
|
||||
type: 'HealEntity',
|
||||
title: 'Heal Entity',
|
||||
category: 'gameplay',
|
||||
color: '#22aa22',
|
||||
description: 'Heal an entity with HealthComponent',
|
||||
keywords: ['heal', 'health', 'restore'],
|
||||
menuPath: ['Gameplay', 'Combat', 'Heal Entity'],
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', displayName: '' },
|
||||
{ name: 'entity', type: 'entity', displayName: 'Target' },
|
||||
{ name: 'amount', type: 'float', displayName: 'Amount', defaultValue: 10 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', displayName: '' },
|
||||
{ name: 'newHealth', type: 'float', displayName: 'New Health' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(HealEntityTemplate)
|
||||
class HealEntityExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
|
||||
const amount = context.evaluateInput(node.id, 'amount', 10) as number;
|
||||
|
||||
if (!entity || entity.isDestroyed) {
|
||||
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
|
||||
}
|
||||
|
||||
// 获取 HealthComponent
|
||||
const health = entity.components.find(c =>
|
||||
(c.constructor as any).__componentName__ === 'Health'
|
||||
) as any;
|
||||
|
||||
if (health) {
|
||||
health.current = Math.min(health.current + amount, health.max);
|
||||
return {
|
||||
outputs: { newHealth: health.current },
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
|
||||
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
876
docs/src/content/docs/modules/blueprint/editor-guide.md
Normal file
876
docs/src/content/docs/modules/blueprint/editor-guide.md
Normal file
@@ -0,0 +1,876 @@
|
||||
---
|
||||
title: "蓝图编辑器使用指南"
|
||||
description: "Cocos Creator 蓝图可视化脚本编辑器完整使用教程"
|
||||
---
|
||||
|
||||
<script src="/js/blueprint-graph.js"></script>
|
||||
|
||||
本指南介绍如何在 Cocos Creator 中使用蓝图可视化脚本编辑器。
|
||||
|
||||
## 下载与安装
|
||||
|
||||
### 下载
|
||||
|
||||
从 GitHub Release 下载最新版本(免费):
|
||||
|
||||
**[下载 Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
|
||||
|
||||
> 技术交流 QQ 群:**481923584** | 官网:[esengine.cn](https://esengine.cn/)
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 解压 `cocos-node-editor.zip` 到项目的 `extensions` 目录:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── assets/
|
||||
├── extensions/
|
||||
│ └── cocos-node-editor/ ← 解压到这里
|
||||
└── ...
|
||||
```
|
||||
|
||||
2. 重启 Cocos Creator
|
||||
|
||||
3. 通过菜单 **扩展 → 扩展管理器** 确认插件已启用
|
||||
|
||||
4. 通过菜单 **面板 → Node Editor** 打开编辑器
|
||||
|
||||
## 界面介绍
|
||||
|
||||
- **工具栏** - 位于顶部,包含新建、打开、保存、撤销、重做等操作
|
||||
- **变量面板** - 位于左上角,用于定义和管理蓝图变量
|
||||
- **画布区域** - 主区域,用于放置和连接节点
|
||||
- **节点菜单** - 右键点击画布空白处打开,按分类列出所有可用节点
|
||||
|
||||
## 画布操作
|
||||
|
||||
| 操作 | 方式 |
|
||||
|------|------|
|
||||
| 平移画布 | 鼠标中键拖拽 / Alt + 左键拖拽 |
|
||||
| 缩放画布 | 鼠标滚轮 |
|
||||
| 打开节点菜单 | 右键点击空白处 |
|
||||
| 框选多个节点 | 在空白处拖拽 |
|
||||
| 追加框选 | Ctrl + 拖拽 |
|
||||
| 删除选中 | Delete 键 |
|
||||
|
||||
## 节点操作
|
||||
|
||||
### 添加节点
|
||||
|
||||
1. **从节点面板拖拽** - 将节点从左侧面板拖到画布
|
||||
2. **右键菜单** - 右键点击画布空白处,选择节点
|
||||
|
||||
### 连接节点
|
||||
|
||||
1. 从输出引脚拖拽到输入引脚
|
||||
2. 兼容类型的引脚会高亮显示
|
||||
3. 松开鼠标完成连接
|
||||
|
||||
**引脚类型说明:**
|
||||
|
||||
| 引脚颜色 | 类型 | 说明 |
|
||||
|---------|------|------|
|
||||
| 白色 ▶ | Exec | 执行流程(控制执行顺序) |
|
||||
| 青色 ◆ | Entity | 实体引用 |
|
||||
| 紫色 ◆ | Component | 组件引用 |
|
||||
| 浅蓝 ◆ | String | 字符串 |
|
||||
| 绿色 ◆ | Number | 数值 |
|
||||
| 红色 ◆ | Boolean | 布尔值 |
|
||||
| 灰色 ◆ | Any | 任意类型 |
|
||||
|
||||
### 删除连接
|
||||
|
||||
点击连接线选中,按 Delete 键删除。
|
||||
|
||||
## 节点类型详解
|
||||
|
||||
### 事件节点 (Event)
|
||||
|
||||
事件节点是蓝图的入口点,当特定事件发生时触发执行。
|
||||
|
||||
| 节点 | 触发时机 | 输出 |
|
||||
|------|---------|------|
|
||||
| **Event BeginPlay** | 蓝图开始运行时 | Exec, Self (实体) |
|
||||
| **Event Tick** | 每帧执行 | Exec, Delta Time |
|
||||
| **Event EndPlay** | 蓝图停止时 | Exec |
|
||||
|
||||
**示例:游戏开始时打印消息**
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg1-exec","to":"eg1-print","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="eg1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Self</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg1-print"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Message</span>
|
||||
<span class="bp-pin-value">"游戏开始!"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 实体节点 (Entity)
|
||||
|
||||
操作 ECS 实体的节点。
|
||||
|
||||
| 节点 | 功能 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| **Get Self** | 获取当前实体 | - | Entity |
|
||||
| **Create Entity** | 创建新实体 | Exec, Name | Exec, Entity |
|
||||
| **Destroy Entity** | 销毁实体 | Exec, Entity | Exec |
|
||||
| **Find Entity By Name** | 按名称查找 | Name | Entity |
|
||||
| **Find Entities By Tag** | 按标签查找 | Tag | Entity[] |
|
||||
| **Is Valid** | 检查实体有效性 | Entity | Boolean |
|
||||
| **Get/Set Entity Name** | 获取/设置名称 | Entity | String |
|
||||
| **Set Active** | 设置激活状态 | Exec, Entity, Active | Exec |
|
||||
|
||||
**示例:创建新实体**
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg2-exec","to":"eg2-create","type":"exec"},{"from":"eg2-create-out","to":"eg2-add","type":"exec"},{"from":"eg2-entity","to":"eg2-add-entity","type":"entity"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="eg2-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Create Entity</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg2-create"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Name</span>
|
||||
<span class="bp-pin-value">"Bullet"</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg2-create-out"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg2-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 520px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Add Transform</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg2-add"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg2-add-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 组件节点 (Component)
|
||||
|
||||
访问和操作 ECS 组件。
|
||||
|
||||
| 节点 | 功能 |
|
||||
|------|------|
|
||||
| **Has Component** | 检查实体是否有指定组件 |
|
||||
| **Get Component** | 获取组件实例 |
|
||||
| **Add Component** | 添加组件到实体 |
|
||||
| **Remove Component** | 移除组件 |
|
||||
| **Get/Set Property** | 获取/设置组件属性 |
|
||||
|
||||
**示例:修改 Transform 组件**
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg3-self","to":"eg3-getcomp","type":"entity"},{"from":"eg3-comp","to":"eg3-setprop","type":"component"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 100px;">
|
||||
<div class="bp-node-header pure">Get Self</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg3-self"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header pure">Get Component</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg3-getcomp"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg3-comp"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
|
||||
<span class="bp-pin-label">Transform</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 430px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header function">Set Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg3-setprop"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 流程控制节点 (Flow)
|
||||
|
||||
控制执行流程的节点。
|
||||
|
||||
#### Branch (分支)
|
||||
|
||||
条件判断,类似 if/else。
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg4-true","to":"eg4-do1","type":"exec"},{"from":"eg4-false","to":"eg4-do2","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 50px; width: 110px;">
|
||||
<div class="bp-node-header flow">Branch</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#8c0000" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Condition</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg4-true"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">True</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg4-false"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">False</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header function">DoSomething</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg4-do1"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 110px; width: 130px;">
|
||||
<div class="bp-node-header function">DoOtherThing</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg4-do2"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#### Sequence (序列)
|
||||
|
||||
按顺序执行多个分支。
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg5-then0","to":"eg5-step1","type":"exec"},{"from":"eg5-then1","to":"eg5-step2","type":"exec"},{"from":"eg5-then2","to":"eg5-step3","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 110px;">
|
||||
<div class="bp-node-header flow">Sequence</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg5-then0"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Then 0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg5-then1"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Then 1</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg5-then2"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Then 2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 20px; width: 100px;">
|
||||
<div class="bp-node-header function">Step 1</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg5-step1"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 100px; width: 100px;">
|
||||
<div class="bp-node-header function">Step 2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg5-step2"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 180px; width: 100px;">
|
||||
<div class="bp-node-header function">Step 3</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg5-step3"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#### For Loop (循环)
|
||||
|
||||
循环执行指定次数。
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg6-body","to":"eg6-iter","type":"exec"},{"from":"eg6-done","to":"eg6-finish","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header flow">For Loop</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">First Index</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Last Index</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg6-body"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Loop Body</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Index</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg6-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 250px; top: 80px; width: 130px;">
|
||||
<div class="bp-node-header function">每次迭代执行</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg6-iter"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 250px; top: 160px; width: 140px;">
|
||||
<div class="bp-node-header function">循环结束后执行</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg6-finish"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#### For Each (遍历)
|
||||
|
||||
遍历数组元素。
|
||||
|
||||
#### While Loop (条件循环)
|
||||
|
||||
当条件为真时持续循环。
|
||||
|
||||
#### Do Once (单次执行)
|
||||
|
||||
只执行一次,之后跳过。
|
||||
|
||||
#### Flip Flop (交替执行)
|
||||
|
||||
每次执行时交替触发 A 和 B 输出。
|
||||
|
||||
#### Gate (门)
|
||||
|
||||
可通过 Open/Close/Toggle 控制是否允许执行通过。
|
||||
|
||||
### 时间节点 (Time)
|
||||
|
||||
| 节点 | 功能 | 输出类型 |
|
||||
|------|------|---------|
|
||||
| **Delay** | 延迟指定时间后继续执行 | Exec |
|
||||
| **Get Delta Time** | 获取帧间隔时间 | Number |
|
||||
| **Get Time** | 获取运行总时间 | Number |
|
||||
|
||||
**示例:延迟 2 秒后执行**
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"eg7-exec","to":"eg7-delay","type":"exec"},{"from":"eg7-done","to":"eg7-print","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="eg7-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header time">Delay</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg7-delay"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Duration</span>
|
||||
<span class="bp-pin-value">2.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="eg7-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 490px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="eg7-print"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"2秒后执行"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 数学节点 (Math)
|
||||
|
||||
| 节点 | 功能 |
|
||||
|------|------|
|
||||
| **Add / Subtract / Multiply / Divide** | 四则运算 |
|
||||
| **Abs** | 绝对值 |
|
||||
| **Clamp** | 限制在范围内 |
|
||||
| **Lerp** | 线性插值 |
|
||||
| **Min / Max** | 最小/最大值 |
|
||||
| **Random Range** | 随机数 |
|
||||
| **Sin / Cos / Tan** | 三角函数 |
|
||||
|
||||
### 调试节点 (Debug)
|
||||
|
||||
| 节点 | 功能 |
|
||||
|------|------|
|
||||
| **Print** | 输出到控制台 |
|
||||
|
||||
## 变量系统
|
||||
|
||||
变量用于在蓝图中存储和共享数据。
|
||||
|
||||
### 创建变量
|
||||
|
||||
1. 在变量面板点击 **+** 按钮
|
||||
2. 输入变量名称
|
||||
3. 选择变量类型
|
||||
4. 设置默认值(可选)
|
||||
|
||||
### 使用变量
|
||||
|
||||
- **拖拽到画布** - 创建 Get 或 Set 节点
|
||||
- **Get 节点** - 读取变量值
|
||||
- **Set 节点** - 写入变量值
|
||||
|
||||
### 变量类型
|
||||
|
||||
| 类型 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| Boolean | 布尔值 | false |
|
||||
| Number | 数值 | 0 |
|
||||
| String | 字符串 | "" |
|
||||
| Entity | 实体引用 | null |
|
||||
| Vector2 | 二维向量 | (0, 0) |
|
||||
| Vector3 | 三维向量 | (0, 0, 0) |
|
||||
|
||||
### 变量节点错误状态
|
||||
|
||||
如果删除了一个变量,但画布上还有引用该变量的节点:
|
||||
- 节点会显示 **红色边框** 和 **警告图标**
|
||||
- 需要重新创建变量或删除这些节点
|
||||
|
||||
## 节点分组
|
||||
|
||||
可以将多个节点组织到一个可视化组框中,便于整理复杂蓝图。
|
||||
|
||||
### 创建组
|
||||
|
||||
1. 框选或 Ctrl+点击 选中多个节点(至少 2 个)
|
||||
2. 右键点击选中的节点
|
||||
3. 选择 **创建分组**
|
||||
4. 组框会自动包裹所有选中的节点
|
||||
|
||||
### 组操作
|
||||
|
||||
| 操作 | 方式 |
|
||||
|------|------|
|
||||
| 移动组 | 拖拽组框头部,所有节点一起移动 |
|
||||
| 取消分组 | 右键点击组框 → **取消分组** |
|
||||
|
||||
### 特性
|
||||
|
||||
- **动态大小**:组框会自动调整大小以包裹所有节点
|
||||
- **独立移动**:可以单独移动组内的节点,组框会自动调整
|
||||
- **仅编辑器**:组是纯视觉组织,不影响运行时逻辑
|
||||
|
||||
## 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `Ctrl + S` | 保存蓝图 |
|
||||
| `Ctrl + Z` | 撤销 |
|
||||
| `Ctrl + Shift + Z` | 重做 |
|
||||
| `Ctrl + C` | 复制选中节点 |
|
||||
| `Ctrl + X` | 剪切选中节点 |
|
||||
| `Ctrl + V` | 粘贴节点 |
|
||||
| `Delete` | 删除选中项 |
|
||||
| `Ctrl + A` | 全选 |
|
||||
|
||||
## 保存与加载
|
||||
|
||||
### 保存蓝图
|
||||
|
||||
1. 点击工具栏 **保存** 按钮
|
||||
2. 选择保存位置(**必须保存在 `assets/resources` 目录下**,否则 Cocos Creator 无法动态加载)
|
||||
3. 文件扩展名为 `.blueprint.json`
|
||||
|
||||
> **重要提示**:蓝图文件必须放在 `resources` 目录下,游戏运行时才能通过 `cc.resources.load()` 加载。
|
||||
|
||||
### 加载蓝图
|
||||
|
||||
1. 点击工具栏 **打开** 按钮
|
||||
2. 选择 `.blueprint.json` 文件
|
||||
|
||||
### 蓝图文件格式
|
||||
|
||||
蓝图保存为 JSON 格式,可与 `@esengine/blueprint` 运行时兼容:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "blueprint",
|
||||
"metadata": {
|
||||
"name": "PlayerController",
|
||||
"description": "玩家控制逻辑"
|
||||
},
|
||||
"variables": [],
|
||||
"nodes": [],
|
||||
"connections": []
|
||||
}
|
||||
```
|
||||
|
||||
## 实战示例
|
||||
|
||||
### 示例 1:移动控制
|
||||
|
||||
实现每帧移动实体:
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"ex1-exec","to":"ex1-setprop","type":"exec"},{"from":"ex1-delta","to":"ex1-mul-a","type":"float"},{"from":"ex1-mul-result","to":"ex1-x","type":"float"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event Tick</span>
|
||||
<span class="bp-header-exec" data-pin="ex1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex1-delta"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Delta Time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 110px; width: 120px;">
|
||||
<div class="bp-node-header math">Multiply</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex1-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">B (Speed)</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex1-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 380px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Set Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex1-setprop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7030c0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex1-x"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 示例 2:生命值系统
|
||||
|
||||
受伤后检查死亡逻辑。`Event OnDamage` 是一个自定义事件节点,可以通过代码 `vm.triggerCustomEvent('OnDamage', { damage: 50 })` 触发:
|
||||
|
||||
<div class="bp-graph" data-graph='{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "event", "title": "Event OnDamage", "category": "event",
|
||||
"outputs": [
|
||||
{"id": "event-exec", "type": "exec", "inHeader": true},
|
||||
{"id": "event-self", "type": "entity", "label": "Self"},
|
||||
{"id": "event-damage", "type": "float", "label": "Damage"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "getcomp", "title": "Get Component", "category": "function",
|
||||
"inputs": [
|
||||
{"id": "getcomp-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "getcomp-entity", "type": "entity", "label": "Entity"},
|
||||
{"id": "getcomp-type", "type": "string", "label": "Type", "value": "Health", "connected": false}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "getcomp-out", "type": "exec"},
|
||||
{"id": "getcomp-comp", "type": "component", "label": "Component"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "getprop", "title": "Get Property", "category": "pure",
|
||||
"inputs": [
|
||||
{"id": "getprop-target", "type": "component", "label": "Target"},
|
||||
{"id": "getprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "getprop-val", "type": "float", "label": "Value"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sub", "title": "Subtract", "category": "math",
|
||||
"inputs": [
|
||||
{"id": "sub-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "sub-a", "type": "float", "label": "A"},
|
||||
{"id": "sub-b", "type": "float", "label": "B"}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "sub-out", "type": "exec"},
|
||||
{"id": "sub-result", "type": "float", "label": "Result"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "setprop", "title": "Set Property", "category": "function",
|
||||
"inputs": [
|
||||
{"id": "setprop-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "setprop-target", "type": "component", "label": "Target"},
|
||||
{"id": "setprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false},
|
||||
{"id": "setprop-val", "type": "float", "label": "Value"}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "setprop-out", "type": "exec"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lte", "title": "Less Or Equal", "category": "pure",
|
||||
"inputs": [
|
||||
{"id": "lte-a", "type": "float", "label": "A"},
|
||||
{"id": "lte-b", "type": "float", "label": "B", "value": "0", "connected": false}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "lte-result", "type": "bool", "label": "Result"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "branch", "title": "Branch", "category": "flow",
|
||||
"inputs": [
|
||||
{"id": "branch-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "branch-cond", "type": "bool", "label": "Condition"}
|
||||
],
|
||||
"outputs": [
|
||||
{"id": "branch-true", "type": "exec", "label": "True"},
|
||||
{"id": "branch-false", "type": "exec", "label": "False"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "destroy", "title": "Destroy Entity", "category": "function",
|
||||
"inputs": [
|
||||
{"id": "destroy-exec", "type": "exec", "label": "Exec"},
|
||||
{"id": "destroy-entity", "type": "entity", "label": "Entity"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{"from": "event-exec", "to": "getcomp-exec", "type": "exec"},
|
||||
{"from": "getcomp-out", "to": "sub-exec", "type": "exec"},
|
||||
{"from": "sub-out", "to": "setprop-exec", "type": "exec"},
|
||||
{"from": "setprop-out", "to": "branch-exec", "type": "exec"},
|
||||
{"from": "branch-true", "to": "destroy-exec", "type": "exec"},
|
||||
{"from": "event-self", "to": "getcomp-entity", "type": "entity"},
|
||||
{"from": "event-self", "to": "destroy-entity", "type": "entity"},
|
||||
{"from": "getcomp-comp", "to": "getprop-target", "type": "component"},
|
||||
{"from": "getcomp-comp", "to": "setprop-target", "type": "component"},
|
||||
{"from": "getprop-val", "to": "sub-a", "type": "float"},
|
||||
{"from": "event-damage", "to": "sub-b", "type": "float"},
|
||||
{"from": "sub-result", "to": "setprop-val", "type": "float"},
|
||||
{"from": "sub-result", "to": "lte-a", "type": "float"},
|
||||
{"from": "lte-result", "to": "branch-cond", "type": "bool"}
|
||||
]
|
||||
}'></div>
|
||||
|
||||
### 示例 3:延迟生成
|
||||
|
||||
每 2 秒生成一个敌人:
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"ex3-begin-exec","to":"ex3-loop","type":"exec"},{"from":"ex3-loop-body","to":"ex3-delay","type":"exec"},{"from":"ex3-delay-done","to":"ex3-create","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="ex3-begin-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header flow">Do N Times</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex3-loop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">N</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex3-loop-body"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Loop Body</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Index</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 430px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header time">Delay</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex3-delay"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Duration</span>
|
||||
<span class="bp-pin-value">2.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="ex3-delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 610px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header function">Create Entity</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="ex3-create"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Name</span>
|
||||
<span class="bp-pin-value">"Enemy"</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 节点无法连接?
|
||||
|
||||
检查引脚类型是否匹配。执行引脚(白色)只能连接执行引脚,数据引脚需要类型兼容。
|
||||
|
||||
### Q: 蓝图不执行?
|
||||
|
||||
1. 确保实体添加了 `BlueprintComponent`
|
||||
2. 确保场景添加了 `BlueprintSystem`
|
||||
3. 检查 `autoStart` 是否为 `true`
|
||||
|
||||
### Q: 如何调试?
|
||||
|
||||
使用 **Print** 节点输出变量值到控制台。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [ECS 节点参考](/modules/blueprint/nodes) - 完整节点列表
|
||||
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
|
||||
- [运行时集成](/modules/blueprint/vm) - 蓝图虚拟机 API
|
||||
- [实际示例](/modules/blueprint/examples) - 更多游戏逻辑示例
|
||||
@@ -5,7 +5,17 @@ description: "与 ECS 框架深度集成的可视化脚本系统"
|
||||
|
||||
`@esengine/blueprint` 提供与 ECS 框架深度集成的可视化脚本系统,支持通过节点式编程控制实体行为。
|
||||
|
||||
## 安装
|
||||
## 编辑器下载
|
||||
|
||||
Cocos Creator 蓝图编辑器插件(免费):
|
||||
|
||||
**[下载 Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
|
||||
|
||||
> 技术交流 QQ 群:**481923584** | 官网:[esengine.cn](https://esengine.cn/)
|
||||
|
||||
详细使用教程请参考 [编辑器使用指南](./editor-guide)。
|
||||
|
||||
## 安装运行时
|
||||
|
||||
```bash
|
||||
npm install @esengine/blueprint
|
||||
@@ -144,6 +154,7 @@ interface BlueprintAsset {
|
||||
|
||||
## 文档导航
|
||||
|
||||
- [编辑器使用指南](./editor-guide) - Cocos Creator 蓝图编辑器教程
|
||||
- [虚拟机 API](./vm) - BlueprintVM 与 ECS 集成
|
||||
- [ECS 节点参考](./nodes) - 内置 ECS 操作节点
|
||||
- [自定义节点](./custom-nodes) - 创建自定义 ECS 节点
|
||||
|
||||
@@ -1,17 +1,117 @@
|
||||
---
|
||||
title: "ECS 节点参考"
|
||||
description: "蓝图内置 ECS 操作节点"
|
||||
description: "蓝图内置 ECS 操作节点完整参考"
|
||||
---
|
||||
|
||||
本文档提供蓝图系统所有内置节点的完整参考,包含可视化示例。
|
||||
|
||||
## 引脚类型说明
|
||||
|
||||
<div class="bp-legend">
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff" stroke="#fff" stroke-width="1"/></svg> 执行流 (Exec)</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#00a0e0" stroke-width="2"/></svg> 实体 (Entity)</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#7030c0" stroke-width="2"/></svg> 组件 (Component)</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#7ecd32" stroke-width="2"/></svg> 数值 (Float)</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#e060e0" stroke-width="2"/></svg> 字符串 (String)</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#8c0000" stroke-width="2"/></svg> 布尔 (Boolean)</div>
|
||||
</div>
|
||||
|
||||
## 事件节点
|
||||
|
||||
生命周期事件,作为蓝图执行的入口点:
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `EventBeginPlay` | 蓝图启动时触发 |
|
||||
| `EventTick` | 每帧触发,接收 deltaTime |
|
||||
| `EventEndPlay` | 蓝图停止时触发 |
|
||||
| 节点 | 说明 | 输出 |
|
||||
|------|------|------|
|
||||
| `EventBeginPlay` | 蓝图启动时触发 | Exec, Self (Entity) |
|
||||
| `EventTick` | 每帧触发 | Exec, Delta Time |
|
||||
| `EventEndPlay` | 蓝图停止时触发 | Exec |
|
||||
|
||||
### 示例:游戏初始化
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"beginplay-exec","to":"print-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="beginplay-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Self</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="print-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Message</span>
|
||||
<span class="bp-pin-value">"游戏开始!"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 示例:每帧移动
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"tick-exec","to":"setprop-exec","type":"exec"},{"from":"tick-delta","to":"mul-a","type":"float"},{"from":"mul-result","to":"setprop-x","type":"float"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event Tick</span>
|
||||
<span class="bp-header-exec" data-pin="tick-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="tick-delta"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Delta Time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 110px; width: 120px;">
|
||||
<div class="bp-node-header math">Multiply</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 380px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Set Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="setprop-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7030c0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="setprop-x"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 实体节点 (Entity)
|
||||
|
||||
@@ -26,93 +126,422 @@ description: "蓝图内置 ECS 操作节点"
|
||||
| `Is Valid` | 检查实体是否有效 | 纯节点 |
|
||||
| `Get Entity Name` | 获取实体名称 | 纯节点 |
|
||||
| `Set Entity Name` | 设置实体名称 | 执行节点 |
|
||||
| `Get Entity Tag` | 获取实体标签 | 纯节点 |
|
||||
| `Set Entity Tag` | 设置实体标签 | 执行节点 |
|
||||
| `Set Active` | 设置实体激活状态 | 执行节点 |
|
||||
| `Is Active` | 检查实体是否激活 | 纯节点 |
|
||||
| `Find Entity By Name` | 按名称查找实体 | 纯节点 |
|
||||
| `Find Entities By Tag` | 按标签查找所有实体 | 纯节点 |
|
||||
| `Get Entity ID` | 获取实体唯一 ID | 纯节点 |
|
||||
| `Find Entity By ID` | 按 ID 查找实体 | 纯节点 |
|
||||
|
||||
### 示例:创建子弹
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"bp-exec","to":"create-exec","type":"exec"},{"from":"create-exec-out","to":"add-exec","type":"exec"},{"from":"create-entity","to":"add-entity","type":"entity"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Self</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Create Entity</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="create-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="create-exec-out"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="create-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 520px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header function">Add Transform</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="add-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="add-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 组件节点 (Component)
|
||||
|
||||
操作 ECS 组件:
|
||||
读写组件属性:
|
||||
|
||||
| 节点 | 说明 | 类型 |
|
||||
|------|------|------|
|
||||
| `Has Component` | 检查实体是否有指定组件 | 纯节点 |
|
||||
| `Get Component` | 获取实体的组件 | 纯节点 |
|
||||
| `Get All Components` | 获取实体所有组件 | 纯节点 |
|
||||
| `Remove Component` | 移除组件 | 执行节点 |
|
||||
| `Get Component Property` | 获取组件属性值 | 纯节点 |
|
||||
| `Set Component Property` | 设置组件属性值 | 执行节点 |
|
||||
| `Get Component Type` | 获取组件类型名称 | 纯节点 |
|
||||
| `Get Owner Entity` | 从组件获取所属实体 | 纯节点 |
|
||||
| `Get Component` | 从实体获取指定类型组件 | 纯节点 |
|
||||
| `Has Component` | 检查实体是否拥有指定组件 | 纯节点 |
|
||||
| `Add Component` | 为实体添加组件 | 执行节点 |
|
||||
| `Remove Component` | 从实体移除组件 | 执行节点 |
|
||||
| `Get Property` | 获取组件属性值 | 纯节点 |
|
||||
| `Set Property` | 设置组件属性值 | 执行节点 |
|
||||
|
||||
## 流程控制节点 (Flow)
|
||||
### 示例:修改位置
|
||||
|
||||
控制执行流程:
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"self-entity","to":"getcomp-entity","type":"entity"},{"from":"getcomp-transform","to":"getprop-target","type":"component"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 100px;">
|
||||
<div class="bp-node-header pure">Get Self</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="self-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 200px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header pure">Get Component</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="getcomp-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
|
||||
<span class="bp-pin-label">Entity</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="getcomp-transform"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
|
||||
<span class="bp-pin-label">Transform</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 430px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header pure">Get Property</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="getprop-target"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
|
||||
<span class="bp-pin-label">Target</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 流程控制节点
|
||||
|
||||
控制蓝图执行流程:
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Branch` | 条件分支 (if/else) |
|
||||
| `Sequence` | 顺序执行多个输出 |
|
||||
| `For Loop` | 循环执行 |
|
||||
| `For Each` | 遍历数组 |
|
||||
| `Branch` | 条件分支(if/else) |
|
||||
| `Sequence` | 按顺序执行多个分支 |
|
||||
| `For Loop` | 指定次数循环 |
|
||||
| `For Each` | 遍历数组元素 |
|
||||
| `While Loop` | 条件循环 |
|
||||
| `Do Once` | 只执行一次 |
|
||||
| `Flip Flop` | 交替执行两个分支 |
|
||||
| `Gate` | 可开关的执行门 |
|
||||
| `Do Once` | 仅执行一次 |
|
||||
| `Flip Flop` | 交替执行 A/B |
|
||||
| `Gate` | 门控开关 |
|
||||
|
||||
## 时间节点 (Time)
|
||||
### 示例:条件分支
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"cond-exec","to":"branch-exec","type":"exec"},{"from":"cond-result","to":"branch-cond","type":"bool"},{"from":"branch-true","to":"print1-exec","type":"exec"},{"from":"branch-false","to":"print2-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 60px; width: 120px;">
|
||||
<div class="bp-node-header pure">Condition</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="cond-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="cond-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#8c0000"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 220px; top: 60px; width: 110px;">
|
||||
<div class="bp-node-header flow">Branch</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="branch-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="branch-cond"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#8c0000"/></svg></span>
|
||||
<span class="bp-pin-label">Cond</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="branch-true"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">True</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="branch-false"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">False</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 420px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="print1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"是"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 420px; top: 130px; width: 120px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="print2-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"否"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 示例:For 循环
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"forloop-bp-exec","to":"forloop-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="forloop-bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
|
||||
<div class="bp-node-header flow">For Loop</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="forloop-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">First</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Last</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Body</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
|
||||
<span class="bp-pin-label">Index</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 时间节点
|
||||
|
||||
| 节点 | 说明 | 输出 |
|
||||
|------|------|------|
|
||||
| `Delay` | 延迟执行指定秒数 | Exec |
|
||||
| `Get Delta Time` | 获取帧间隔时间 | Float |
|
||||
| `Get Time` | 获取运行总时间 | Float |
|
||||
|
||||
### 示例:延迟执行
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"delay-bp-exec","to":"delay-exec","type":"exec"},{"from":"delay-done","to":"delay-print-exec","type":"exec"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
|
||||
<div class="bp-node-header event">
|
||||
<span class="bp-node-header-icon"></span>
|
||||
<span class="bp-node-header-title">Event BeginPlay</span>
|
||||
<span class="bp-header-exec" data-pin="delay-bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 280px; top: 20px; width: 120px;">
|
||||
<div class="bp-node-header time">Delay</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="delay-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Duration</span>
|
||||
<span class="bp-pin-value">2.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 490px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header debug">Print</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="delay-print-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
|
||||
<span class="bp-pin-label">Exec</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Msg</span>
|
||||
<span class="bp-pin-value">"2秒后"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 数学节点
|
||||
|
||||
### 基础运算
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Add` | 加法 | A, B | A + B |
|
||||
| `Subtract` | 减法 | A, B | A - B |
|
||||
| `Multiply` | 乘法 | A, B | A × B |
|
||||
| `Divide` | 除法 | A, B | A / B |
|
||||
| `Modulo` | 取模 | A, B | A % B |
|
||||
|
||||
### 数学函数
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Abs` | 绝对值 | Value | \|Value\| |
|
||||
| `Sqrt` | 平方根 | Value | √Value |
|
||||
| `Pow` | 幂运算 | Base, Exp | Base^Exp |
|
||||
| `Floor` | 向下取整 | Value | ⌊Value⌋ |
|
||||
| `Ceil` | 向上取整 | Value | ⌈Value⌉ |
|
||||
| `Round` | 四舍五入 | Value | round(Value) |
|
||||
| `Clamp` | 区间钳制 | Value, Min, Max | min(max(V, Min), Max) |
|
||||
| `Lerp` | 线性插值 | A, B, Alpha | A + (B-A) × Alpha |
|
||||
| `Min` | 取最小值 | A, B | min(A, B) |
|
||||
| `Max` | 取最大值 | A, B | max(A, B) |
|
||||
|
||||
### 三角函数
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Sin` | 正弦 |
|
||||
| `Cos` | 余弦 |
|
||||
| `Tan` | 正切 |
|
||||
| `Asin` | 反正弦 |
|
||||
| `Acos` | 反余弦 |
|
||||
| `Atan` | 反正切 |
|
||||
| `Atan2` | 二参数反正切 |
|
||||
|
||||
### 随机数
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Random` | 随机浮点数 [0, 1) | - | Float |
|
||||
| `Random Range` | 范围内随机数 | Min, Max | Float |
|
||||
| `Random Int` | 随机整数 | Min, Max | Int |
|
||||
|
||||
### 比较节点
|
||||
|
||||
| 节点 | 说明 | 输出 |
|
||||
|------|------|------|
|
||||
| `Equal` | A == B | Boolean |
|
||||
| `Not Equal` | A != B | Boolean |
|
||||
| `Greater` | A > B | Boolean |
|
||||
| `Greater Or Equal` | A >= B | Boolean |
|
||||
| `Less` | A < B | Boolean |
|
||||
| `Less Or Equal` | A <= B | Boolean |
|
||||
|
||||
### 扩展数学节点
|
||||
|
||||
> **Vector2、Fixed32、FixedVector2、Color** 等高级数学节点由 `@esengine/ecs-framework-math` 模块提供。
|
||||
>
|
||||
> 详见:[数学库蓝图节点](/modules/math/blueprint-nodes)
|
||||
|
||||
### 示例:钳制数值
|
||||
|
||||
<div class="bp-graph" style="" data-connections='[{"from":"rand-result","to":"clamp-value","type":"float"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="left: 20px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header math">Random Range</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Min</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Max</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="rand-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header math">Clamp</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="clamp-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Min</span>
|
||||
<span class="bp-pin-value">20</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Max</span>
|
||||
<span class="bp-pin-value">80</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 变量节点
|
||||
|
||||
蓝图定义的变量会自动生成 Get 和 Set 节点:
|
||||
|
||||
| 节点 | 说明 | 类型 |
|
||||
|------|------|------|
|
||||
| `Delay` | 延迟执行 | 执行节点 |
|
||||
| `Get Delta Time` | 获取帧间隔时间 | 纯节点 |
|
||||
| `Get Time` | 获取运行总时间 | 纯节点 |
|
||||
| `Get <变量名>` | 读取变量值 | 纯节点 |
|
||||
| `Set <变量名>` | 设置变量值 | 执行节点 |
|
||||
|
||||
## 数学节点 (Math)
|
||||
## 调试节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Add` / `Subtract` / `Multiply` / `Divide` | 四则运算 |
|
||||
| `Abs` | 绝对值 |
|
||||
| `Clamp` | 限制范围 |
|
||||
| `Lerp` | 线性插值 |
|
||||
| `Min` / `Max` | 最小/最大值 |
|
||||
| `Print` | 输出消息到控制台 |
|
||||
|
||||
## 调试节点 (Debug)
|
||||
## 相关文档
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Print` | 输出到控制台 |
|
||||
|
||||
## 自动生成的组件节点
|
||||
|
||||
使用 `@BlueprintExpose` 装饰器标记的组件会自动生成节点:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
@BlueprintExpose({ displayName: '变换', category: 'core' })
|
||||
export class TransformComponent extends Component {
|
||||
@BlueprintProperty({ displayName: 'X 坐标' })
|
||||
x: number = 0;
|
||||
|
||||
@BlueprintProperty({ displayName: 'Y 坐标' })
|
||||
y: number = 0;
|
||||
|
||||
@BlueprintMethod({ displayName: '移动' })
|
||||
translate(dx: number, dy: number): void {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
生成的节点:
|
||||
- **Get Transform** - 获取 Transform 组件
|
||||
- **Get X 坐标** / **Set X 坐标** - 访问 x 属性
|
||||
- **Get Y 坐标** / **Set Y 坐标** - 访问 y 属性
|
||||
- **移动** - 调用 translate 方法
|
||||
- [数学库蓝图节点](/modules/math/blueprint-nodes) - Vector2、Fixed32、Color 等数学节点
|
||||
- [蓝图编辑器指南](/modules/blueprint/editor-guide) - 学习如何使用编辑器
|
||||
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
|
||||
- [蓝图虚拟机](/modules/blueprint/vm) - 运行时 API
|
||||
|
||||
489
docs/src/content/docs/modules/math/blueprint-nodes.md
Normal file
489
docs/src/content/docs/modules/math/blueprint-nodes.md
Normal file
@@ -0,0 +1,489 @@
|
||||
---
|
||||
title: "数学库蓝图节点"
|
||||
description: "Math 模块提供的蓝图节点 - Vector2、Fixed32、FixedVector2、Color"
|
||||
---
|
||||
|
||||
本文档介绍 `@esengine/ecs-framework-math` 模块提供的蓝图节点。
|
||||
|
||||
> **注意**:这些节点需要安装 math 模块才能使用。
|
||||
|
||||
<script src="/js/blueprint-graph.js"></script>
|
||||
|
||||
## 引脚类型说明
|
||||
|
||||
<div class="bp-legend">
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#7ecd32" stroke-width="2"/></svg> 浮点数 (Float)</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#2196F3" stroke-width="2"/></svg> Vector2</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#9C27B0" stroke-width="2"/></svg> Fixed32</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#673AB7" stroke-width="2"/></svg> FixedVector2</div>
|
||||
<div class="bp-legend-item"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="transparent" stroke="#FF9800" stroke-width="2"/></svg> Color</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Vector2 节点
|
||||
|
||||
2D 向量操作,用于位置、速度、方向计算。
|
||||
|
||||
### 节点列表
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Make Vector2` | 从 X, Y 创建 Vector2 | X, Y | Vector2 |
|
||||
| `Break Vector2` | 分解 Vector2 为 X, Y | Vector | X, Y |
|
||||
| `Vector2 +` | 向量加法 | A, B | Vector2 |
|
||||
| `Vector2 -` | 向量减法 | A, B | Vector2 |
|
||||
| `Vector2 *` | 向量缩放 | Vector, Scalar | Vector2 |
|
||||
| `Vector2 Length` | 获取向量长度 | Vector | Float |
|
||||
| `Vector2 Normalize` | 归一化为单位向量 | Vector | Vector2 |
|
||||
| `Vector2 Dot` | 点积 | A, B | Float |
|
||||
| `Vector2 Cross` | 2D 叉积 | A, B | Float |
|
||||
| `Vector2 Distance` | 两点距离 | A, B | Float |
|
||||
| `Vector2 Lerp` | 线性插值 | A, B, T | Vector2 |
|
||||
| `Vector2 Rotate` | 旋转(弧度) | Vector, Angle | Vector2 |
|
||||
| `Vector2 From Angle` | 从角度创建单位向量 | Angle | Vector2 |
|
||||
|
||||
### 示例:计算移动方向
|
||||
|
||||
从起点到终点的方向向量:
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"v2-start","to":"v2-sub-a","type":"vector2"},{"from":"v2-end","to":"v2-sub-b","type":"vector2"},{"from":"v2-sub-result","to":"v2-norm-in","type":"vector2"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Make Vector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-start"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 180px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Make Vector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">100</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">50</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-end"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 220px; top: 90px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 -</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-sub-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-sub-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-sub-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 400px; top: 55px; width: 140px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 Normalize</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-norm-in"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 示例:圆周运动
|
||||
|
||||
使用角度和半径计算圆周位置:
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"v2-angle-out","to":"v2-scale-vec","type":"vector2"},{"from":"v2-scale-result","to":"v2-add-b","type":"vector2"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 40px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 From Angle</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Angle</span>
|
||||
<span class="bp-pin-value">1.57</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-angle-out"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 230px; top: 40px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 *</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-scale-vec"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Vector</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Scalar</span>
|
||||
<span class="bp-pin-value">50</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="v2-scale-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 420px; top: 40px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #2196F3;">Vector2 +</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#2196F3" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">A (Center)</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="v2-add-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#2196F3"/></svg></span>
|
||||
<span class="bp-pin-label">Position</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Fixed32 定点数节点
|
||||
|
||||
Q16.16 定点数运算,适用于帧同步网络游戏,保证跨平台计算一致性。
|
||||
|
||||
### 节点列表
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Fixed32 From Float` | 从浮点数创建 | Float | Fixed32 |
|
||||
| `Fixed32 From Int` | 从整数创建 | Int | Fixed32 |
|
||||
| `Fixed32 To Float` | 转换为浮点数 | Fixed32 | Float |
|
||||
| `Fixed32 To Int` | 转换为整数 | Fixed32 | Int |
|
||||
| `Fixed32 +` | 加法 | A, B | Fixed32 |
|
||||
| `Fixed32 -` | 减法 | A, B | Fixed32 |
|
||||
| `Fixed32 *` | 乘法 | A, B | Fixed32 |
|
||||
| `Fixed32 /` | 除法 | A, B | Fixed32 |
|
||||
| `Fixed32 Abs` | 绝对值 | Value | Fixed32 |
|
||||
| `Fixed32 Sqrt` | 平方根 | Value | Fixed32 |
|
||||
| `Fixed32 Floor` | 向下取整 | Value | Fixed32 |
|
||||
| `Fixed32 Ceil` | 向上取整 | Value | Fixed32 |
|
||||
| `Fixed32 Round` | 四舍五入 | Value | Fixed32 |
|
||||
| `Fixed32 Sign` | 符号 (-1, 0, 1) | Value | Fixed32 |
|
||||
| `Fixed32 Min` | 最小值 | A, B | Fixed32 |
|
||||
| `Fixed32 Max` | 最大值 | A, B | Fixed32 |
|
||||
| `Fixed32 Clamp` | 钳制范围 | Value, Min, Max | Fixed32 |
|
||||
| `Fixed32 Lerp` | 线性插值 | A, B, T | Fixed32 |
|
||||
|
||||
### 示例:帧同步移动速度计算
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"f32-speed","to":"f32-mul-a","type":"fixed32"},{"from":"f32-dt","to":"f32-mul-b","type":"fixed32"},{"from":"f32-mul-result","to":"f32-tofloat","type":"fixed32"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 From Float</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
<span class="bp-pin-value">5.0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="f32-speed"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">Speed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 160px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 From Float</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Value</span>
|
||||
<span class="bp-pin-value">0.016</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="f32-dt"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">DeltaTime</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 240px; top: 75px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 *</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="f32-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="f32-mul-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="f32-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 430px; top: 75px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #9C27B0;">Fixed32 To Float</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="f32-tofloat"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9C27B0"/></svg></span>
|
||||
<span class="bp-pin-label">Fixed</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">Float</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## FixedVector2 定点向量节点
|
||||
|
||||
定点向量运算,用于确定性物理计算,适用于帧同步。
|
||||
|
||||
### 节点列表
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Make FixedVector2` | 从 X, Y 浮点数创建 | X, Y | FixedVector2 |
|
||||
| `Break FixedVector2` | 分解为 X, Y 浮点数 | Vector | X, Y |
|
||||
| `FixedVector2 +` | 向量加法 | A, B | FixedVector2 |
|
||||
| `FixedVector2 -` | 向量减法 | A, B | FixedVector2 |
|
||||
| `FixedVector2 *` | 按 Fixed32 缩放 | Vector, Scalar | FixedVector2 |
|
||||
| `FixedVector2 Negate` | 取反 | Vector | FixedVector2 |
|
||||
| `FixedVector2 Length` | 获取长度 | Vector | Fixed32 |
|
||||
| `FixedVector2 Normalize` | 归一化 | Vector | FixedVector2 |
|
||||
| `FixedVector2 Dot` | 点积 | A, B | Fixed32 |
|
||||
| `FixedVector2 Cross` | 2D 叉积 | A, B | Fixed32 |
|
||||
| `FixedVector2 Distance` | 两点距离 | A, B | Fixed32 |
|
||||
| `FixedVector2 Lerp` | 线性插值 | A, B, T | FixedVector2 |
|
||||
|
||||
### 示例:确定性位置更新
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"fv2-pos","to":"fv2-add-a","type":"fixedvector2"},{"from":"fv2-vel","to":"fv2-add-b","type":"fixedvector2"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #673AB7;">Make FixedVector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">10</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">20</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="fv2-pos"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">Position</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 180px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #673AB7;">Make FixedVector2</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">X</span>
|
||||
<span class="bp-pin-value">1</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Y</span>
|
||||
<span class="bp-pin-value">0</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="fv2-vel"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">Velocity</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 250px; top: 90px; width: 140px;">
|
||||
<div class="bp-node-header math" style="background: #673AB7;">FixedVector2 +</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="fv2-add-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="fv2-add-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#673AB7"/></svg></span>
|
||||
<span class="bp-pin-label">New Position</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Color 颜色节点
|
||||
|
||||
颜色创建与操作节点。
|
||||
|
||||
### 节点列表
|
||||
|
||||
| 节点 | 说明 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `Make Color` | 从 RGBA 创建 | R, G, B, A | Color |
|
||||
| `Break Color` | 分解为 RGBA | Color | R, G, B, A |
|
||||
| `Color From Hex` | 从十六进制字符串创建 | Hex | Color |
|
||||
| `Color To Hex` | 转换为十六进制字符串 | Color | String |
|
||||
| `Color From HSL` | 从 HSL 创建 | H, S, L | Color |
|
||||
| `Color To HSL` | 转换为 HSL | Color | H, S, L |
|
||||
| `Color Lerp` | 颜色插值 | A, B, T | Color |
|
||||
| `Color Lighten` | 提亮 | Color, Amount | Color |
|
||||
| `Color Darken` | 变暗 | Color, Amount | Color |
|
||||
| `Color Saturate` | 增加饱和度 | Color, Amount | Color |
|
||||
| `Color Desaturate` | 降低饱和度 | Color, Amount | Color |
|
||||
| `Color Invert` | 反色 | Color | Color |
|
||||
| `Color Grayscale` | 灰度化 | Color | Color |
|
||||
| `Color Luminance` | 获取亮度 | Color | Float |
|
||||
|
||||
### 颜色常量
|
||||
|
||||
| 节点 | 值 |
|
||||
|------|------|
|
||||
| `Color White` | (1, 1, 1, 1) |
|
||||
| `Color Black` | (0, 0, 0, 1) |
|
||||
| `Color Red` | (1, 0, 0, 1) |
|
||||
| `Color Green` | (0, 1, 0, 1) |
|
||||
| `Color Blue` | (0, 0, 1, 1) |
|
||||
| `Color Transparent` | (0, 0, 0, 0) |
|
||||
|
||||
### 示例:颜色过渡动画
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"color-a","to":"color-lerp-a","type":"color"},{"from":"color-b","to":"color-lerp-b","type":"color"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 10px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color Red</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="color-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 130px; width: 120px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color Blue</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="color-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 220px; top: 50px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color Lerp</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="color-lerp-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="color-lerp-b"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">T</span>
|
||||
<span class="bp-pin-value">0.5</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Result</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### 示例:从 Hex 创建颜色
|
||||
|
||||
<div class="bp-graph" data-connections='[{"from":"hex-color","to":"break-color","type":"color"}]'>
|
||||
<svg class="bp-connections"></svg>
|
||||
<div class="bp-node" style="position: absolute; left: 20px; top: 30px; width: 150px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Color From Hex</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
|
||||
<span class="bp-pin-label">Hex</span>
|
||||
<span class="bp-pin-value">"#FF5722"</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin" data-pin="hex-color"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-node" style="position: absolute; left: 250px; top: 20px; width: 130px;">
|
||||
<div class="bp-node-header math" style="background: #FF9800;">Break Color</div>
|
||||
<div class="bp-node-body">
|
||||
<div class="bp-pin-row input">
|
||||
<span class="bp-pin" data-pin="break-color"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#FF9800"/></svg></span>
|
||||
<span class="bp-pin-label">Color</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">R</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">G</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">B</span>
|
||||
</div>
|
||||
<div class="bp-pin-row output">
|
||||
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
|
||||
<span class="bp-pin-label">A</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [蓝图节点参考](/modules/blueprint/nodes) - 核心蓝图节点
|
||||
- [蓝图编辑器指南](/modules/blueprint/editor-guide) - 编辑器使用方法
|
||||
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
|
||||
79
docs/src/content/docs/modules/math/index.md
Normal file
79
docs/src/content/docs/modules/math/index.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "数学库"
|
||||
description: "ESEngine 数学库 - Vector2, Fixed32, FixedVector2, Color 等数学类型"
|
||||
---
|
||||
|
||||
`@esengine/ecs-framework-math` 模块提供游戏开发常用的数学类型和运算。
|
||||
|
||||
## 核心类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `Vector2` | 2D 浮点向量,用于位置、速度、方向 |
|
||||
| `Fixed32` | Q16.16 定点数,用于帧同步确定性计算 |
|
||||
| `FixedVector2` | 2D 定点向量,用于确定性物理 |
|
||||
| `Color` | RGBA 颜色 |
|
||||
|
||||
## 功能特性
|
||||
|
||||
### Vector2
|
||||
|
||||
- 加法、减法、缩放
|
||||
- 点积、叉积
|
||||
- 长度、归一化
|
||||
- 距离、插值
|
||||
- 旋转、角度转换
|
||||
|
||||
### Fixed32 定点数
|
||||
|
||||
专为帧同步网络游戏设计,保证跨平台计算一致性:
|
||||
|
||||
- 基本运算:加、减、乘、除
|
||||
- 数学函数:绝对值、平方根、取整
|
||||
- 比较、钳制、插值
|
||||
- 常量:0、1、0.5、π、2π
|
||||
|
||||
### Color 颜色
|
||||
|
||||
- RGB/RGBA 创建与分解
|
||||
- Hex 十六进制转换
|
||||
- HSL 色彩空间转换
|
||||
- 颜色操作:提亮、变暗、饱和度调整
|
||||
- 颜色混合与插值
|
||||
|
||||
## 蓝图支持
|
||||
|
||||
数学库提供了丰富的蓝图节点,详见:
|
||||
|
||||
- [数学库蓝图节点](/modules/math/blueprint-nodes)
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pnpm add @esengine/ecs-framework-math
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
```typescript
|
||||
import { Vector2, Fixed32, FixedVector2, Color } from '@esengine/ecs-framework-math';
|
||||
|
||||
// Vector2
|
||||
const pos = new Vector2(10, 20);
|
||||
const dir = pos.normalized();
|
||||
|
||||
// Fixed32 (帧同步)
|
||||
const speed = Fixed32.from(5.0);
|
||||
const dt = Fixed32.from(0.016);
|
||||
const distance = speed.mul(dt);
|
||||
|
||||
// FixedVector2
|
||||
const fixedPos = FixedVector2.from(10, 20);
|
||||
const fixedVel = FixedVector2.from(1, 0);
|
||||
const newPos = fixedPos.add(fixedVel);
|
||||
|
||||
// Color
|
||||
const red = Color.RED;
|
||||
const blue = Color.BLUE;
|
||||
const purple = Color.lerp(red, blue, 0.5);
|
||||
```
|
||||
326
docs/src/content/docs/modules/network/fixed-point.md
Normal file
326
docs/src/content/docs/modules/network/fixed-point.md
Normal file
@@ -0,0 +1,326 @@
|
||||
---
|
||||
title: "定点数"
|
||||
description: "用于帧同步的确定性定点数数学库"
|
||||
---
|
||||
|
||||
`@esengine/ecs-framework-math` 提供确定性定点数计算,专为**帧同步 (Lockstep)** 设计。定点数在所有平台上保证产生完全相同的计算结果。
|
||||
|
||||
## 为什么需要定点数?
|
||||
|
||||
浮点数在不同平台上可能产生不同的舍入结果:
|
||||
|
||||
```typescript
|
||||
// 浮点数:不同平台可能得到不同结果
|
||||
const a = 0.1 + 0.2; // 0.30000000000000004 (某些平台)
|
||||
// 0.3 (其他平台)
|
||||
|
||||
// 定点数:所有平台结果一致
|
||||
const x = Fixed32.from(0.1);
|
||||
const y = Fixed32.from(0.2);
|
||||
const z = x.add(y); // raw = 19661 (所有平台)
|
||||
```
|
||||
|
||||
| 特性 | 浮点数 | 定点数 |
|
||||
|------|--------|--------|
|
||||
| 跨平台一致性 | ❌ 可能不同 | ✅ 完全一致 |
|
||||
| 网络同步模式 | 状态同步 | 帧同步 (Lockstep) |
|
||||
| 适用游戏类型 | FPS、RPG | RTS、MOBA、格斗 |
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework-math
|
||||
```
|
||||
|
||||
## Fixed32 定点数
|
||||
|
||||
Q16.16 格式:16 位整数 + 16 位小数,范围 ±32767.99998。
|
||||
|
||||
### 创建定点数
|
||||
|
||||
```typescript
|
||||
import { Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// 从浮点数创建
|
||||
const speed = Fixed32.from(5.5);
|
||||
|
||||
// 从整数创建(无精度损失)
|
||||
const count = Fixed32.fromInt(10);
|
||||
|
||||
// 从原始值创建(网络接收后使用)
|
||||
const received = Fixed32.fromRaw(360448); // 等于 5.5
|
||||
|
||||
// 预定义常量
|
||||
Fixed32.ZERO // 0
|
||||
Fixed32.ONE // 1
|
||||
Fixed32.HALF // 0.5
|
||||
Fixed32.PI // π
|
||||
Fixed32.TWO_PI // 2π
|
||||
Fixed32.HALF_PI // π/2
|
||||
```
|
||||
|
||||
### 基本运算
|
||||
|
||||
```typescript
|
||||
const a = Fixed32.from(10);
|
||||
const b = Fixed32.from(3);
|
||||
|
||||
const sum = a.add(b); // 13
|
||||
const diff = a.sub(b); // 7
|
||||
const prod = a.mul(b); // 30
|
||||
const quot = a.div(b); // 3.333...
|
||||
const mod = a.mod(b); // 1
|
||||
const neg = a.neg(); // -10
|
||||
const abs = neg.abs(); // 10
|
||||
```
|
||||
|
||||
### 比较运算
|
||||
|
||||
```typescript
|
||||
const x = Fixed32.from(5);
|
||||
const y = Fixed32.from(3);
|
||||
|
||||
x.eq(y) // false - 等于
|
||||
x.ne(y) // true - 不等于
|
||||
x.lt(y) // false - 小于
|
||||
x.le(y) // false - 小于等于
|
||||
x.gt(y) // true - 大于
|
||||
x.ge(y) // true - 大于等于
|
||||
|
||||
x.isZero() // false
|
||||
x.isPositive() // true
|
||||
x.isNegative() // false
|
||||
```
|
||||
|
||||
### 数学函数
|
||||
|
||||
```typescript
|
||||
// 平方根(牛顿迭代法,确定性)
|
||||
const sqrt = Fixed32.sqrt(Fixed32.from(16)); // 4
|
||||
|
||||
// 取整
|
||||
Fixed32.floor(Fixed32.from(3.7)) // 3
|
||||
Fixed32.ceil(Fixed32.from(3.2)) // 4
|
||||
Fixed32.round(Fixed32.from(3.5)) // 4
|
||||
|
||||
// 范围限制
|
||||
Fixed32.clamp(value, min, max)
|
||||
|
||||
// 线性插值
|
||||
Fixed32.lerp(from, to, t)
|
||||
|
||||
// 最大/最小值
|
||||
Fixed32.min(a, b)
|
||||
Fixed32.max(a, b)
|
||||
```
|
||||
|
||||
### 类型转换
|
||||
|
||||
```typescript
|
||||
const value = Fixed32.from(3.14159);
|
||||
|
||||
// 转为浮点数(用于渲染)
|
||||
const float = value.toNumber(); // 3.14159
|
||||
|
||||
// 获取原始值(用于网络传输)
|
||||
const raw = value.toRaw(); // 205887
|
||||
|
||||
// 转为整数(向下取整)
|
||||
const int = value.toInt(); // 3
|
||||
```
|
||||
|
||||
## FixedVector2 定点数向量
|
||||
|
||||
不可变的 2D 向量类,所有运算返回新实例。
|
||||
|
||||
### 创建向量
|
||||
|
||||
```typescript
|
||||
import { FixedVector2, Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// 从浮点数创建
|
||||
const pos = FixedVector2.from(100, 200);
|
||||
|
||||
// 从原始值创建(网络接收后使用)
|
||||
const received = FixedVector2.fromRaw(6553600, 13107200);
|
||||
|
||||
// 从 Fixed32 创建
|
||||
const vec = new FixedVector2(Fixed32.from(10), Fixed32.from(20));
|
||||
|
||||
// 预定义常量
|
||||
FixedVector2.ZERO // (0, 0)
|
||||
FixedVector2.ONE // (1, 1)
|
||||
FixedVector2.RIGHT // (1, 0)
|
||||
FixedVector2.LEFT // (-1, 0)
|
||||
FixedVector2.UP // (0, 1)
|
||||
FixedVector2.DOWN // (0, -1)
|
||||
```
|
||||
|
||||
### 向量运算
|
||||
|
||||
```typescript
|
||||
const a = FixedVector2.from(3, 4);
|
||||
const b = FixedVector2.from(1, 2);
|
||||
|
||||
// 基本运算
|
||||
const sum = a.add(b); // (4, 6)
|
||||
const diff = a.sub(b); // (2, 2)
|
||||
const scaled = a.mul(Fixed32.from(2)); // (6, 8)
|
||||
const divided = a.div(Fixed32.from(2)); // (1.5, 2)
|
||||
|
||||
// 向量积
|
||||
const dot = a.dot(b); // 3*1 + 4*2 = 11
|
||||
const cross = a.cross(b); // 3*2 - 4*1 = 2
|
||||
|
||||
// 长度
|
||||
const lenSq = a.lengthSquared(); // 25
|
||||
const len = a.length(); // 5
|
||||
|
||||
// 归一化
|
||||
const norm = a.normalize(); // (0.6, 0.8)
|
||||
|
||||
// 距离
|
||||
const dist = a.distanceTo(b); // sqrt((3-1)² + (4-2)²)
|
||||
```
|
||||
|
||||
### 旋转和角度
|
||||
|
||||
```typescript
|
||||
import { FixedMath } from '@esengine/ecs-framework-math';
|
||||
|
||||
const vec = FixedVector2.from(1, 0);
|
||||
const angle = Fixed32.from(Math.PI / 2); // 90度
|
||||
|
||||
// 旋转向量
|
||||
const rotated = vec.rotate(angle); // (0, 1)
|
||||
|
||||
// 围绕点旋转
|
||||
const center = FixedVector2.from(5, 5);
|
||||
const around = vec.rotateAround(center, angle);
|
||||
|
||||
// 获取向量角度
|
||||
const vecAngle = vec.angle();
|
||||
|
||||
// 两向量夹角
|
||||
const between = vec.angleTo(other);
|
||||
|
||||
// 从角度创建单位向量
|
||||
const dir = FixedVector2.fromAngle(angle);
|
||||
|
||||
// 从极坐标创建
|
||||
const polar = FixedVector2.fromPolar(length, angle);
|
||||
```
|
||||
|
||||
### 类型转换
|
||||
|
||||
```typescript
|
||||
const pos = FixedVector2.from(100.5, 200.5);
|
||||
|
||||
// 转为浮点对象(用于渲染)
|
||||
const obj = pos.toObject(); // { x: 100.5, y: 200.5 }
|
||||
|
||||
// 转为数组
|
||||
const arr = pos.toArray(); // [100.5, 200.5]
|
||||
|
||||
// 获取原始值(用于网络传输)
|
||||
const raw = pos.toRawObject(); // { x: 6586368, y: 13140992 }
|
||||
```
|
||||
|
||||
## FixedMath 三角函数
|
||||
|
||||
使用查找表实现确定性三角函数。
|
||||
|
||||
```typescript
|
||||
import { FixedMath, Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
const angle = Fixed32.from(Math.PI / 6); // 30度
|
||||
|
||||
// 三角函数
|
||||
const sin = FixedMath.sin(angle); // 0.5
|
||||
const cos = FixedMath.cos(angle); // 0.866
|
||||
const tan = FixedMath.tan(angle); // 0.577
|
||||
|
||||
// 反三角函数
|
||||
const atan = FixedMath.atan2(y, x);
|
||||
const asin = FixedMath.asin(value);
|
||||
const acos = FixedMath.acos(value);
|
||||
|
||||
// 角度规范化到 [-π, π]
|
||||
const normalized = FixedMath.normalizeAngle(angle);
|
||||
|
||||
// 角度差(最短路径)
|
||||
const delta = FixedMath.angleDelta(from, to);
|
||||
|
||||
// 角度插值(处理 360° 环绕)
|
||||
const lerped = FixedMath.lerpAngle(from, to, t);
|
||||
|
||||
// 弧度/角度转换
|
||||
const deg = FixedMath.radToDeg(rad);
|
||||
const rad = FixedMath.degToRad(deg);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 全程使用定点数计算
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:所有游戏逻辑使用定点数
|
||||
function calculateDamage(baseDamage: Fixed32, multiplier: Fixed32): Fixed32 {
|
||||
return baseDamage.mul(multiplier);
|
||||
}
|
||||
|
||||
// ❌ 错误:混用浮点数
|
||||
function calculateDamage(baseDamage: number, multiplier: number): number {
|
||||
return baseDamage * multiplier; // 可能不一致
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 只在渲染时转换为浮点数
|
||||
|
||||
```typescript
|
||||
// 游戏逻辑层
|
||||
const position: FixedVector2 = calculatePosition(input);
|
||||
|
||||
// 渲染层
|
||||
const { x, y } = position.toObject();
|
||||
sprite.position.set(x, y);
|
||||
```
|
||||
|
||||
### 3. 使用原始值进行网络传输
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:传输整数原始值
|
||||
const raw = position.toRawObject();
|
||||
send(JSON.stringify(raw));
|
||||
|
||||
// ❌ 错误:传输浮点数
|
||||
const float = position.toObject();
|
||||
send(JSON.stringify(float)); // 可能丢失精度
|
||||
```
|
||||
|
||||
### 4. 使用 FixedMath 进行三角运算
|
||||
|
||||
```typescript
|
||||
// ✅ 正确:使用查找表
|
||||
const direction = FixedVector2.fromAngle(FixedMath.atan2(dy, dx));
|
||||
|
||||
// ❌ 错误:使用 Math 库
|
||||
const angle = Math.atan2(dy.toNumber(), dx.toNumber()); // 不确定
|
||||
```
|
||||
|
||||
## API 导出
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Fixed32,
|
||||
FixedVector2,
|
||||
FixedMath,
|
||||
type IFixed32,
|
||||
type IFixedVector2
|
||||
} from '@esengine/ecs-framework-math';
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [状态同步](/modules/network/sync) - 定点数快照缓冲区
|
||||
- [客户端预测](/modules/network/prediction) - 定点数客户端预测
|
||||
@@ -252,3 +252,145 @@ if (predictionSystem) {
|
||||
console.log('Current sequence:', predictionSystem.inputSequence);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 定点数客户端预测(帧同步)
|
||||
|
||||
用于**帧同步 (Lockstep)** 的确定性客户端预测。
|
||||
|
||||
> 定点数基础知识请参考 [定点数文档](/modules/network/fixed-point)
|
||||
|
||||
### 基本用法
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedClientPrediction,
|
||||
createFixedClientPrediction,
|
||||
type IFixedPredictor,
|
||||
type IFixedStatePositionExtractor
|
||||
} from '@esengine/network';
|
||||
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// 定义游戏状态
|
||||
interface GameState {
|
||||
position: FixedVector2;
|
||||
velocity: FixedVector2;
|
||||
}
|
||||
|
||||
// 实现预测器(必须使用定点数运算)
|
||||
const predictor: IFixedPredictor<GameState, PlayerInput> = {
|
||||
predict(state: GameState, input: PlayerInput, deltaTime: Fixed32): GameState {
|
||||
const speed = Fixed32.from(100);
|
||||
const inputVec = FixedVector2.from(input.dx, input.dy);
|
||||
const velocity = inputVec.normalize().mul(speed);
|
||||
const displacement = velocity.mul(deltaTime);
|
||||
|
||||
return {
|
||||
position: state.position.add(displacement),
|
||||
velocity
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 创建预测器
|
||||
const prediction = createFixedClientPrediction(predictor, {
|
||||
maxUnacknowledgedInputs: 60,
|
||||
fixedDeltaTime: Fixed32.from(1 / 60),
|
||||
reconciliationThreshold: Fixed32.from(0.001),
|
||||
enableSmoothReconciliation: false // 帧同步通常关闭
|
||||
});
|
||||
```
|
||||
|
||||
### 记录输入
|
||||
|
||||
```typescript
|
||||
function onUpdate(input: PlayerInput, currentState: GameState) {
|
||||
// 记录输入并获得预测状态
|
||||
const predicted = prediction.recordInput(input, currentState);
|
||||
|
||||
// 渲染预测状态
|
||||
const pos = predicted.position.toObject();
|
||||
sprite.position.set(pos.x, pos.y);
|
||||
|
||||
// 发送输入
|
||||
socket.send(JSON.stringify({
|
||||
frame: prediction.currentFrame,
|
||||
input
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器校正
|
||||
|
||||
```typescript
|
||||
// 位置提取器
|
||||
const posExtractor: IFixedStatePositionExtractor<GameState> = {
|
||||
getPosition(state: GameState): FixedVector2 {
|
||||
return state.position;
|
||||
}
|
||||
};
|
||||
|
||||
// 收到服务器状态
|
||||
function onServerState(serverState: GameState, serverFrame: number) {
|
||||
const reconciled = prediction.reconcile(
|
||||
serverState,
|
||||
serverFrame,
|
||||
posExtractor
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 回滚重播
|
||||
|
||||
```typescript
|
||||
// 发现不同步时回滚
|
||||
const correctedState = prediction.rollbackAndResimulate(
|
||||
serverFrame,
|
||||
authoritativeState
|
||||
);
|
||||
|
||||
// 查看历史状态
|
||||
const historicalState = prediction.getStateAtFrame(100);
|
||||
```
|
||||
|
||||
### 预设移动预测器
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createFixedMovementPredictor,
|
||||
createFixedMovementPositionExtractor,
|
||||
type IFixedMovementInput,
|
||||
type IFixedMovementState
|
||||
} from '@esengine/network';
|
||||
|
||||
// 创建移动预测器(速度 100 单位/秒)
|
||||
const movePredictor = createFixedMovementPredictor(Fixed32.from(100));
|
||||
const posExtractor = createFixedMovementPositionExtractor();
|
||||
|
||||
const prediction = createFixedClientPrediction<IFixedMovementState, IFixedMovementInput>(
|
||||
movePredictor,
|
||||
{ fixedDeltaTime: Fixed32.from(1 / 60) }
|
||||
);
|
||||
|
||||
// 输入格式
|
||||
const input: IFixedMovementInput = { dx: 1, dy: 0 };
|
||||
```
|
||||
|
||||
### API 导出
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedClientPrediction,
|
||||
createFixedClientPrediction,
|
||||
createFixedMovementPredictor,
|
||||
createFixedMovementPositionExtractor,
|
||||
type IFixedInputSnapshot,
|
||||
type IFixedPredictedState,
|
||||
type IFixedPredictor,
|
||||
type IFixedStatePositionExtractor,
|
||||
type FixedClientPredictionConfig,
|
||||
type IFixedMovementInput,
|
||||
type IFixedMovementState
|
||||
} from '@esengine/network';
|
||||
```
|
||||
|
||||
@@ -340,3 +340,139 @@ if (!identity.bIsLocalPlayer) {
|
||||
3. **校正阈值**:根据游戏精度需求设置合适的阈值
|
||||
|
||||
4. **快照数量**:保持足够的快照以应对网络抖动
|
||||
|
||||
---
|
||||
|
||||
## 定点数同步(帧同步)
|
||||
|
||||
以下内容用于**帧同步 (Lockstep)** 架构,使用定点数确保跨平台确定性。
|
||||
|
||||
> 定点数基础知识请参考 [定点数文档](/modules/network/fixed-point)
|
||||
|
||||
### FixedTransformState
|
||||
|
||||
定点数变换状态,用于网络传输:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
type IFixedTransformStateRaw
|
||||
} from '@esengine/network';
|
||||
|
||||
// 创建状态
|
||||
const state = FixedTransformState.from(100, 200, Math.PI / 4);
|
||||
|
||||
// 序列化(发送方)
|
||||
const raw: IFixedTransformStateRaw = state.toRaw();
|
||||
socket.send(JSON.stringify({ type: 'sync', state: raw }));
|
||||
|
||||
// 反序列化(接收方)
|
||||
const received = FixedTransformState.fromRaw(message.state);
|
||||
|
||||
// 用于渲染
|
||||
const { x, y, rotation } = received.toFloat();
|
||||
sprite.position.set(x, y);
|
||||
```
|
||||
|
||||
带速度的状态(用于外推):
|
||||
|
||||
```typescript
|
||||
const state = FixedTransformStateWithVelocity.from(
|
||||
100, 200, // 位置
|
||||
0, // 旋转
|
||||
5, 3, // 速度
|
||||
0.1 // 角速度
|
||||
);
|
||||
```
|
||||
|
||||
### 定点数插值器
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator
|
||||
} from '@esengine/network';
|
||||
import { Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// 线性插值器
|
||||
const interpolator = createFixedTransformInterpolator();
|
||||
|
||||
const from = FixedTransformState.from(0, 0, 0);
|
||||
const to = FixedTransformState.from(100, 50, Math.PI);
|
||||
const t = Fixed32.from(0.5);
|
||||
|
||||
const result = interpolator.interpolate(from, to, t);
|
||||
|
||||
// Hermite 插值器(更平滑)
|
||||
const hermite = createFixedHermiteTransformInterpolator(100);
|
||||
```
|
||||
|
||||
### 定点数快照缓冲区
|
||||
|
||||
管理定点数状态历史,用于帧同步回放:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FixedSnapshotBuffer,
|
||||
createFixedSnapshotBuffer
|
||||
} from '@esengine/network';
|
||||
|
||||
// 创建缓冲区(最多 30 快照,2 帧延迟)
|
||||
const buffer = createFixedSnapshotBuffer<FixedTransformState>(30, 2);
|
||||
|
||||
// 添加快照
|
||||
buffer.push({
|
||||
frame: 100,
|
||||
state: FixedTransformState.from(100, 200, 0)
|
||||
});
|
||||
|
||||
// 获取插值快照
|
||||
const result = buffer.getInterpolationSnapshots(103);
|
||||
if (result) {
|
||||
const { from, to, t } = result;
|
||||
const interpolated = interpolator.interpolate(from.state, to.state, t);
|
||||
}
|
||||
|
||||
// 获取最新/指定帧快照
|
||||
const latest = buffer.getLatest();
|
||||
const atFrame = buffer.getAtFrame(100);
|
||||
|
||||
// 回滚重播
|
||||
const snapshotsToReplay = buffer.getSnapshotsAfter(98);
|
||||
|
||||
// 清理旧快照
|
||||
buffer.removeSnapshotsBefore(95);
|
||||
```
|
||||
|
||||
子帧插值:
|
||||
|
||||
```typescript
|
||||
// 使用 Fixed32 帧时间(支持小数帧)
|
||||
const frameTime = Fixed32.from(102.5);
|
||||
const result = buffer.getInterpolationSnapshotsFixed(frameTime);
|
||||
```
|
||||
|
||||
### API 导出
|
||||
|
||||
```typescript
|
||||
import {
|
||||
// 状态类
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
type IFixedTransformStateRaw,
|
||||
type IFixedTransformStateWithVelocityRaw,
|
||||
|
||||
// 插值器
|
||||
FixedTransformInterpolator,
|
||||
FixedHermiteTransformInterpolator,
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator,
|
||||
|
||||
// 快照缓冲区
|
||||
FixedSnapshotBuffer,
|
||||
createFixedSnapshotBuffer,
|
||||
type IFixedStateSnapshot,
|
||||
type IFixedInterpolationResult
|
||||
} from '@esengine/network';
|
||||
```
|
||||
|
||||
@@ -236,3 +236,261 @@ 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; }
|
||||
.bp-conn.vector2 { stroke: #2196F3; }
|
||||
.bp-conn.fixed32 { stroke: #9C27B0; }
|
||||
.bp-conn.fixedvector2 { stroke: #673AB7; }
|
||||
.bp-conn.color { stroke: #FF9800; }
|
||||
|
||||
/* ==================== 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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
# @esengine/node-editor
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#438](https://github.com/esengine/esengine/pull/438) [`0d33cf0`](https://github.com/esengine/esengine/commit/0d33cf00977d16e6282931aba2cf771ec2c84c6b) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): add visual group box for organizing nodes
|
||||
- Add NodeGroup model with dynamic bounds calculation based on node pin counts
|
||||
- Add GroupNodeComponent for rendering group boxes behind nodes
|
||||
- Groups automatically resize to wrap contained nodes
|
||||
- Dragging group header moves all nodes inside together
|
||||
- Support group serialization/deserialization
|
||||
- Export `estimateNodeHeight` and `NodeBounds` for accurate size calculation
|
||||
|
||||
feat(blueprint): add comprehensive math and logic nodes
|
||||
|
||||
Math nodes:
|
||||
- Modulo, Abs, Min, Max, Power, Sqrt
|
||||
- Floor, Ceil, Round, Sign, Negate
|
||||
- Sin, Cos, Tan, Asin, Acos, Atan, Atan2
|
||||
- DegToRad, RadToDeg, Lerp, InverseLerp
|
||||
- Clamp, Wrap, RandomRange, RandomInt
|
||||
|
||||
Logic nodes:
|
||||
- Equal, NotEqual, GreaterThan, GreaterThanOrEqual
|
||||
- LessThan, LessThanOrEqual, InRange
|
||||
- AND, OR, NOT, XOR, NAND
|
||||
- IsNull, Select (ternary)
|
||||
|
||||
## 1.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [`3bfb8a1`](https://github.com/esengine/esengine/commit/3bfb8a1c9baba18373717910d29f266a71c1f63e) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): add box selection and variable node error states
|
||||
- Add box selection: drag on empty canvas to select multiple nodes
|
||||
- Support Ctrl+drag for additive selection (add to existing selection)
|
||||
- Add error state styling for invalid variable references (red border, warning icon)
|
||||
- Support dynamic node title via `data.displayTitle`
|
||||
- Support hiding inputs via `data.hiddenInputs` array
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#435](https://github.com/esengine/esengine/pull/435) [`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e) Thanks [@esengine](https://github.com/esengine)! - fix(node-editor): 修复节点收缩后连线不显示的问题
|
||||
- 节点收缩时,连线会连接到节点头部(输入引脚在左侧,输出引脚在右侧)
|
||||
- 展开后连线会自动恢复到正确位置
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/node-editor",
|
||||
"version": "1.2.1",
|
||||
"version": "1.4.0",
|
||||
"description": "Universal node-based visual editor for blueprint, shader graph, and state machine",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -50,6 +50,15 @@ export interface GraphCanvasProps {
|
||||
/** Canvas context menu callback (画布右键菜单回调) */
|
||||
onContextMenu?: (position: Position, e: React.MouseEvent) => void;
|
||||
|
||||
/** Canvas mouse down callback for box selection (画布鼠标按下回调,用于框选) */
|
||||
onMouseDown?: (position: Position, e: React.MouseEvent) => void;
|
||||
|
||||
/** Canvas mouse move callback (画布鼠标移动回调) */
|
||||
onCanvasMouseMove?: (position: Position, e: React.MouseEvent) => void;
|
||||
|
||||
/** Canvas mouse up callback (画布鼠标释放回调) */
|
||||
onCanvasMouseUp?: (position: Position, e: React.MouseEvent) => void;
|
||||
|
||||
/** Children to render (要渲染的子元素) */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
@@ -75,6 +84,9 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
|
||||
onZoomChange,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
onMouseDown: onMouseDownProp,
|
||||
onCanvasMouseMove,
|
||||
onCanvasMouseUp,
|
||||
children
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -132,22 +144,30 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
|
||||
}, [zoom, pan, minZoom, maxZoom, updateZoom, updatePan]);
|
||||
|
||||
/**
|
||||
* Handles mouse down for panning
|
||||
* 处理鼠标按下开始平移
|
||||
* Handles mouse down for panning or box selection
|
||||
* 处理鼠标按下开始平移或框选
|
||||
*/
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
// Middle mouse button or space + left click for panning
|
||||
// 中键或空格+左键平移
|
||||
// Middle mouse button or Alt + left click for panning
|
||||
// 中键或 Alt+左键平移
|
||||
if (e.button === 1 || (e.button === 0 && e.altKey)) {
|
||||
e.preventDefault();
|
||||
setIsPanning(true);
|
||||
lastMousePos.current = new Position(e.clientX, e.clientY);
|
||||
} else if (e.button === 0) {
|
||||
// Left click on canvas background - start box selection
|
||||
// 左键点击画布背景 - 开始框选
|
||||
const target = e.target as HTMLElement;
|
||||
if (target === containerRef.current || target.classList.contains('ne-canvas-content')) {
|
||||
const canvasPos = screenToCanvas(e.clientX, e.clientY);
|
||||
onMouseDownProp?.(canvasPos, e);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [screenToCanvas, onMouseDownProp]);
|
||||
|
||||
/**
|
||||
* Handles mouse move for panning
|
||||
* 处理鼠标移动进行平移
|
||||
* Handles mouse move for panning or box selection
|
||||
* 处理鼠标移动进行平移或框选
|
||||
*/
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
if (isPanning) {
|
||||
@@ -157,13 +177,27 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
|
||||
const newPan = new Position(pan.x + dx, pan.y + dy);
|
||||
updatePan(newPan);
|
||||
}
|
||||
}, [isPanning, pan, updatePan]);
|
||||
// Always call canvas mouse move for box selection
|
||||
// 始终调用画布鼠标移动回调用于框选
|
||||
const canvasPos = screenToCanvas(e.clientX, e.clientY);
|
||||
onCanvasMouseMove?.(canvasPos, e);
|
||||
}, [isPanning, pan, updatePan, screenToCanvas, onCanvasMouseMove]);
|
||||
|
||||
/**
|
||||
* Handles mouse up to stop panning
|
||||
* 处理鼠标释放停止平移
|
||||
* Handles mouse up to stop panning or box selection
|
||||
* 处理鼠标释放停止平移或框选
|
||||
*/
|
||||
const handleMouseUp = useCallback(() => {
|
||||
const handleMouseUp = useCallback((e: React.MouseEvent) => {
|
||||
setIsPanning(false);
|
||||
const canvasPos = screenToCanvas(e.clientX, e.clientY);
|
||||
onCanvasMouseUp?.(canvasPos, e);
|
||||
}, [screenToCanvas, onCanvasMouseUp]);
|
||||
|
||||
/**
|
||||
* Handles mouse leave to stop panning
|
||||
* 处理鼠标离开停止平移
|
||||
*/
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsPanning(false);
|
||||
}, []);
|
||||
|
||||
@@ -276,7 +310,7 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
|
||||
@@ -4,8 +4,10 @@ import { GraphNode, NodeTemplate } from '../../domain/models/GraphNode';
|
||||
import { Connection } from '../../domain/models/Connection';
|
||||
import { Pin } from '../../domain/models/Pin';
|
||||
import { Position } from '../../domain/value-objects/Position';
|
||||
import { NodeGroup, computeGroupBounds, estimateNodeHeight } from '../../domain/models/NodeGroup';
|
||||
import { GraphCanvas } from '../canvas/GraphCanvas';
|
||||
import { MemoizedGraphNodeComponent, NodeExecutionState } from '../nodes/GraphNodeComponent';
|
||||
import { MemoizedGroupNodeComponent } from '../nodes/GroupNodeComponent';
|
||||
import { ConnectionLayer } from '../connections/ConnectionLine';
|
||||
|
||||
/**
|
||||
@@ -56,6 +58,12 @@ export interface NodeEditorProps {
|
||||
|
||||
/** Connection context menu callback (连接右键菜单回调) */
|
||||
onConnectionContextMenu?: (connection: Connection, e: React.MouseEvent) => void;
|
||||
|
||||
/** Group context menu callback (组右键菜单回调) */
|
||||
onGroupContextMenu?: (group: NodeGroup, e: React.MouseEvent) => void;
|
||||
|
||||
/** Group double click callback - typically used to expand group (组双击回调 - 通常用于展开组) */
|
||||
onGroupDoubleClick?: (group: NodeGroup) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +88,16 @@ interface ConnectionDragState {
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Box selection state
|
||||
* 框选状态
|
||||
*/
|
||||
interface BoxSelectState {
|
||||
startPos: Position;
|
||||
currentPos: Position;
|
||||
additive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* NodeEditor - Complete node graph editor component
|
||||
* NodeEditor - 完整的节点图编辑器组件
|
||||
@@ -102,7 +120,9 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
onNodeDoubleClick: _onNodeDoubleClick,
|
||||
onCanvasContextMenu,
|
||||
onNodeContextMenu,
|
||||
onConnectionContextMenu
|
||||
onConnectionContextMenu,
|
||||
onGroupContextMenu,
|
||||
onGroupDoubleClick
|
||||
}) => {
|
||||
// Silence unused variable warnings (消除未使用变量警告)
|
||||
void _templates;
|
||||
@@ -126,6 +146,11 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
const [dragState, setDragState] = useState<DragState | null>(null);
|
||||
const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null);
|
||||
const [hoveredPin, setHoveredPin] = useState<Pin | null>(null);
|
||||
const [boxSelectState, setBoxSelectState] = useState<BoxSelectState | null>(null);
|
||||
|
||||
// Track if box selection just ended to prevent click from clearing selection
|
||||
// 跟踪框选是否刚刚结束,以防止 click 清除选择
|
||||
const boxSelectJustEndedRef = useRef(false);
|
||||
|
||||
// Force re-render after mount to ensure connections are drawn correctly
|
||||
// 挂载后强制重渲染以确保连接线正确绘制
|
||||
@@ -137,6 +162,64 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
return graph.nodes.map(n => `${n.id}:${n.isCollapsed}`).join(',');
|
||||
}, [graph.nodes]);
|
||||
|
||||
// Groups are now simple visual boxes - no node hiding
|
||||
// 组现在是简单的可视化框 - 不隐藏节点
|
||||
|
||||
// Track selected group IDs (local state, managed similarly to nodes)
|
||||
// 跟踪选中的组ID(本地状态,类似节点管理方式)
|
||||
const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Group drag state - includes initial positions of nodes in the group
|
||||
// 组拖拽状态 - 包含组内节点的初始位置
|
||||
const [groupDragState, setGroupDragState] = useState<{
|
||||
groupId: string;
|
||||
startGroupPosition: Position;
|
||||
startMouse: { x: number; y: number };
|
||||
nodeStartPositions: Map<string, Position>;
|
||||
} | null>(null);
|
||||
|
||||
// Key for tracking group changes
|
||||
const groupsKey = useMemo(() => {
|
||||
return graph.groups.map(g => `${g.id}:${g.position.x}:${g.position.y}`).join(',');
|
||||
}, [graph.groups]);
|
||||
|
||||
// Compute dynamic group bounds based on current node positions and sizes
|
||||
// 根据当前节点位置和尺寸动态计算组边界
|
||||
const groupsWithDynamicBounds = useMemo(() => {
|
||||
const defaultNodeWidth = 200;
|
||||
|
||||
return graph.groups.map(group => {
|
||||
// Get current bounds of all nodes in this group
|
||||
const nodeBounds = group.nodeIds
|
||||
.map(nodeId => graph.getNode(nodeId))
|
||||
.filter((node): node is GraphNode => node !== undefined)
|
||||
.map(node => ({
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
width: defaultNodeWidth,
|
||||
height: estimateNodeHeight(
|
||||
node.inputPins.length,
|
||||
node.outputPins.length,
|
||||
node.isCollapsed
|
||||
)
|
||||
}));
|
||||
|
||||
if (nodeBounds.length === 0) {
|
||||
// No nodes found, use stored position/size as fallback
|
||||
return group;
|
||||
}
|
||||
|
||||
// Calculate dynamic bounds based on actual node sizes
|
||||
const { position, size } = computeGroupBounds(nodeBounds);
|
||||
|
||||
return {
|
||||
...group,
|
||||
position,
|
||||
size
|
||||
};
|
||||
});
|
||||
}, [graph.groups, graph.nodes]);
|
||||
|
||||
useEffect(() => {
|
||||
// Use requestAnimationFrame to wait for DOM to be fully rendered
|
||||
// 使用 requestAnimationFrame 等待 DOM 完全渲染
|
||||
@@ -144,7 +227,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
forceUpdate(n => n + 1);
|
||||
});
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [graph.id, collapsedNodesKey]);
|
||||
}, [graph.id, collapsedNodesKey, groupsKey]);
|
||||
|
||||
/**
|
||||
* Converts screen coordinates to canvas coordinates
|
||||
@@ -166,6 +249,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
*
|
||||
* 直接从节点位置和引脚在节点内的相对位置计算,不依赖 DOM 测量
|
||||
* 当节点收缩时,返回节点头部的位置
|
||||
* 当节点在折叠组中时,返回组节点的位置
|
||||
*/
|
||||
const getPinPosition = useCallback((pinId: string): Position | undefined => {
|
||||
// First, find which node this pin belongs to
|
||||
@@ -304,6 +388,22 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
onGraphChange?.(newGraph);
|
||||
}
|
||||
|
||||
// Group dragging - moves all nodes inside (group bounds are dynamic)
|
||||
// 组拖拽 - 移动组内所有节点(组边界是动态计算的)
|
||||
if (groupDragState) {
|
||||
const dx = mousePos.x - groupDragState.startMouse.x;
|
||||
const dy = mousePos.y - groupDragState.startMouse.y;
|
||||
|
||||
// Only move nodes - group bounds will auto-recalculate
|
||||
let newGraph = graph;
|
||||
for (const [nodeId, startPos] of groupDragState.nodeStartPositions) {
|
||||
const newNodePos = new Position(startPos.x + dx, startPos.y + dy);
|
||||
newGraph = newGraph.moveNode(nodeId, newNodePos);
|
||||
}
|
||||
|
||||
onGraphChange?.(newGraph);
|
||||
}
|
||||
|
||||
// Connection dragging (连接拖拽)
|
||||
if (connectionDrag) {
|
||||
const isValid = hoveredPin ? connectionDrag.fromPin.canConnectTo(hoveredPin) : undefined;
|
||||
@@ -315,7 +415,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
isValid
|
||||
} : null);
|
||||
}
|
||||
}, [graph, dragState, connectionDrag, hoveredPin, screenToCanvas, onGraphChange]);
|
||||
}, [graph, dragState, groupDragState, connectionDrag, hoveredPin, screenToCanvas, onGraphChange]);
|
||||
|
||||
/**
|
||||
* Handles mouse up to end dragging
|
||||
@@ -327,6 +427,11 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
setDragState(null);
|
||||
}
|
||||
|
||||
// End group dragging (结束组拖拽)
|
||||
if (groupDragState) {
|
||||
setGroupDragState(null);
|
||||
}
|
||||
|
||||
// End connection dragging (结束连接拖拽)
|
||||
if (connectionDrag) {
|
||||
// Use hoveredPin directly instead of relying on async state update
|
||||
@@ -361,7 +466,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
|
||||
setConnectionDrag(null);
|
||||
}
|
||||
}, [graph, dragState, connectionDrag, hoveredPin, onGraphChange]);
|
||||
}, [graph, dragState, groupDragState, connectionDrag, hoveredPin, onGraphChange]);
|
||||
|
||||
/**
|
||||
* Handles pin mouse down
|
||||
@@ -472,13 +577,95 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
}
|
||||
}, [graph, onConnectionContextMenu]);
|
||||
|
||||
/**
|
||||
* Handles group selection
|
||||
* 处理组选择
|
||||
*/
|
||||
const handleGroupSelect = useCallback((groupId: string, additive: boolean) => {
|
||||
if (readOnly) return;
|
||||
|
||||
const newSelection = new Set(selectedGroupIds);
|
||||
|
||||
if (additive) {
|
||||
if (newSelection.has(groupId)) {
|
||||
newSelection.delete(groupId);
|
||||
} else {
|
||||
newSelection.add(groupId);
|
||||
}
|
||||
} else {
|
||||
newSelection.clear();
|
||||
newSelection.add(groupId);
|
||||
}
|
||||
|
||||
setSelectedGroupIds(newSelection);
|
||||
// Clear node and connection selection when selecting groups
|
||||
onSelectionChange?.(new Set(), new Set());
|
||||
}, [selectedGroupIds, readOnly, onSelectionChange]);
|
||||
|
||||
/**
|
||||
* Handles group drag start
|
||||
* 处理组拖拽开始
|
||||
*
|
||||
* Captures initial positions of both the group and all nodes inside it
|
||||
* 捕获组和组内所有节点的初始位置
|
||||
*/
|
||||
const handleGroupDragStart = useCallback((groupId: string, startMouse: { x: number; y: number }) => {
|
||||
if (readOnly) return;
|
||||
|
||||
const group = graph.getGroup(groupId);
|
||||
if (!group) return;
|
||||
|
||||
// Convert screen coordinates to canvas coordinates (same as node dragging)
|
||||
// 将屏幕坐标转换为画布坐标(与节点拖拽相同)
|
||||
const canvasPos = screenToCanvas(startMouse.x, startMouse.y);
|
||||
|
||||
// Capture initial positions of all nodes in the group
|
||||
const nodeStartPositions = new Map<string, Position>();
|
||||
for (const nodeId of group.nodeIds) {
|
||||
const node = graph.getNode(nodeId);
|
||||
if (node) {
|
||||
nodeStartPositions.set(nodeId, node.position);
|
||||
}
|
||||
}
|
||||
|
||||
setGroupDragState({
|
||||
groupId,
|
||||
startGroupPosition: group.position,
|
||||
startMouse: { x: canvasPos.x, y: canvasPos.y },
|
||||
nodeStartPositions
|
||||
});
|
||||
}, [graph, readOnly, screenToCanvas]);
|
||||
|
||||
/**
|
||||
* Handles group context menu
|
||||
* 处理组右键菜单
|
||||
*/
|
||||
const handleGroupContextMenu = useCallback((group: NodeGroup, e: React.MouseEvent) => {
|
||||
onGroupContextMenu?.(group, e);
|
||||
}, [onGroupContextMenu]);
|
||||
|
||||
/**
|
||||
* Handles group double click
|
||||
* 处理组双击
|
||||
*/
|
||||
const handleGroupDoubleClick = useCallback((group: NodeGroup) => {
|
||||
onGroupDoubleClick?.(group);
|
||||
}, [onGroupDoubleClick]);
|
||||
|
||||
/**
|
||||
* Handles canvas click to deselect
|
||||
* 处理画布点击取消选择
|
||||
*/
|
||||
const handleCanvasClick = useCallback((_position: Position, _e: React.MouseEvent) => {
|
||||
// Skip if box selection just ended (click fires after mouseup)
|
||||
// 如果框选刚刚结束则跳过(click 在 mouseup 之后触发)
|
||||
if (boxSelectJustEndedRef.current) {
|
||||
boxSelectJustEndedRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (!readOnly) {
|
||||
onSelectionChange?.(new Set(), new Set());
|
||||
setSelectedGroupIds(new Set());
|
||||
}
|
||||
}, [readOnly, onSelectionChange]);
|
||||
|
||||
@@ -490,6 +677,79 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
onCanvasContextMenu?.(position, e);
|
||||
}, [onCanvasContextMenu]);
|
||||
|
||||
/**
|
||||
* Handles box selection start
|
||||
* 处理框选开始
|
||||
*/
|
||||
const handleBoxSelectStart = useCallback((position: Position, e: React.MouseEvent) => {
|
||||
if (readOnly) return;
|
||||
setBoxSelectState({
|
||||
startPos: position,
|
||||
currentPos: position,
|
||||
additive: e.ctrlKey || e.metaKey
|
||||
});
|
||||
}, [readOnly]);
|
||||
|
||||
/**
|
||||
* Handles box selection move
|
||||
* 处理框选移动
|
||||
*/
|
||||
const handleBoxSelectMove = useCallback((position: Position) => {
|
||||
if (boxSelectState) {
|
||||
setBoxSelectState(prev => prev ? { ...prev, currentPos: position } : null);
|
||||
}
|
||||
}, [boxSelectState]);
|
||||
|
||||
/**
|
||||
* Handles box selection end
|
||||
* 处理框选结束
|
||||
*/
|
||||
const handleBoxSelectEnd = useCallback(() => {
|
||||
if (!boxSelectState) return;
|
||||
|
||||
const { startPos, currentPos, additive } = boxSelectState;
|
||||
|
||||
// Calculate selection box bounds
|
||||
const minX = Math.min(startPos.x, currentPos.x);
|
||||
const maxX = Math.max(startPos.x, currentPos.x);
|
||||
const minY = Math.min(startPos.y, currentPos.y);
|
||||
const maxY = Math.max(startPos.y, currentPos.y);
|
||||
|
||||
// Find nodes within the selection box
|
||||
const nodesInBox: string[] = [];
|
||||
const nodeWidth = 200; // Approximate node width
|
||||
const nodeHeight = 100; // Approximate node height
|
||||
|
||||
for (const node of graph.nodes) {
|
||||
const nodeLeft = node.position.x;
|
||||
const nodeTop = node.position.y;
|
||||
const nodeRight = nodeLeft + nodeWidth;
|
||||
const nodeBottom = nodeTop + nodeHeight;
|
||||
|
||||
// Check if node intersects with selection box
|
||||
const intersects = !(nodeRight < minX || nodeLeft > maxX || nodeBottom < minY || nodeTop > maxY);
|
||||
if (intersects) {
|
||||
nodesInBox.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Update selection
|
||||
if (additive) {
|
||||
// Add to existing selection
|
||||
const newSelection = new Set(selectedNodeIds);
|
||||
nodesInBox.forEach(id => newSelection.add(id));
|
||||
onSelectionChange?.(newSelection, new Set());
|
||||
} else {
|
||||
// Replace selection
|
||||
onSelectionChange?.(new Set(nodesInBox), new Set());
|
||||
}
|
||||
|
||||
// Mark that box selection just ended to prevent click from clearing selection
|
||||
// 标记框选刚刚结束,以防止 click 清除选择
|
||||
boxSelectJustEndedRef.current = true;
|
||||
setBoxSelectState(null);
|
||||
}, [boxSelectState, graph.nodes, selectedNodeIds, onSelectionChange]);
|
||||
|
||||
/**
|
||||
* Handles pin value change
|
||||
* 处理引脚值变化
|
||||
@@ -546,7 +806,25 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
onContextMenu={handleCanvasContextMenu}
|
||||
onPanChange={handlePanChange}
|
||||
onZoomChange={handleZoomChange}
|
||||
onMouseDown={handleBoxSelectStart}
|
||||
onCanvasMouseMove={handleBoxSelectMove}
|
||||
onCanvasMouseUp={handleBoxSelectEnd}
|
||||
>
|
||||
{/* Group boxes - rendered first so they appear behind nodes (组框 - 先渲染,这样显示在节点后面) */}
|
||||
{/* Use dynamically calculated bounds so groups auto-resize to fit nodes */}
|
||||
{groupsWithDynamicBounds.map(group => (
|
||||
<MemoizedGroupNodeComponent
|
||||
key={group.id}
|
||||
group={group}
|
||||
isSelected={selectedGroupIds.has(group.id)}
|
||||
isDragging={groupDragState?.groupId === group.id}
|
||||
onSelect={handleGroupSelect}
|
||||
onDragStart={handleGroupDragStart}
|
||||
onContextMenu={handleGroupContextMenu}
|
||||
onDoubleClick={handleGroupDoubleClick}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Connection layer (连接层) */}
|
||||
<ConnectionLayer
|
||||
connections={graph.connections}
|
||||
@@ -558,7 +836,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
onConnectionContextMenu={handleConnectionContextMenu}
|
||||
/>
|
||||
|
||||
{/* Nodes (节点) */}
|
||||
{/* All Nodes (所有节点) */}
|
||||
{graph.nodes.map(node => (
|
||||
<MemoizedGraphNodeComponent
|
||||
key={node.id}
|
||||
@@ -580,6 +858,19 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
onToggleCollapse={handleToggleCollapse}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Box selection overlay (框选覆盖层) */}
|
||||
{boxSelectState && (
|
||||
<div
|
||||
className="ne-selection-box"
|
||||
style={{
|
||||
left: Math.min(boxSelectState.startPos.x, boxSelectState.currentPos.x),
|
||||
top: Math.min(boxSelectState.startPos.y, boxSelectState.currentPos.y),
|
||||
width: Math.abs(boxSelectState.currentPos.x - boxSelectState.startPos.x),
|
||||
height: Math.abs(boxSelectState.currentPos.y - boxSelectState.startPos.y)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</GraphCanvas>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,6 +64,8 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
|
||||
return draggingFromPin.canConnectTo(pin);
|
||||
}, [draggingFromPin]);
|
||||
|
||||
const hasError = Boolean(node.data.invalidVariable);
|
||||
|
||||
const classNames = useMemo(() => {
|
||||
const classes = ['ne-node'];
|
||||
if (isSelected) classes.push('selected');
|
||||
@@ -71,8 +73,9 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
|
||||
if (node.isCollapsed) classes.push('collapsed');
|
||||
if (node.category === 'comment') classes.push('comment');
|
||||
if (executionState !== 'idle') classes.push(executionState);
|
||||
if (hasError) classes.push('has-error');
|
||||
return classes.join(' ');
|
||||
}, [isSelected, isDragging, node.isCollapsed, node.category, executionState]);
|
||||
}, [isSelected, isDragging, node.isCollapsed, node.category, executionState, hasError]);
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
@@ -102,8 +105,10 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
|
||||
: undefined;
|
||||
|
||||
// Separate exec pins from data pins
|
||||
// Also filter out pins listed in data.hiddenInputs
|
||||
const hiddenInputs = (node.data.hiddenInputs as string[]) || [];
|
||||
const inputExecPins = node.inputPins.filter(p => p.isExec && !p.hidden);
|
||||
const inputDataPins = node.inputPins.filter(p => !p.isExec && !p.hidden);
|
||||
const inputDataPins = node.inputPins.filter(p => !p.isExec && !p.hidden && !hiddenInputs.includes(p.name));
|
||||
const outputExecPins = node.outputPins.filter(p => p.isExec && !p.hidden);
|
||||
const outputDataPins = node.outputPins.filter(p => !p.isExec && !p.hidden);
|
||||
|
||||
@@ -129,13 +134,17 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
|
||||
className={`ne-node-header ${node.category}`}
|
||||
style={headerStyle}
|
||||
>
|
||||
{/* Diamond icon for event nodes, or custom icon */}
|
||||
{/* Warning icon for invalid nodes, or diamond/custom icon */}
|
||||
<span className="ne-node-header-icon">
|
||||
{node.icon && renderIcon ? renderIcon(node.icon) : null}
|
||||
{hasError ? (
|
||||
<span className="ne-node-warning-icon" title={`Variable '${node.data.variableName}' not found`}>⚠</span>
|
||||
) : (
|
||||
node.icon && renderIcon ? renderIcon(node.icon) : null
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="ne-node-header-title">
|
||||
{node.title}
|
||||
{(node.data.displayTitle as string) || node.title}
|
||||
{node.subtitle && (
|
||||
<span className="ne-node-header-subtitle">
|
||||
{node.subtitle}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { NodeGroup } from '../../domain/models/NodeGroup';
|
||||
import '../../styles/GroupNode.css';
|
||||
|
||||
export interface GroupNodeComponentProps {
|
||||
/** The group to render */
|
||||
group: NodeGroup;
|
||||
|
||||
/** Whether the group is selected */
|
||||
isSelected?: boolean;
|
||||
|
||||
/** Whether the group is being dragged */
|
||||
isDragging?: boolean;
|
||||
|
||||
/** Selection handler */
|
||||
onSelect?: (groupId: string, additive: boolean) => void;
|
||||
|
||||
/** Drag start handler */
|
||||
onDragStart?: (groupId: string, startPosition: { x: number; y: number }) => void;
|
||||
|
||||
/** Context menu handler */
|
||||
onContextMenu?: (group: NodeGroup, e: React.MouseEvent) => void;
|
||||
|
||||
/** Double click handler for editing name */
|
||||
onDoubleClick?: (group: NodeGroup) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* GroupNodeComponent - Renders a visual group box around nodes
|
||||
* GroupNodeComponent - 渲染节点周围的可视化组框
|
||||
*
|
||||
* This is a simple background box that provides visual organization.
|
||||
* 这是一个简单的背景框,提供视觉组织功能。
|
||||
*/
|
||||
export const GroupNodeComponent: React.FC<GroupNodeComponentProps> = ({
|
||||
group,
|
||||
isSelected = false,
|
||||
isDragging = false,
|
||||
onSelect,
|
||||
onDragStart,
|
||||
onContextMenu,
|
||||
onDoubleClick
|
||||
}) => {
|
||||
const groupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
// Only handle clicks on the header or border, not on the content area
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.ne-group-box-header') && !target.classList.contains('ne-group-box')) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
|
||||
const additive = e.ctrlKey || e.metaKey;
|
||||
onSelect?.(group.id, additive);
|
||||
onDragStart?.(group.id, { x: e.clientX, y: e.clientY });
|
||||
}, [group.id, onSelect, onDragStart]);
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onContextMenu?.(group, e);
|
||||
}, [group, onContextMenu]);
|
||||
|
||||
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
||||
// Only handle double-click on the header
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.ne-group-box-header')) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
onDoubleClick?.(group);
|
||||
}, [group, onDoubleClick]);
|
||||
|
||||
const classNames = useMemo(() => {
|
||||
const classes = ['ne-group-box'];
|
||||
if (isSelected) classes.push('selected');
|
||||
if (isDragging) classes.push('dragging');
|
||||
return classes.join(' ');
|
||||
}, [isSelected, isDragging]);
|
||||
|
||||
const style: React.CSSProperties = useMemo(() => ({
|
||||
left: group.position.x,
|
||||
top: group.position.y,
|
||||
width: group.size.width,
|
||||
height: group.size.height,
|
||||
'--group-color': group.color || 'rgba(100, 149, 237, 0.15)'
|
||||
} as React.CSSProperties), [group.position.x, group.position.y, group.size.width, group.size.height, group.color]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={groupRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-group-id={group.id}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div className="ne-group-box-header">
|
||||
<span className="ne-group-box-title">{group.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Memoized version for performance
|
||||
*/
|
||||
export const MemoizedGroupNodeComponent = React.memo(GroupNodeComponent, (prev, next) => {
|
||||
if (prev.group.id !== next.group.id) return false;
|
||||
if (prev.isSelected !== next.isSelected) return false;
|
||||
if (prev.isDragging !== next.isDragging) return false;
|
||||
if (prev.group.position.x !== next.group.position.x ||
|
||||
prev.group.position.y !== next.group.position.y) return false;
|
||||
if (prev.group.size.width !== next.group.size.width ||
|
||||
prev.group.size.height !== next.group.size.height) return false;
|
||||
if (prev.group.name !== next.group.name) return false;
|
||||
if (prev.group.color !== next.group.color) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
export default GroupNodeComponent;
|
||||
@@ -4,3 +4,9 @@ export {
|
||||
type GraphNodeComponentProps,
|
||||
type NodeExecutionState
|
||||
} from './GraphNodeComponent';
|
||||
|
||||
export {
|
||||
GroupNodeComponent,
|
||||
MemoizedGroupNodeComponent,
|
||||
type GroupNodeComponentProps
|
||||
} from './GroupNodeComponent';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GraphNode } from './GraphNode';
|
||||
import { Connection } from './Connection';
|
||||
import { Pin } from './Pin';
|
||||
import { Position } from '../value-objects/Position';
|
||||
import { NodeGroup, serializeNodeGroup } from './NodeGroup';
|
||||
|
||||
/**
|
||||
* Graph - Aggregate root for the node graph
|
||||
@@ -15,6 +16,7 @@ export class Graph {
|
||||
private readonly _name: string;
|
||||
private readonly _nodes: Map<string, GraphNode>;
|
||||
private readonly _connections: Connection[];
|
||||
private readonly _groups: NodeGroup[];
|
||||
private readonly _metadata: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
@@ -22,12 +24,14 @@ export class Graph {
|
||||
name: string,
|
||||
nodes: GraphNode[] = [],
|
||||
connections: Connection[] = [],
|
||||
metadata: Record<string, unknown> = {}
|
||||
metadata: Record<string, unknown> = {},
|
||||
groups: NodeGroup[] = []
|
||||
) {
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._nodes = new Map(nodes.map(n => [n.id, n]));
|
||||
this._connections = [...connections];
|
||||
this._groups = [...groups];
|
||||
this._metadata = { ...metadata };
|
||||
}
|
||||
|
||||
@@ -59,6 +63,29 @@ export class Graph {
|
||||
return this._connections.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all groups (节点组)
|
||||
*/
|
||||
get groups(): NodeGroup[] {
|
||||
return [...this._groups];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a group by ID
|
||||
* 通过ID获取组
|
||||
*/
|
||||
getGroup(groupId: string): NodeGroup | undefined {
|
||||
return this._groups.find(g => g.id === groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the group containing a specific node
|
||||
* 获取包含特定节点的组
|
||||
*/
|
||||
getNodeGroup(nodeId: string): NodeGroup | undefined {
|
||||
return this._groups.find(g => g.nodeIds.includes(nodeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node by ID
|
||||
* 通过ID获取节点
|
||||
@@ -112,7 +139,7 @@ export class Graph {
|
||||
throw new Error(`Node with ID "${node.id}" already exists`);
|
||||
}
|
||||
const newNodes = [...this.nodes, node];
|
||||
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata);
|
||||
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata, this._groups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,7 +152,14 @@ export class Graph {
|
||||
}
|
||||
const newNodes = this.nodes.filter(n => n.id !== nodeId);
|
||||
const newConnections = this._connections.filter(c => !c.involvesNode(nodeId));
|
||||
return new Graph(this._id, this._name, newNodes, newConnections, this._metadata);
|
||||
// Also remove the node from any groups it belongs to
|
||||
const newGroups = this._groups.map(g => {
|
||||
if (g.nodeIds.includes(nodeId)) {
|
||||
return { ...g, nodeIds: g.nodeIds.filter(id => id !== nodeId) };
|
||||
}
|
||||
return g;
|
||||
}).filter(g => g.nodeIds.length > 0); // Remove empty groups
|
||||
return new Graph(this._id, this._name, newNodes, newConnections, this._metadata, newGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +172,7 @@ export class Graph {
|
||||
|
||||
const updatedNode = updater(node);
|
||||
const newNodes = this.nodes.map(n => n.id === nodeId ? updatedNode : n);
|
||||
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata);
|
||||
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata, this._groups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +218,7 @@ export class Graph {
|
||||
}
|
||||
|
||||
newConnections.push(connection);
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata, this._groups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +230,7 @@ export class Graph {
|
||||
if (newConnections.length === this._connections.length) {
|
||||
return this;
|
||||
}
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata, this._groups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +242,47 @@ export class Graph {
|
||||
if (newConnections.length === this._connections.length) {
|
||||
return this;
|
||||
}
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata, this._groups);
|
||||
}
|
||||
|
||||
// ========== Group Operations (组操作) ==========
|
||||
|
||||
/**
|
||||
* Adds a new group (immutable)
|
||||
* 添加新组(不可变)
|
||||
*/
|
||||
addGroup(group: NodeGroup): Graph {
|
||||
if (this._groups.some(g => g.id === group.id)) {
|
||||
throw new Error(`Group with ID "${group.id}" already exists`);
|
||||
}
|
||||
const newGroups = [...this._groups, group];
|
||||
return new Graph(this._id, this._name, this.nodes, this._connections, this._metadata, newGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a group (immutable)
|
||||
* 移除组(不可变)
|
||||
*/
|
||||
removeGroup(groupId: string): Graph {
|
||||
const newGroups = this._groups.filter(g => g.id !== groupId);
|
||||
if (newGroups.length === this._groups.length) {
|
||||
return this;
|
||||
}
|
||||
return new Graph(this._id, this._name, this.nodes, this._connections, this._metadata, newGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a group (immutable)
|
||||
* 更新组(不可变)
|
||||
*/
|
||||
updateGroup(groupId: string, updater: (group: NodeGroup) => NodeGroup): Graph {
|
||||
const groupIndex = this._groups.findIndex(g => g.id === groupId);
|
||||
if (groupIndex === -1) return this;
|
||||
|
||||
const updatedGroup = updater(this._groups[groupIndex]);
|
||||
const newGroups = [...this._groups];
|
||||
newGroups[groupIndex] = updatedGroup;
|
||||
return new Graph(this._id, this._name, this.nodes, this._connections, this._metadata, newGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +293,7 @@ export class Graph {
|
||||
return new Graph(this._id, this._name, this.nodes, this._connections, {
|
||||
...this._metadata,
|
||||
...metadata
|
||||
});
|
||||
}, this._groups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +301,7 @@ export class Graph {
|
||||
* 创建具有更新名称的新图(不可变)
|
||||
*/
|
||||
rename(newName: string): Graph {
|
||||
return new Graph(this._id, newName, this.nodes, this._connections, this._metadata);
|
||||
return new Graph(this._id, newName, this.nodes, this._connections, this._metadata, this._groups);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,6 +331,7 @@ export class Graph {
|
||||
name: this._name,
|
||||
nodes: this.nodes.map(n => n.toJSON()),
|
||||
connections: this._connections.map(c => c.toJSON()),
|
||||
groups: this._groups.map(g => serializeNodeGroup(g)),
|
||||
metadata: this._metadata
|
||||
};
|
||||
}
|
||||
|
||||
146
packages/devtools/node-editor/src/domain/models/NodeGroup.ts
Normal file
146
packages/devtools/node-editor/src/domain/models/NodeGroup.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Position } from '../value-objects/Position';
|
||||
|
||||
/**
|
||||
* NodeGroup - Represents a visual group box around nodes
|
||||
* NodeGroup - 表示节点周围的可视化组框
|
||||
*
|
||||
* Groups are purely visual organization - they don't affect runtime execution.
|
||||
* 组是纯视觉组织 - 不影响运行时执行。
|
||||
*/
|
||||
export interface NodeGroup {
|
||||
/** Unique identifier for the group */
|
||||
id: string;
|
||||
|
||||
/** Display name of the group */
|
||||
name: string;
|
||||
|
||||
/** IDs of nodes contained in this group */
|
||||
nodeIds: string[];
|
||||
|
||||
/** Position of the group box (top-left corner) */
|
||||
position: Position;
|
||||
|
||||
/** Size of the group box */
|
||||
size: { width: number; height: number };
|
||||
|
||||
/** Optional color for the group box */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NodeGroup with the given properties
|
||||
*/
|
||||
export function createNodeGroup(
|
||||
id: string,
|
||||
name: string,
|
||||
nodeIds: string[],
|
||||
position: Position,
|
||||
size: { width: number; height: number },
|
||||
color?: string
|
||||
): NodeGroup {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
nodeIds: [...nodeIds],
|
||||
position,
|
||||
size,
|
||||
color
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node bounds info for group calculation
|
||||
* 用于组计算的节点边界信息
|
||||
*/
|
||||
export interface NodeBounds {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates node height based on pin count
|
||||
* 根据引脚数量估算节点高度
|
||||
*/
|
||||
export function estimateNodeHeight(inputPinCount: number, outputPinCount: number, isCollapsed: boolean = false): number {
|
||||
if (isCollapsed) {
|
||||
return 32; // Just header
|
||||
}
|
||||
const headerHeight = 32;
|
||||
const pinHeight = 26;
|
||||
const bottomPadding = 12;
|
||||
const maxPins = Math.max(inputPinCount, outputPinCount);
|
||||
return headerHeight + maxPins * pinHeight + bottomPadding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the bounding box for a group based on its nodes
|
||||
* Returns position (top-left) and size with padding
|
||||
*
|
||||
* @param nodeBounds - Array of node bounds (position + size)
|
||||
* @param padding - Padding around the group box
|
||||
*/
|
||||
export function computeGroupBounds(
|
||||
nodeBounds: NodeBounds[],
|
||||
padding: number = 30
|
||||
): { position: Position; size: { width: number; height: number } } {
|
||||
if (nodeBounds.length === 0) {
|
||||
return {
|
||||
position: new Position(0, 0),
|
||||
size: { width: 250, height: 150 }
|
||||
};
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const node of nodeBounds) {
|
||||
minX = Math.min(minX, node.x);
|
||||
minY = Math.min(minY, node.y);
|
||||
maxX = Math.max(maxX, node.x + node.width);
|
||||
maxY = Math.max(maxY, node.y + node.height);
|
||||
}
|
||||
|
||||
// Add padding and header space for group title
|
||||
const groupHeaderHeight = 28;
|
||||
return {
|
||||
position: new Position(minX - padding, minY - padding - groupHeaderHeight),
|
||||
size: {
|
||||
width: maxX - minX + padding * 2,
|
||||
height: maxY - minY + padding * 2 + groupHeaderHeight
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a NodeGroup for JSON storage
|
||||
*/
|
||||
export function serializeNodeGroup(group: NodeGroup): Record<string, unknown> {
|
||||
return {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
nodeIds: [...group.nodeIds],
|
||||
position: { x: group.position.x, y: group.position.y },
|
||||
size: { width: group.size.width, height: group.size.height },
|
||||
color: group.color
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a NodeGroup from JSON
|
||||
*/
|
||||
export function deserializeNodeGroup(data: Record<string, unknown>): NodeGroup {
|
||||
const pos = data.position as { x: number; y: number } | undefined;
|
||||
const size = data.size as { width: number; height: number } | undefined;
|
||||
return {
|
||||
id: data.id as string,
|
||||
name: data.name as string,
|
||||
nodeIds: (data.nodeIds as string[]) || [],
|
||||
position: new Position(pos?.x || 0, pos?.y || 0),
|
||||
size: size || { width: 250, height: 150 },
|
||||
color: data.color as string | undefined
|
||||
};
|
||||
}
|
||||
@@ -2,3 +2,12 @@ export { Pin, type PinDefinition } from './Pin';
|
||||
export { GraphNode, type NodeTemplate, type NodeCategory } from './GraphNode';
|
||||
export { Connection } from './Connection';
|
||||
export { Graph } from './Graph';
|
||||
export {
|
||||
type NodeGroup,
|
||||
type NodeBounds,
|
||||
createNodeGroup,
|
||||
computeGroupBounds,
|
||||
estimateNodeHeight,
|
||||
serializeNodeGroup,
|
||||
deserializeNodeGroup
|
||||
} from './NodeGroup';
|
||||
|
||||
@@ -23,7 +23,15 @@ export {
|
||||
// Types
|
||||
type NodeTemplate,
|
||||
type NodeCategory,
|
||||
type PinDefinition
|
||||
type PinDefinition,
|
||||
// NodeGroup
|
||||
type NodeGroup,
|
||||
type NodeBounds,
|
||||
createNodeGroup,
|
||||
computeGroupBounds,
|
||||
estimateNodeHeight,
|
||||
serializeNodeGroup,
|
||||
deserializeNodeGroup
|
||||
} from './domain/models';
|
||||
|
||||
// Value objects (值对象)
|
||||
|
||||
@@ -38,6 +38,24 @@
|
||||
z-index: var(--ne-z-dragging);
|
||||
}
|
||||
|
||||
/* Error state for invalid variable references */
|
||||
.ne-node.has-error {
|
||||
border-color: #e74c3c;
|
||||
box-shadow: 0 0 0 1px #e74c3c,
|
||||
0 0 12px rgba(231, 76, 60, 0.4),
|
||||
0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.ne-node.has-error .ne-node-header {
|
||||
background: linear-gradient(180deg, #c0392b 0%, #962d22 100%) !important;
|
||||
}
|
||||
|
||||
.ne-node-warning-icon {
|
||||
color: #f1c40f;
|
||||
font-size: 14px;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
/* ==================== Node Header 节点头部 ==================== */
|
||||
.ne-node-header {
|
||||
display: flex;
|
||||
@@ -263,3 +281,32 @@
|
||||
0 0 20px rgba(255, 167, 38, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== Group Node 组节点 ==================== */
|
||||
.ne-node.ne-group-node {
|
||||
border: 2px dashed rgba(100, 149, 237, 0.6);
|
||||
background: rgba(40, 60, 90, 0.85);
|
||||
}
|
||||
|
||||
.ne-node.ne-group-node:hover {
|
||||
border-color: rgba(100, 149, 237, 0.8);
|
||||
}
|
||||
|
||||
.ne-node.ne-group-node.selected {
|
||||
border-color: #6495ed;
|
||||
box-shadow: 0 0 0 1px #6495ed,
|
||||
0 0 16px rgba(100, 149, 237, 0.5),
|
||||
0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.ne-group-header {
|
||||
background: linear-gradient(90deg, #3a5f8a 0%, #2a4a6a 100%) !important;
|
||||
}
|
||||
|
||||
.ne-group-hint {
|
||||
padding: 8px 12px;
|
||||
font-size: 11px;
|
||||
color: #8899aa;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
60
packages/devtools/node-editor/src/styles/GroupNode.css
Normal file
60
packages/devtools/node-editor/src/styles/GroupNode.css
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Group Box Styles
|
||||
* 组框样式
|
||||
*/
|
||||
|
||||
/* ==================== Group Box Container 组框容器 ==================== */
|
||||
.ne-group-box {
|
||||
position: absolute;
|
||||
background: var(--group-color, rgba(100, 149, 237, 0.15));
|
||||
border: 2px dashed rgba(100, 149, 237, 0.5);
|
||||
border-radius: 8px;
|
||||
pointer-events: auto;
|
||||
z-index: 0;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.ne-group-box:hover {
|
||||
border-color: rgba(100, 149, 237, 0.7);
|
||||
}
|
||||
|
||||
.ne-group-box.selected {
|
||||
border-color: var(--ne-node-border-selected, #e5a020);
|
||||
box-shadow: 0 0 0 1px var(--ne-node-border-selected, #e5a020),
|
||||
0 0 12px rgba(229, 160, 32, 0.3);
|
||||
}
|
||||
|
||||
.ne-group-box.dragging {
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* ==================== Group Box Header 组框头部 ==================== */
|
||||
.ne-group-box-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
background: rgba(100, 149, 237, 0.3);
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ne-group-box-header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.ne-group-box-title {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
@import './variables.css';
|
||||
@import './Canvas.css';
|
||||
@import './GraphNode.css';
|
||||
@import './GroupNode.css';
|
||||
@import './NodePin.css';
|
||||
@import './Connection.css';
|
||||
@import './ContextMenu.css';
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* 清理开发服务器进程
|
||||
* 用于 Windows 平台自动清理残留的 Vite 进程
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const PORT = 5173;
|
||||
|
||||
try {
|
||||
console.log(`正在查找占用端口 ${PORT} 的进程...`);
|
||||
|
||||
// Windows 命令
|
||||
const result = execSync(`netstat -ano | findstr :${PORT}`, { encoding: 'utf8' });
|
||||
|
||||
// 解析 PID
|
||||
const lines = result.split('\n');
|
||||
const pids = new Set();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('LISTENING')) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parts[parts.length - 1];
|
||||
if (pid && pid !== '0') {
|
||||
pids.add(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pids.size === 0) {
|
||||
console.log(`✓ 端口 ${PORT} 未被占用`);
|
||||
} else {
|
||||
console.log(`发现 ${pids.size} 个进程占用端口 ${PORT}`);
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
// Windows 需要使用 /F /PID 而不是 //F //PID
|
||||
execSync(`taskkill /F /PID ${pid}`, { encoding: 'utf8', stdio: 'ignore' });
|
||||
console.log(`✓ 已终止进程 PID: ${pid}`);
|
||||
} catch (e) {
|
||||
console.log(`✗ 无法终止进程 PID: ${pid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果 netstat 没有找到结果,会抛出错误,这是正常的
|
||||
console.log(`✓ 端口 ${PORT} 未被占用`);
|
||||
}
|
||||
@@ -1,5 +1,67 @@
|
||||
# @esengine/blueprint
|
||||
|
||||
## 4.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#447](https://github.com/esengine/esengine/pull/447) [`4e66bd8`](https://github.com/esengine/esengine/commit/4e66bd8e2be80b366a7723dcc48b99df0457aed4) Thanks [@esengine](https://github.com/esengine)! - feat(blueprint): add Schema type system and @BlueprintArray decorator
|
||||
- Add `Schema` fluent API for defining complex data types:
|
||||
- Primitive types: `Schema.float()`, `Schema.int()`, `Schema.string()`, `Schema.boolean()`, `Schema.vector2()`, `Schema.vector3()`
|
||||
- Composite types: `Schema.object()`, `Schema.array()`, `Schema.enum()`, `Schema.ref()`
|
||||
- Support for constraints: `min`, `max`, `step`, `defaultValue`, `placeholder`, etc.
|
||||
- Add `@BlueprintArray` decorator for array properties:
|
||||
- `itemSchema`: Define schema for array items using Schema API
|
||||
- `reorderable`: Allow drag-and-drop reordering
|
||||
- `exposeElementPorts`: Create individual ports for each array element
|
||||
- `portNameTemplate`: Custom naming for element ports (e.g., "Waypoint {index1}")
|
||||
- Update documentation with examples and usage guide
|
||||
|
||||
## 4.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#438](https://github.com/esengine/esengine/pull/438) [`0d33cf0`](https://github.com/esengine/esengine/commit/0d33cf00977d16e6282931aba2cf771ec2c84c6b) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): add visual group box for organizing nodes
|
||||
- Add NodeGroup model with dynamic bounds calculation based on node pin counts
|
||||
- Add GroupNodeComponent for rendering group boxes behind nodes
|
||||
- Groups automatically resize to wrap contained nodes
|
||||
- Dragging group header moves all nodes inside together
|
||||
- Support group serialization/deserialization
|
||||
- Export `estimateNodeHeight` and `NodeBounds` for accurate size calculation
|
||||
|
||||
feat(blueprint): add comprehensive math and logic nodes
|
||||
|
||||
Math nodes:
|
||||
- Modulo, Abs, Min, Max, Power, Sqrt
|
||||
- Floor, Ceil, Round, Sign, Negate
|
||||
- Sin, Cos, Tan, Asin, Acos, Atan, Atan2
|
||||
- DegToRad, RadToDeg, Lerp, InverseLerp
|
||||
- Clamp, Wrap, RandomRange, RandomInt
|
||||
|
||||
Logic nodes:
|
||||
- Equal, NotEqual, GreaterThan, GreaterThanOrEqual
|
||||
- LessThan, LessThanOrEqual, InRange
|
||||
- AND, OR, NOT, XOR, NAND
|
||||
- IsNull, Select (ternary)
|
||||
|
||||
## 4.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#435](https://github.com/esengine/esengine/pull/435) [`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e) Thanks [@esengine](https://github.com/esengine)! - feat(blueprint): 添加 Add Component 节点支持 + 变量节点 + ECS 模式重构
|
||||
|
||||
新功能:
|
||||
- 为每个 @BlueprintExpose 组件自动生成 Add_ComponentName 节点
|
||||
- Add 节点支持设置初始属性值
|
||||
- 添加通用 ECS_AddComponent 节点用于动态添加组件
|
||||
- @BlueprintExpose 装饰的组件自动注册,无需手动调用 registerComponentClass()
|
||||
- 添加变量节点:GetVariable, SetVariable, GetBoolVariable, GetFloatVariable, GetIntVariable, GetStringVariable
|
||||
|
||||
重构:
|
||||
- BlueprintComponent 使用 @ECSComponent 装饰器注册
|
||||
- BlueprintSystem 继承标准 System 基类
|
||||
- 简化组件 API,优化 VM 生命周期管理
|
||||
- ExecutionContext.getComponentClass() 自动查找 @BlueprintExpose 注册的组件
|
||||
|
||||
## 4.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/blueprint",
|
||||
"version": "4.2.0",
|
||||
"version": "4.5.0",
|
||||
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
* @zh 节点分类:
|
||||
* - events: 生命周期事件(BeginPlay, Tick, EndPlay)
|
||||
* - ecs: ECS 操作(Entity, Component, Flow)
|
||||
* - variables: 变量读写
|
||||
* - math: 数学运算
|
||||
* - logic: 比较和逻辑运算
|
||||
* - time: 时间工具
|
||||
* - debug: 调试工具
|
||||
*
|
||||
* @en Node categories:
|
||||
* - events: Lifecycle events (BeginPlay, Tick, EndPlay)
|
||||
* - ecs: ECS operations (Entity, Component, Flow)
|
||||
* - variables: Variable get/set
|
||||
* - math: Math operations
|
||||
* - logic: Comparison and logical operations
|
||||
* - time: Time utilities
|
||||
* - debug: Debug utilities
|
||||
*/
|
||||
@@ -23,9 +27,15 @@ export * from './events';
|
||||
// ECS operations | ECS 操作
|
||||
export * from './ecs';
|
||||
|
||||
// Variables | 变量
|
||||
export * from './variables';
|
||||
|
||||
// Math operations | 数学运算
|
||||
export * from './math';
|
||||
|
||||
// Logic operations | 逻辑运算
|
||||
export * from './logic';
|
||||
|
||||
// Time utilities | 时间工具
|
||||
export * from './time';
|
||||
|
||||
|
||||
435
packages/framework/blueprint/src/nodes/logic/ComparisonNodes.ts
Normal file
435
packages/framework/blueprint/src/nodes/logic/ComparisonNodes.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* @zh 比较运算节点
|
||||
* @en Comparison Operation Nodes
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
// ============================================================================
|
||||
// Equal Node (等于节点)
|
||||
// ============================================================================
|
||||
|
||||
export const EqualTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Equal',
|
||||
title: 'Equal',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if A equals B (如果 A 等于 B 则返回 true)',
|
||||
keywords: ['equal', '==', 'same', 'compare', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'any', displayName: 'A' },
|
||||
{ name: 'b', type: 'any', displayName: 'B' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(EqualTemplate)
|
||||
export class EqualExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = context.evaluateInput(node.id, 'a', null);
|
||||
const b = context.evaluateInput(node.id, 'b', null);
|
||||
return { outputs: { result: a === b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Not Equal Node (不等于节点)
|
||||
// ============================================================================
|
||||
|
||||
export const NotEqualTemplate: BlueprintNodeTemplate = {
|
||||
type: 'NotEqual',
|
||||
title: 'Not Equal',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if A does not equal B (如果 A 不等于 B 则返回 true)',
|
||||
keywords: ['not', 'equal', '!=', 'different', 'compare', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'any', displayName: 'A' },
|
||||
{ name: 'b', type: 'any', displayName: 'B' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(NotEqualTemplate)
|
||||
export class NotEqualExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = context.evaluateInput(node.id, 'a', null);
|
||||
const b = context.evaluateInput(node.id, 'b', null);
|
||||
return { outputs: { result: a !== b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Greater Than Node (大于节点)
|
||||
// ============================================================================
|
||||
|
||||
export const GreaterThanTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GreaterThan',
|
||||
title: 'Greater Than',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if A is greater than B (如果 A 大于 B 则返回 true)',
|
||||
keywords: ['greater', 'than', '>', 'compare', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GreaterThanTemplate)
|
||||
export class GreaterThanExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a > b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Greater Than Or Equal Node (大于等于节点)
|
||||
// ============================================================================
|
||||
|
||||
export const GreaterThanOrEqualTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GreaterThanOrEqual',
|
||||
title: 'Greater Or Equal',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if A is greater than or equal to B (如果 A 大于等于 B 则返回 true)',
|
||||
keywords: ['greater', 'equal', '>=', 'compare', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GreaterThanOrEqualTemplate)
|
||||
export class GreaterThanOrEqualExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a >= b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Less Than Node (小于节点)
|
||||
// ============================================================================
|
||||
|
||||
export const LessThanTemplate: BlueprintNodeTemplate = {
|
||||
type: 'LessThan',
|
||||
title: 'Less Than',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if A is less than B (如果 A 小于 B 则返回 true)',
|
||||
keywords: ['less', 'than', '<', 'compare', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(LessThanTemplate)
|
||||
export class LessThanExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a < b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Less Than Or Equal Node (小于等于节点)
|
||||
// ============================================================================
|
||||
|
||||
export const LessThanOrEqualTemplate: BlueprintNodeTemplate = {
|
||||
type: 'LessThanOrEqual',
|
||||
title: 'Less Or Equal',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if A is less than or equal to B (如果 A 小于等于 B 则返回 true)',
|
||||
keywords: ['less', 'equal', '<=', 'compare', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(LessThanOrEqualTemplate)
|
||||
export class LessThanOrEqualExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a <= b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// And Node (逻辑与节点)
|
||||
// ============================================================================
|
||||
|
||||
export const AndTemplate: BlueprintNodeTemplate = {
|
||||
type: 'And',
|
||||
title: 'AND',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if both A and B are true (如果 A 和 B 都为 true 则返回 true)',
|
||||
keywords: ['and', '&&', 'both', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'bool', displayName: 'A', defaultValue: false },
|
||||
{ name: 'b', type: 'bool', displayName: 'B', defaultValue: false }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AndTemplate)
|
||||
export class AndExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Boolean(context.evaluateInput(node.id, 'a', false));
|
||||
const b = Boolean(context.evaluateInput(node.id, 'b', false));
|
||||
return { outputs: { result: a && b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Or Node (逻辑或节点)
|
||||
// ============================================================================
|
||||
|
||||
export const OrTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Or',
|
||||
title: 'OR',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if either A or B is true (如果 A 或 B 为 true 则返回 true)',
|
||||
keywords: ['or', '||', 'either', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'bool', displayName: 'A', defaultValue: false },
|
||||
{ name: 'b', type: 'bool', displayName: 'B', defaultValue: false }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(OrTemplate)
|
||||
export class OrExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Boolean(context.evaluateInput(node.id, 'a', false));
|
||||
const b = Boolean(context.evaluateInput(node.id, 'b', false));
|
||||
return { outputs: { result: a || b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Not Node (逻辑非节点)
|
||||
// ============================================================================
|
||||
|
||||
export const NotTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Not',
|
||||
title: 'NOT',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns the opposite boolean value (返回相反的布尔值)',
|
||||
keywords: ['not', '!', 'negate', 'invert', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'bool', displayName: 'Value', defaultValue: false }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(NotTemplate)
|
||||
export class NotExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Boolean(context.evaluateInput(node.id, 'value', false));
|
||||
return { outputs: { result: !value } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// XOR Node (异或节点)
|
||||
// ============================================================================
|
||||
|
||||
export const XorTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Xor',
|
||||
title: 'XOR',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if exactly one of A or B is true (如果 A 和 B 中恰好有一个为 true 则返回 true)',
|
||||
keywords: ['xor', 'exclusive', 'or', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'bool', displayName: 'A', defaultValue: false },
|
||||
{ name: 'b', type: 'bool', displayName: 'B', defaultValue: false }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(XorTemplate)
|
||||
export class XorExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Boolean(context.evaluateInput(node.id, 'a', false));
|
||||
const b = Boolean(context.evaluateInput(node.id, 'b', false));
|
||||
return { outputs: { result: (a || b) && !(a && b) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NAND Node (与非节点)
|
||||
// ============================================================================
|
||||
|
||||
export const NandTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Nand',
|
||||
title: 'NAND',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if not both A and B are true (如果 A 和 B 不都为 true 则返回 true)',
|
||||
keywords: ['nand', 'not', 'and', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'bool', displayName: 'A', defaultValue: false },
|
||||
{ name: 'b', type: 'bool', displayName: 'B', defaultValue: false }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(NandTemplate)
|
||||
export class NandExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Boolean(context.evaluateInput(node.id, 'a', false));
|
||||
const b = Boolean(context.evaluateInput(node.id, 'b', false));
|
||||
return { outputs: { result: !(a && b) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// In Range Node (范围检查节点)
|
||||
// ============================================================================
|
||||
|
||||
export const InRangeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'InRange',
|
||||
title: 'In Range',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if value is between min and max (如果值在 min 和 max 之间则返回 true)',
|
||||
keywords: ['range', 'between', 'check', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 },
|
||||
{ name: 'min', type: 'float', displayName: 'Min', defaultValue: 0 },
|
||||
{ name: 'max', type: 'float', displayName: 'Max', defaultValue: 1 },
|
||||
{ name: 'inclusive', type: 'bool', displayName: 'Inclusive', defaultValue: true }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(InRangeTemplate)
|
||||
export class InRangeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
const min = Number(context.evaluateInput(node.id, 'min', 0));
|
||||
const max = Number(context.evaluateInput(node.id, 'max', 1));
|
||||
const inclusive = Boolean(context.evaluateInput(node.id, 'inclusive', true));
|
||||
|
||||
const result = inclusive
|
||||
? value >= min && value <= max
|
||||
: value > min && value < max;
|
||||
|
||||
return { outputs: { result } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Is Null Node (空值检查节点)
|
||||
// ============================================================================
|
||||
|
||||
export const IsNullTemplate: BlueprintNodeTemplate = {
|
||||
type: 'IsNull',
|
||||
title: 'Is Null',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns true if the value is null or undefined (如果值为 null 或 undefined 则返回 true)',
|
||||
keywords: ['null', 'undefined', 'empty', 'check', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'any', displayName: 'Value' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'bool', displayName: 'Is Null' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(IsNullTemplate)
|
||||
export class IsNullExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = context.evaluateInput(node.id, 'value', null);
|
||||
return { outputs: { result: value == null } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Select Node (选择节点)
|
||||
// ============================================================================
|
||||
|
||||
export const SelectTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Select',
|
||||
title: 'Select',
|
||||
category: 'logic',
|
||||
color: '#9C27B0',
|
||||
description: 'Returns A if condition is true, otherwise returns B (如果条件为 true 返回 A,否则返回 B)',
|
||||
keywords: ['select', 'choose', 'ternary', '?:', 'logic'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'condition', type: 'bool', displayName: 'Condition', defaultValue: false },
|
||||
{ name: 'a', type: 'any', displayName: 'A (True)' },
|
||||
{ name: 'b', type: 'any', displayName: 'B (False)' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'any', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SelectTemplate)
|
||||
export class SelectExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const condition = Boolean(context.evaluateInput(node.id, 'condition', false));
|
||||
const a = context.evaluateInput(node.id, 'a', null);
|
||||
const b = context.evaluateInput(node.id, 'b', null);
|
||||
return { outputs: { result: condition ? a : b } };
|
||||
}
|
||||
}
|
||||
6
packages/framework/blueprint/src/nodes/logic/index.ts
Normal file
6
packages/framework/blueprint/src/nodes/logic/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @zh 逻辑节点 - 比较和逻辑运算节点
|
||||
* @en Logic Nodes - Comparison and logical operation nodes
|
||||
*/
|
||||
|
||||
export * from './ComparisonNodes';
|
||||
@@ -120,3 +120,766 @@ export class DivideExecutor implements INodeExecutor {
|
||||
return { outputs: { result: a / b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Modulo Node (取模节点)
|
||||
// ============================================================================
|
||||
|
||||
export const ModuloTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Modulo',
|
||||
title: 'Modulo',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the remainder of A divided by B (返回 A 除以 B 的余数)',
|
||||
keywords: ['modulo', 'mod', 'remainder', '%', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(ModuloTemplate)
|
||||
export class ModuloExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
if (b === 0) return { outputs: { result: 0 } };
|
||||
return { outputs: { result: a % b } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Absolute Value Node (绝对值节点)
|
||||
// ============================================================================
|
||||
|
||||
export const AbsTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Abs',
|
||||
title: 'Absolute',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the absolute value (返回绝对值)',
|
||||
keywords: ['abs', 'absolute', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AbsTemplate)
|
||||
export class AbsExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.abs(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Min Node (最小值节点)
|
||||
// ============================================================================
|
||||
|
||||
export const MinTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Min',
|
||||
title: 'Min',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the smaller of two values (返回两个值中较小的一个)',
|
||||
keywords: ['min', 'minimum', 'smaller', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(MinTemplate)
|
||||
export class MinExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: Math.min(a, b) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Max Node (最大值节点)
|
||||
// ============================================================================
|
||||
|
||||
export const MaxTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Max',
|
||||
title: 'Max',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the larger of two values (返回两个值中较大的一个)',
|
||||
keywords: ['max', 'maximum', 'larger', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(MaxTemplate)
|
||||
export class MaxExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: Math.max(a, b) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Clamp Node (限制范围节点)
|
||||
// ============================================================================
|
||||
|
||||
export const ClampTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Clamp',
|
||||
title: 'Clamp',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Clamps a value between min and max (将值限制在最小和最大之间)',
|
||||
keywords: ['clamp', 'limit', 'range', 'bound', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 },
|
||||
{ name: 'min', type: 'float', displayName: 'Min', defaultValue: 0 },
|
||||
{ name: 'max', type: 'float', displayName: 'Max', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(ClampTemplate)
|
||||
export class ClampExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
const min = Number(context.evaluateInput(node.id, 'min', 0));
|
||||
const max = Number(context.evaluateInput(node.id, 'max', 1));
|
||||
return { outputs: { result: Math.max(min, Math.min(max, value)) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lerp Node (线性插值节点)
|
||||
// ============================================================================
|
||||
|
||||
export const LerpTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Lerp',
|
||||
title: 'Lerp',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Linear interpolation between A and B (A 和 B 之间的线性插值)',
|
||||
keywords: ['lerp', 'interpolate', 'blend', 'mix', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 },
|
||||
{ name: 't', type: 'float', displayName: 'Alpha', defaultValue: 0.5 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(LerpTemplate)
|
||||
export class LerpExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
const t = Number(context.evaluateInput(node.id, 't', 0.5));
|
||||
return { outputs: { result: a + (b - a) * t } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Random Range Node (随机范围节点)
|
||||
// ============================================================================
|
||||
|
||||
export const RandomRangeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'RandomRange',
|
||||
title: 'Random Range',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns a random number between min and max (返回 min 和 max 之间的随机数)',
|
||||
keywords: ['random', 'range', 'rand', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'min', type: 'float', displayName: 'Min', defaultValue: 0 },
|
||||
{ name: 'max', type: 'float', displayName: 'Max', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(RandomRangeTemplate)
|
||||
export class RandomRangeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const min = Number(context.evaluateInput(node.id, 'min', 0));
|
||||
const max = Number(context.evaluateInput(node.id, 'max', 1));
|
||||
return { outputs: { result: min + Math.random() * (max - min) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Random Integer Node (随机整数节点)
|
||||
// ============================================================================
|
||||
|
||||
export const RandomIntTemplate: BlueprintNodeTemplate = {
|
||||
type: 'RandomInt',
|
||||
title: 'Random Integer',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns a random integer between min and max inclusive (返回 min 和 max 之间的随机整数,包含边界)',
|
||||
keywords: ['random', 'int', 'integer', 'rand', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'min', type: 'int', displayName: 'Min', defaultValue: 0 },
|
||||
{ name: 'max', type: 'int', displayName: 'Max', defaultValue: 10 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'int', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(RandomIntTemplate)
|
||||
export class RandomIntExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const min = Math.floor(Number(context.evaluateInput(node.id, 'min', 0)));
|
||||
const max = Math.floor(Number(context.evaluateInput(node.id, 'max', 10)));
|
||||
return { outputs: { result: Math.floor(min + Math.random() * (max - min + 1)) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Power Node (幂运算节点)
|
||||
// ============================================================================
|
||||
|
||||
export const PowerTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Power',
|
||||
title: 'Power',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns base raised to the power of exponent (返回底数的指数次幂)',
|
||||
keywords: ['power', 'pow', 'exponent', '^', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'base', type: 'float', displayName: 'Base', defaultValue: 2 },
|
||||
{ name: 'exponent', type: 'float', displayName: 'Exponent', defaultValue: 2 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(PowerTemplate)
|
||||
export class PowerExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const base = Number(context.evaluateInput(node.id, 'base', 2));
|
||||
const exponent = Number(context.evaluateInput(node.id, 'exponent', 2));
|
||||
return { outputs: { result: Math.pow(base, exponent) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Square Root Node (平方根节点)
|
||||
// ============================================================================
|
||||
|
||||
export const SqrtTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Sqrt',
|
||||
title: 'Square Root',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the square root (返回平方根)',
|
||||
keywords: ['sqrt', 'square', 'root', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 4 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SqrtTemplate)
|
||||
export class SqrtExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 4));
|
||||
return { outputs: { result: Math.sqrt(Math.abs(value)) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Floor Node (向下取整节点)
|
||||
// ============================================================================
|
||||
|
||||
export const FloorTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Floor',
|
||||
title: 'Floor',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Rounds down to the nearest integer (向下取整)',
|
||||
keywords: ['floor', 'round', 'down', 'int', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'int', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(FloorTemplate)
|
||||
export class FloorExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.floor(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ceil Node (向上取整节点)
|
||||
// ============================================================================
|
||||
|
||||
export const CeilTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Ceil',
|
||||
title: 'Ceil',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Rounds up to the nearest integer (向上取整)',
|
||||
keywords: ['ceil', 'ceiling', 'round', 'up', 'int', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'int', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(CeilTemplate)
|
||||
export class CeilExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.ceil(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Round Node (四舍五入节点)
|
||||
// ============================================================================
|
||||
|
||||
export const RoundTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Round',
|
||||
title: 'Round',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Rounds to the nearest integer (四舍五入到最近的整数)',
|
||||
keywords: ['round', 'int', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'int', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(RoundTemplate)
|
||||
export class RoundExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.round(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Negate Node (取反节点)
|
||||
// ============================================================================
|
||||
|
||||
export const NegateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Negate',
|
||||
title: 'Negate',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the negative of a value (返回值的负数)',
|
||||
keywords: ['negate', 'negative', 'minus', '-', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(NegateTemplate)
|
||||
export class NegateExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: -value } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sign Node (符号节点)
|
||||
// ============================================================================
|
||||
|
||||
export const SignTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Sign',
|
||||
title: 'Sign',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns -1, 0, or 1 based on the sign of the value (根据值的符号返回 -1、0 或 1)',
|
||||
keywords: ['sign', 'positive', 'negative', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'int', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SignTemplate)
|
||||
export class SignExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.sign(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wrap Node (循环限制节点)
|
||||
// ============================================================================
|
||||
|
||||
export const WrapTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Wrap',
|
||||
title: 'Wrap',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Wraps value to stay within min and max range (将值循环限制在范围内)',
|
||||
keywords: ['wrap', 'loop', 'cycle', 'range', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 },
|
||||
{ name: 'min', type: 'float', displayName: 'Min', defaultValue: 0 },
|
||||
{ name: 'max', type: 'float', displayName: 'Max', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(WrapTemplate)
|
||||
export class WrapExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
const min = Number(context.evaluateInput(node.id, 'min', 0));
|
||||
const max = Number(context.evaluateInput(node.id, 'max', 1));
|
||||
const range = max - min;
|
||||
if (range <= 0) return { outputs: { result: min } };
|
||||
const wrapped = ((value - min) % range + range) % range + min;
|
||||
return { outputs: { result: wrapped } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sin Node (正弦节点)
|
||||
// ============================================================================
|
||||
|
||||
export const SinTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Sin',
|
||||
title: 'Sin',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the sine of angle in radians (返回弧度角的正弦值)',
|
||||
keywords: ['sin', 'sine', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'radians', type: 'float', displayName: 'Radians', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SinTemplate)
|
||||
export class SinExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const radians = Number(context.evaluateInput(node.id, 'radians', 0));
|
||||
return { outputs: { result: Math.sin(radians) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cos Node (余弦节点)
|
||||
// ============================================================================
|
||||
|
||||
export const CosTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Cos',
|
||||
title: 'Cos',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the cosine of angle in radians (返回弧度角的余弦值)',
|
||||
keywords: ['cos', 'cosine', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'radians', type: 'float', displayName: 'Radians', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(CosTemplate)
|
||||
export class CosExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const radians = Number(context.evaluateInput(node.id, 'radians', 0));
|
||||
return { outputs: { result: Math.cos(radians) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tan Node (正切节点)
|
||||
// ============================================================================
|
||||
|
||||
export const TanTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Tan',
|
||||
title: 'Tan',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the tangent of angle in radians (返回弧度角的正切值)',
|
||||
keywords: ['tan', 'tangent', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'radians', type: 'float', displayName: 'Radians', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(TanTemplate)
|
||||
export class TanExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const radians = Number(context.evaluateInput(node.id, 'radians', 0));
|
||||
return { outputs: { result: Math.tan(radians) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Asin Node (反正弦节点)
|
||||
// ============================================================================
|
||||
|
||||
export const AsinTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Asin',
|
||||
title: 'Asin',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the arc sine in radians (返回反正弦值,单位为弧度)',
|
||||
keywords: ['asin', 'arc', 'sine', 'inverse', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Radians' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AsinTemplate)
|
||||
export class AsinExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.asin(Math.max(-1, Math.min(1, value))) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Acos Node (反余弦节点)
|
||||
// ============================================================================
|
||||
|
||||
export const AcosTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Acos',
|
||||
title: 'Acos',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the arc cosine in radians (返回反余弦值,单位为弧度)',
|
||||
keywords: ['acos', 'arc', 'cosine', 'inverse', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Radians' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AcosTemplate)
|
||||
export class AcosExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.acos(Math.max(-1, Math.min(1, value))) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Atan Node (反正切节点)
|
||||
// ============================================================================
|
||||
|
||||
export const AtanTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Atan',
|
||||
title: 'Atan',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the arc tangent in radians (返回反正切值,单位为弧度)',
|
||||
keywords: ['atan', 'arc', 'tangent', 'inverse', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Radians' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AtanTemplate)
|
||||
export class AtanExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { result: Math.atan(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Atan2 Node (两参数反正切节点)
|
||||
// ============================================================================
|
||||
|
||||
export const Atan2Template: BlueprintNodeTemplate = {
|
||||
type: 'Atan2',
|
||||
title: 'Atan2',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the angle in radians between the positive X axis and the point (x, y) (返回点(x,y)与正X轴之间的弧度角)',
|
||||
keywords: ['atan2', 'angle', 'direction', 'trig', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'y', type: 'float', displayName: 'Y', defaultValue: 0 },
|
||||
{ name: 'x', type: 'float', displayName: 'X', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Radians' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(Atan2Template)
|
||||
export class Atan2Executor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const y = Number(context.evaluateInput(node.id, 'y', 0));
|
||||
const x = Number(context.evaluateInput(node.id, 'x', 1));
|
||||
return { outputs: { result: Math.atan2(y, x) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Degrees to Radians Node (角度转弧度节点)
|
||||
// ============================================================================
|
||||
|
||||
export const DegToRadTemplate: BlueprintNodeTemplate = {
|
||||
type: 'DegToRad',
|
||||
title: 'Degrees to Radians',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Converts degrees to radians (将角度转换为弧度)',
|
||||
keywords: ['degrees', 'radians', 'convert', 'angle', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'degrees', type: 'float', displayName: 'Degrees', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Radians' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(DegToRadTemplate)
|
||||
export class DegToRadExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const degrees = Number(context.evaluateInput(node.id, 'degrees', 0));
|
||||
return { outputs: { result: degrees * (Math.PI / 180) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Radians to Degrees Node (弧度转角度节点)
|
||||
// ============================================================================
|
||||
|
||||
export const RadToDegTemplate: BlueprintNodeTemplate = {
|
||||
type: 'RadToDeg',
|
||||
title: 'Radians to Degrees',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Converts radians to degrees (将弧度转换为角度)',
|
||||
keywords: ['radians', 'degrees', 'convert', 'angle', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'radians', type: 'float', displayName: 'Radians', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Degrees' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(RadToDegTemplate)
|
||||
export class RadToDegExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const radians = Number(context.evaluateInput(node.id, 'radians', 0));
|
||||
return { outputs: { result: radians * (180 / Math.PI) } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Inverse Lerp Node (反向线性插值节点)
|
||||
// ============================================================================
|
||||
|
||||
export const InverseLerpTemplate: BlueprintNodeTemplate = {
|
||||
type: 'InverseLerp',
|
||||
title: 'Inverse Lerp',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Returns the percentage of Value between A and B (返回值在 A 和 B 之间的百分比位置)',
|
||||
keywords: ['inverse', 'lerp', 'percentage', 'ratio', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 },
|
||||
{ name: 'value', type: 'float', displayName: 'Value', defaultValue: 0.5 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Alpha (0-1)' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(InverseLerpTemplate)
|
||||
export class InverseLerpExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
const value = Number(context.evaluateInput(node.id, 'value', 0.5));
|
||||
if (b === a) return { outputs: { result: 0 } };
|
||||
return { outputs: { result: (value - a) / (b - a) } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @zh 变量节点 - 读取和设置蓝图变量
|
||||
* @en Variable Nodes - Get and set blueprint variables
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
// ============================================================================
|
||||
// Get Variable | 获取变量
|
||||
// ============================================================================
|
||||
|
||||
export const GetVariableTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetVariable',
|
||||
title: 'Get Variable',
|
||||
category: 'variable',
|
||||
color: '#4a9c6d',
|
||||
isPure: true,
|
||||
description: 'Gets the value of a variable (获取变量的值)',
|
||||
keywords: ['variable', 'get', 'read', 'value'],
|
||||
menuPath: ['Variable', 'Get Variable'],
|
||||
inputs: [
|
||||
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', type: 'any', displayName: 'Value' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GetVariableTemplate)
|
||||
export class GetVariableExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
|
||||
|
||||
if (!variableName) {
|
||||
return { outputs: { value: null } };
|
||||
}
|
||||
|
||||
const value = context.getVariable(variableName);
|
||||
return { outputs: { value } };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Set Variable | 设置变量
|
||||
// ============================================================================
|
||||
|
||||
export const SetVariableTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SetVariable',
|
||||
title: 'Set Variable',
|
||||
category: 'variable',
|
||||
color: '#4a9c6d',
|
||||
description: 'Sets the value of a variable (设置变量的值)',
|
||||
keywords: ['variable', 'set', 'write', 'assign', 'value'],
|
||||
menuPath: ['Variable', 'Set Variable'],
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', displayName: '' },
|
||||
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' },
|
||||
{ name: 'value', type: 'any', displayName: 'Value' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', displayName: '' },
|
||||
{ name: 'value', type: 'any', displayName: 'Value' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SetVariableTemplate)
|
||||
export class SetVariableExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
|
||||
const value = context.evaluateInput(node.id, 'value', null);
|
||||
|
||||
if (!variableName) {
|
||||
return { outputs: { value: null }, nextExec: 'exec' };
|
||||
}
|
||||
|
||||
context.setVariable(variableName, value);
|
||||
return { outputs: { value }, nextExec: 'exec' };
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Get Variable By Name (typed variants) | 按名称获取变量(类型变体)
|
||||
// ============================================================================
|
||||
|
||||
export const GetBoolVariableTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetBoolVariable',
|
||||
title: 'Get Bool',
|
||||
category: 'variable',
|
||||
color: '#8b1e3f',
|
||||
isPure: true,
|
||||
description: 'Gets a boolean variable (获取布尔变量)',
|
||||
keywords: ['variable', 'get', 'bool', 'boolean'],
|
||||
menuPath: ['Variable', 'Get Bool'],
|
||||
inputs: [
|
||||
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', type: 'bool', displayName: 'Value' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GetBoolVariableTemplate)
|
||||
export class GetBoolVariableExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
|
||||
const value = context.getVariable(variableName);
|
||||
return { outputs: { value: Boolean(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
export const GetFloatVariableTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetFloatVariable',
|
||||
title: 'Get Float',
|
||||
category: 'variable',
|
||||
color: '#39c5bb',
|
||||
isPure: true,
|
||||
description: 'Gets a float variable (获取浮点变量)',
|
||||
keywords: ['variable', 'get', 'float', 'number'],
|
||||
menuPath: ['Variable', 'Get Float'],
|
||||
inputs: [
|
||||
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', type: 'float', displayName: 'Value' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GetFloatVariableTemplate)
|
||||
export class GetFloatVariableExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
|
||||
const value = context.getVariable(variableName);
|
||||
return { outputs: { value: Number(value) || 0 } };
|
||||
}
|
||||
}
|
||||
|
||||
export const GetIntVariableTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetIntVariable',
|
||||
title: 'Get Int',
|
||||
category: 'variable',
|
||||
color: '#1c8b8b',
|
||||
isPure: true,
|
||||
description: 'Gets an integer variable (获取整数变量)',
|
||||
keywords: ['variable', 'get', 'int', 'integer', 'number'],
|
||||
menuPath: ['Variable', 'Get Int'],
|
||||
inputs: [
|
||||
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', type: 'int', displayName: 'Value' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GetIntVariableTemplate)
|
||||
export class GetIntVariableExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
|
||||
const value = context.getVariable(variableName);
|
||||
return { outputs: { value: Math.floor(Number(value) || 0) } };
|
||||
}
|
||||
}
|
||||
|
||||
export const GetStringVariableTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetStringVariable',
|
||||
title: 'Get String',
|
||||
category: 'variable',
|
||||
color: '#e91e8c',
|
||||
isPure: true,
|
||||
description: 'Gets a string variable (获取字符串变量)',
|
||||
keywords: ['variable', 'get', 'string', 'text'],
|
||||
menuPath: ['Variable', 'Get String'],
|
||||
inputs: [
|
||||
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', type: 'string', displayName: 'Value' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(GetStringVariableTemplate)
|
||||
export class GetStringVariableExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
|
||||
const value = context.getVariable(variableName);
|
||||
return { outputs: { value: String(value ?? '') } };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @zh 变量节点导出
|
||||
* @en Variable nodes export
|
||||
*/
|
||||
|
||||
export * from './VariableNodes';
|
||||
@@ -1,118 +1,75 @@
|
||||
/**
|
||||
* @zh 蓝图装饰器 - 用于标记可在蓝图中使用的组件、属性和方法
|
||||
* @en Blueprint Decorators - Mark components, properties and methods for blueprint use
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
|
||||
*
|
||||
* @ECSComponent('Health')
|
||||
* @BlueprintExpose({ displayName: '生命值组件', category: 'gameplay' })
|
||||
* export class HealthComponent extends Component {
|
||||
*
|
||||
* @BlueprintProperty({ displayName: '当前生命值', type: 'float' })
|
||||
* current: number = 100;
|
||||
*
|
||||
* @BlueprintProperty({ displayName: '最大生命值', type: 'float', readonly: true })
|
||||
* max: number = 100;
|
||||
*
|
||||
* @BlueprintMethod({
|
||||
* displayName: '治疗',
|
||||
* params: [{ name: 'amount', type: 'float' }]
|
||||
* })
|
||||
* heal(amount: number): void {
|
||||
* this.current = Math.min(this.current + amount, this.max);
|
||||
* }
|
||||
*
|
||||
* @BlueprintMethod({
|
||||
* displayName: '受伤',
|
||||
* params: [{ name: 'amount', type: 'float' }],
|
||||
* returnType: 'bool'
|
||||
* })
|
||||
* takeDamage(amount: number): boolean {
|
||||
* this.current -= amount;
|
||||
* return this.current <= 0;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { BlueprintPinType } from '../types/pins';
|
||||
import type { PropertySchema, ArraySchema, ObjectSchema } from '../types/schema';
|
||||
|
||||
// ============================================================================
|
||||
// Types | 类型定义
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 参数定义
|
||||
* @en Parameter definition
|
||||
*/
|
||||
export interface BlueprintParamDef {
|
||||
/** @zh 参数名称 @en Parameter name */
|
||||
name: string;
|
||||
/** @zh 显示名称 @en Display name */
|
||||
displayName?: string;
|
||||
/** @zh 引脚类型 @en Pin type */
|
||||
type?: BlueprintPinType;
|
||||
/** @zh 默认值 @en Default value */
|
||||
defaultValue?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图暴露选项
|
||||
* @en Blueprint expose options
|
||||
*/
|
||||
export interface BlueprintExposeOptions {
|
||||
/** @zh 组件显示名称 @en Component display name */
|
||||
displayName?: string;
|
||||
/** @zh 组件描述 @en Component description */
|
||||
description?: string;
|
||||
/** @zh 组件分类 @en Component category */
|
||||
category?: string;
|
||||
/** @zh 组件颜色 @en Component color */
|
||||
color?: string;
|
||||
/** @zh 组件图标 @en Component icon */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图属性选项
|
||||
* @en Blueprint property options
|
||||
*/
|
||||
export interface BlueprintPropertyOptions {
|
||||
/** @zh 属性显示名称 @en Property display name */
|
||||
displayName?: string;
|
||||
/** @zh 属性描述 @en Property description */
|
||||
description?: string;
|
||||
/** @zh 引脚类型 @en Pin type */
|
||||
type?: BlueprintPinType;
|
||||
/** @zh 是否只读(不生成 Set 节点)@en Readonly (no Set node generated) */
|
||||
readonly?: boolean;
|
||||
/** @zh 默认值 @en Default value */
|
||||
defaultValue?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图方法选项
|
||||
* @en Blueprint method options
|
||||
*/
|
||||
export interface BlueprintMethodOptions {
|
||||
/** @zh 方法显示名称 @en Method display name */
|
||||
displayName?: string;
|
||||
/** @zh 方法描述 @en Method description */
|
||||
description?: string;
|
||||
/** @zh 是否是纯函数(无副作用)@en Is pure function (no side effects) */
|
||||
isPure?: boolean;
|
||||
/** @zh 参数列表 @en Parameter list */
|
||||
params?: BlueprintParamDef[];
|
||||
/** @zh 返回值类型 @en Return type */
|
||||
returnType?: BlueprintPinType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 属性元数据
|
||||
* @en Property metadata
|
||||
* @zh 蓝图数组属性选项
|
||||
* @en Blueprint array property options
|
||||
*/
|
||||
export interface BlueprintArrayOptions {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
itemSchema: PropertySchema;
|
||||
reorderable?: boolean;
|
||||
collapsible?: boolean;
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
defaultValue?: unknown[];
|
||||
itemLabel?: string;
|
||||
exposeElementPorts?: boolean;
|
||||
portNameTemplate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 蓝图对象属性选项
|
||||
* @en Blueprint object property options
|
||||
*/
|
||||
export interface BlueprintObjectOptions {
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
properties: Record<string, PropertySchema>;
|
||||
collapsible?: boolean;
|
||||
}
|
||||
|
||||
export interface PropertyMetadata {
|
||||
propertyKey: string;
|
||||
displayName: string;
|
||||
@@ -120,12 +77,12 @@ export interface PropertyMetadata {
|
||||
pinType: BlueprintPinType;
|
||||
readonly: boolean;
|
||||
defaultValue?: unknown;
|
||||
schema?: PropertySchema;
|
||||
isDynamicArray?: boolean;
|
||||
exposeElementPorts?: boolean;
|
||||
portNameTemplate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 方法元数据
|
||||
* @en Method metadata
|
||||
*/
|
||||
export interface MethodMetadata {
|
||||
methodKey: string;
|
||||
displayName: string;
|
||||
@@ -135,10 +92,6 @@ export interface MethodMetadata {
|
||||
returnType: BlueprintPinType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 组件蓝图元数据
|
||||
* @en Component blueprint metadata
|
||||
*/
|
||||
export interface ComponentBlueprintMetadata extends BlueprintExposeOptions {
|
||||
componentName: string;
|
||||
properties: PropertyMetadata[];
|
||||
@@ -146,41 +99,25 @@ export interface ComponentBlueprintMetadata extends BlueprintExposeOptions {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Registry | 注册表
|
||||
// Registry
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 已注册的蓝图组件
|
||||
* @en Registered blueprint components
|
||||
*/
|
||||
const registeredComponents = new Map<Function, ComponentBlueprintMetadata>();
|
||||
|
||||
/**
|
||||
* @zh 获取所有已注册的蓝图组件
|
||||
* @en Get all registered blueprint components
|
||||
*/
|
||||
export function getRegisteredBlueprintComponents(): Map<Function, ComponentBlueprintMetadata> {
|
||||
return registeredComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取组件的蓝图元数据
|
||||
* @en Get blueprint metadata for a component
|
||||
*/
|
||||
export function getBlueprintMetadata(componentClass: Function): ComponentBlueprintMetadata | undefined {
|
||||
return registeredComponents.get(componentClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有注册的蓝图组件(用于测试)
|
||||
* @en Clear all registered blueprint components (for testing)
|
||||
*/
|
||||
export function clearRegisteredComponents(): void {
|
||||
registeredComponents.clear();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal Helpers | 内部辅助函数
|
||||
// Internal Helpers
|
||||
// ============================================================================
|
||||
|
||||
function getOrCreateMetadata(constructor: Function): ComponentBlueprintMetadata {
|
||||
@@ -197,20 +134,9 @@ function getOrCreateMetadata(constructor: Function): ComponentBlueprintMetadata
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Decorators | 装饰器
|
||||
// Decorators
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 标记组件可在蓝图中使用
|
||||
* @en Mark component as usable in blueprint
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @ECSComponent('Player')
|
||||
* @BlueprintExpose({ displayName: '玩家', category: 'gameplay' })
|
||||
* export class PlayerComponent extends Component { }
|
||||
* ```
|
||||
*/
|
||||
export function BlueprintExpose(options: BlueprintExposeOptions = {}): ClassDecorator {
|
||||
return function (target: Function) {
|
||||
const metadata = getOrCreateMetadata(target);
|
||||
@@ -220,19 +146,6 @@ export function BlueprintExpose(options: BlueprintExposeOptions = {}): ClassDeco
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记属性可在蓝图中访问
|
||||
* @en Mark property as accessible in blueprint
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BlueprintProperty({ displayName: '生命值', type: 'float' })
|
||||
* health: number = 100;
|
||||
*
|
||||
* @BlueprintProperty({ displayName: '名称', type: 'string', readonly: true })
|
||||
* name: string = 'Player';
|
||||
* ```
|
||||
*/
|
||||
export function BlueprintProperty(options: BlueprintPropertyOptions = {}): PropertyDecorator {
|
||||
return function (target: Object, propertyKey: string | symbol) {
|
||||
const key = String(propertyKey);
|
||||
@@ -257,25 +170,108 @@ export function BlueprintProperty(options: BlueprintPropertyOptions = {}): Prope
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记方法可在蓝图中调用
|
||||
* @en Mark method as callable in blueprint
|
||||
* @zh 标记属性为蓝图数组(支持动态增删、排序)
|
||||
* @en Mark property as blueprint array (supports dynamic add/remove, reorder)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BlueprintMethod({
|
||||
* displayName: '攻击',
|
||||
* params: [
|
||||
* { name: 'target', type: 'entity' },
|
||||
* { name: 'damage', type: 'float' }
|
||||
* ],
|
||||
* returnType: 'bool'
|
||||
* @BlueprintArray({
|
||||
* displayName: '路径点',
|
||||
* itemSchema: Schema.object({
|
||||
* position: Schema.vector2(),
|
||||
* waitTime: Schema.float({ min: 0, defaultValue: 1.0 })
|
||||
* }),
|
||||
* reorderable: true,
|
||||
* exposeElementPorts: true,
|
||||
* portNameTemplate: 'Point {index1}'
|
||||
* })
|
||||
* attack(target: Entity, damage: number): boolean { }
|
||||
*
|
||||
* @BlueprintMethod({ displayName: '获取速度', isPure: true, returnType: 'float' })
|
||||
* getSpeed(): number { return this.speed; }
|
||||
* waypoints: Waypoint[] = [];
|
||||
* ```
|
||||
*/
|
||||
export function BlueprintArray(options: BlueprintArrayOptions): PropertyDecorator {
|
||||
return function (target: Object, propertyKey: string | symbol) {
|
||||
const key = String(propertyKey);
|
||||
const metadata = getOrCreateMetadata(target.constructor);
|
||||
|
||||
const arraySchema: ArraySchema = {
|
||||
type: 'array',
|
||||
items: options.itemSchema,
|
||||
defaultValue: options.defaultValue,
|
||||
minItems: options.minItems,
|
||||
maxItems: options.maxItems,
|
||||
reorderable: options.reorderable,
|
||||
collapsible: options.collapsible,
|
||||
itemLabel: options.itemLabel
|
||||
};
|
||||
|
||||
const propMeta: PropertyMetadata = {
|
||||
propertyKey: key,
|
||||
displayName: options.displayName ?? key,
|
||||
description: options.description,
|
||||
pinType: 'array',
|
||||
readonly: false,
|
||||
defaultValue: options.defaultValue,
|
||||
schema: arraySchema,
|
||||
isDynamicArray: true,
|
||||
exposeElementPorts: options.exposeElementPorts,
|
||||
portNameTemplate: options.portNameTemplate
|
||||
};
|
||||
|
||||
const existingIndex = metadata.properties.findIndex(p => p.propertyKey === key);
|
||||
if (existingIndex >= 0) {
|
||||
metadata.properties[existingIndex] = propMeta;
|
||||
} else {
|
||||
metadata.properties.push(propMeta);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记属性为蓝图对象(支持嵌套结构)
|
||||
* @en Mark property as blueprint object (supports nested structure)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @BlueprintObject({
|
||||
* displayName: '变换',
|
||||
* properties: {
|
||||
* position: Schema.vector2(),
|
||||
* rotation: Schema.float(),
|
||||
* scale: Schema.vector2({ defaultValue: { x: 1, y: 1 } })
|
||||
* }
|
||||
* })
|
||||
* transform: Transform;
|
||||
* ```
|
||||
*/
|
||||
export function BlueprintObject(options: BlueprintObjectOptions): PropertyDecorator {
|
||||
return function (target: Object, propertyKey: string | symbol) {
|
||||
const key = String(propertyKey);
|
||||
const metadata = getOrCreateMetadata(target.constructor);
|
||||
|
||||
const objectSchema: ObjectSchema = {
|
||||
type: 'object',
|
||||
properties: options.properties,
|
||||
collapsible: options.collapsible
|
||||
};
|
||||
|
||||
const propMeta: PropertyMetadata = {
|
||||
propertyKey: key,
|
||||
displayName: options.displayName ?? key,
|
||||
description: options.description,
|
||||
pinType: 'object',
|
||||
readonly: false,
|
||||
schema: objectSchema
|
||||
};
|
||||
|
||||
const existingIndex = metadata.properties.findIndex(p => p.propertyKey === key);
|
||||
if (existingIndex >= 0) {
|
||||
metadata.properties[existingIndex] = propMeta;
|
||||
} else {
|
||||
metadata.properties.push(propMeta);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function BlueprintMethod(options: BlueprintMethodOptions = {}): MethodDecorator {
|
||||
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
|
||||
const key = String(propertyKey);
|
||||
@@ -302,13 +298,9 @@ export function BlueprintMethod(options: BlueprintMethodOptions = {}): MethodDec
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions | 工具函数
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 从 TypeScript 类型名推断蓝图引脚类型
|
||||
* @en Infer blueprint pin type from TypeScript type name
|
||||
*/
|
||||
export function inferPinType(typeName: string): BlueprintPinType {
|
||||
const typeMap: Record<string, BlueprintPinType> = {
|
||||
'number': 'float',
|
||||
|
||||
@@ -1,43 +1,6 @@
|
||||
/**
|
||||
* @zh 蓝图注册系统
|
||||
* @en Blueprint Registry System
|
||||
*
|
||||
* @zh 提供组件自动节点生成功能,用户只需使用装饰器标记组件,
|
||||
* 即可自动在蓝图编辑器中生成对应的 Get/Set/Call 节点
|
||||
*
|
||||
* @en Provides automatic node generation for components. Users only need to
|
||||
* mark components with decorators, and corresponding Get/Set/Call nodes
|
||||
* will be auto-generated in the blueprint editor
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 1. 定义组件时使用装饰器 | Define component with decorators
|
||||
* @ECSComponent('Health')
|
||||
* @BlueprintExpose({ displayName: '生命值', category: 'gameplay' })
|
||||
* export class HealthComponent extends Component {
|
||||
* @BlueprintProperty({ displayName: '当前生命值', type: 'float' })
|
||||
* current: number = 100;
|
||||
*
|
||||
* @BlueprintMethod({
|
||||
* displayName: '治疗',
|
||||
* params: [{ name: 'amount', type: 'float' }]
|
||||
* })
|
||||
* heal(amount: number): void {
|
||||
* this.current = Math.min(this.current + amount, 100);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // 2. 初始化蓝图系统时注册 | Register when initializing blueprint system
|
||||
* import { registerAllComponentNodes } from '@esengine/blueprint';
|
||||
* registerAllComponentNodes();
|
||||
*
|
||||
* // 3. 现在蓝图编辑器中会出现以下节点:
|
||||
* // Now these nodes appear in blueprint editor:
|
||||
* // - Get Health(获取组件)
|
||||
* // - Get 当前生命值(获取属性)
|
||||
* // - Set 当前生命值(设置属性)
|
||||
* // - 治疗(调用方法)
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Decorators | 装饰器
|
||||
@@ -45,6 +8,8 @@ export {
|
||||
BlueprintExpose,
|
||||
BlueprintProperty,
|
||||
BlueprintMethod,
|
||||
BlueprintArray,
|
||||
BlueprintObject,
|
||||
getRegisteredBlueprintComponents,
|
||||
getBlueprintMetadata,
|
||||
clearRegisteredComponents,
|
||||
@@ -56,6 +21,8 @@ export type {
|
||||
BlueprintExposeOptions,
|
||||
BlueprintPropertyOptions,
|
||||
BlueprintMethodOptions,
|
||||
BlueprintArrayOptions,
|
||||
BlueprintObjectOptions,
|
||||
PropertyMetadata,
|
||||
MethodMetadata,
|
||||
ComponentBlueprintMetadata
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './pins';
|
||||
export * from './nodes';
|
||||
export * from './blueprint';
|
||||
export * from './schema';
|
||||
export * from './path-utils';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { BlueprintPinDefinition } from './pins';
|
||||
import { ObjectSchema } from './schema';
|
||||
|
||||
/**
|
||||
* Node category for visual styling and organization
|
||||
@@ -70,6 +71,23 @@ export interface BlueprintNodeTemplate {
|
||||
|
||||
/** Node color for visual distinction (节点颜色用于视觉区分) */
|
||||
color?: string;
|
||||
|
||||
// ========== Schema Support (Schema 支持) ==========
|
||||
|
||||
/**
|
||||
* @zh 节点数据 Schema - 定义节点存储的数据结构
|
||||
* @en Node data schema - defines the data structure stored in the node
|
||||
*
|
||||
* @zh 当定义了 schema 时,节点数据将按照 schema 结构存储和验证
|
||||
* @en When schema is defined, node data will be stored and validated according to the schema structure
|
||||
*/
|
||||
schema?: ObjectSchema;
|
||||
|
||||
/**
|
||||
* @zh 动态数组路径列表 - 指定哪些数组支持动态增删元素
|
||||
* @en Dynamic array paths - specifies which arrays support dynamic add/remove elements
|
||||
*/
|
||||
dynamicArrayPaths?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,6 +114,9 @@ export interface BlueprintNode {
|
||||
/**
|
||||
* Connection between two pins
|
||||
* 两个引脚之间的连接
|
||||
*
|
||||
* @zh 引脚路径支持数组索引,如 "waypoints[0].position"
|
||||
* @en Pin paths support array indices, e.g., "waypoints[0].position"
|
||||
*/
|
||||
export interface BlueprintConnection {
|
||||
/** Unique connection ID (唯一连接ID) */
|
||||
@@ -104,13 +125,19 @@ export interface BlueprintConnection {
|
||||
/** Source node ID (源节点ID) */
|
||||
fromNodeId: string;
|
||||
|
||||
/** Source pin name (源引脚名称) */
|
||||
/**
|
||||
* @zh 源引脚路径(支持数组索引如 "items[0].value")
|
||||
* @en Source pin path (supports array indices like "items[0].value")
|
||||
*/
|
||||
fromPin: string;
|
||||
|
||||
/** Target node ID (目标节点ID) */
|
||||
toNodeId: string;
|
||||
|
||||
/** Target pin name (目标引脚名称) */
|
||||
/**
|
||||
* @zh 目标引脚路径(支持数组索引如 "items[0].value")
|
||||
* @en Target pin path (supports array indices like "items[0].value")
|
||||
*/
|
||||
toPin: string;
|
||||
}
|
||||
|
||||
|
||||
549
packages/framework/blueprint/src/types/path-utils.ts
Normal file
549
packages/framework/blueprint/src/types/path-utils.ts
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* @zh 蓝图路径工具
|
||||
* @en Blueprint Path Utilities
|
||||
*
|
||||
* @zh 用于解析和操作数据路径,支持数组索引和嵌套属性访问
|
||||
* @en Used to parse and manipulate data paths, supports array indices and nested property access
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Path Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 路径部分类型
|
||||
* @en Path part type
|
||||
*/
|
||||
export type PathPartType = 'property' | 'index' | 'wildcard';
|
||||
|
||||
/**
|
||||
* @zh 路径部分
|
||||
* @en Path part
|
||||
*/
|
||||
export interface PathPart {
|
||||
type: PathPartType;
|
||||
/** Property name (for 'property' type) */
|
||||
name?: string;
|
||||
/** Array index (for 'index' type) */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 端口地址 - 解析后的路径结构
|
||||
* @en Port address - parsed path structure
|
||||
*/
|
||||
export interface PortAddress {
|
||||
/** Base property name (基础属性名) */
|
||||
baseName: string;
|
||||
/** Array indices [0, 2] represents arr[0][2] (数组索引路径) */
|
||||
indices: number[];
|
||||
/** Nested property path ['x', 'y'] (嵌套属性路径) */
|
||||
subPath: string[];
|
||||
/** Original path string (原始路径字符串) */
|
||||
original: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Path Parsing
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 解析路径字符串为部分数组
|
||||
* @en Parse path string to parts array
|
||||
*
|
||||
* @example
|
||||
* parsePath("waypoints[0].position.x")
|
||||
* // => [
|
||||
* // { type: 'property', name: 'waypoints' },
|
||||
* // { type: 'index', index: 0 },
|
||||
* // { type: 'property', name: 'position' },
|
||||
* // { type: 'property', name: 'x' }
|
||||
* // ]
|
||||
*/
|
||||
export function parsePath(path: string): PathPart[] {
|
||||
const parts: PathPart[] = [];
|
||||
const regex = /([^.\[\]]+)|\[(\*|\d+)\]/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(path)) !== null) {
|
||||
if (match[1]) {
|
||||
// Property name
|
||||
parts.push({ type: 'property', name: match[1] });
|
||||
} else if (match[2] === '*') {
|
||||
// Wildcard
|
||||
parts.push({ type: 'wildcard' });
|
||||
} else {
|
||||
// Array index
|
||||
parts.push({ type: 'index', index: parseInt(match[2], 10) });
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解析端口路径字符串为 PortAddress
|
||||
* @en Parse port path string to PortAddress
|
||||
*
|
||||
* @example
|
||||
* parsePortPath("waypoints[0].position.x")
|
||||
* // => { baseName: "waypoints", indices: [0], subPath: ["position", "x"], original: "..." }
|
||||
*/
|
||||
export function parsePortPath(path: string): PortAddress {
|
||||
const result: PortAddress = {
|
||||
baseName: '',
|
||||
indices: [],
|
||||
subPath: [],
|
||||
original: path
|
||||
};
|
||||
|
||||
const parts = parsePath(path);
|
||||
let foundFirstIndex = false;
|
||||
let afterIndices = false;
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.type === 'property') {
|
||||
if (!foundFirstIndex) {
|
||||
if (result.baseName) {
|
||||
// Multiple properties before index - treat as nested base
|
||||
result.baseName += '.' + part.name;
|
||||
} else {
|
||||
result.baseName = part.name!;
|
||||
}
|
||||
} else {
|
||||
afterIndices = true;
|
||||
result.subPath.push(part.name!);
|
||||
}
|
||||
} else if (part.type === 'index') {
|
||||
foundFirstIndex = true;
|
||||
if (!afterIndices) {
|
||||
result.indices.push(part.index!);
|
||||
} else {
|
||||
// Index after property - encode in subPath
|
||||
result.subPath.push(`[${part.index}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 构建路径字符串
|
||||
* @en Build path string from parts
|
||||
*/
|
||||
export function buildPath(parts: PathPart[]): string {
|
||||
let path = '';
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part.type) {
|
||||
case 'property':
|
||||
if (path && !path.endsWith('[')) {
|
||||
path += '.';
|
||||
}
|
||||
path += part.name;
|
||||
break;
|
||||
case 'index':
|
||||
path += `[${part.index}]`;
|
||||
break;
|
||||
case 'wildcard':
|
||||
path += '[*]';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从 PortAddress 构建路径字符串
|
||||
* @en Build path string from PortAddress
|
||||
*/
|
||||
export function buildPortPath(address: PortAddress): string {
|
||||
let path = address.baseName;
|
||||
|
||||
for (const index of address.indices) {
|
||||
path += `[${index}]`;
|
||||
}
|
||||
|
||||
if (address.subPath.length > 0) {
|
||||
for (const sub of address.subPath) {
|
||||
if (sub.startsWith('[')) {
|
||||
path += sub;
|
||||
} else {
|
||||
path += '.' + sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data Access
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 根据路径获取数据
|
||||
* @en Get data by path
|
||||
*
|
||||
* @example
|
||||
* const data = { waypoints: [{ position: { x: 10, y: 20 } }] };
|
||||
* getByPath(data, "waypoints[0].position.x") // => 10
|
||||
*/
|
||||
export function getByPath(data: unknown, path: string): unknown {
|
||||
if (!path) return data;
|
||||
|
||||
const parts = parsePath(path);
|
||||
let current: unknown = data;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (part.type) {
|
||||
case 'property':
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
current = (current as Record<string, unknown>)[part.name!];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
if (Array.isArray(current)) {
|
||||
current = current[part.index!];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'wildcard':
|
||||
// Wildcard returns array of all values
|
||||
if (Array.isArray(current)) {
|
||||
return current;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 根据路径设置数据
|
||||
* @en Set data by path
|
||||
*
|
||||
* @example
|
||||
* const data = { waypoints: [{ position: { x: 0, y: 0 } }] };
|
||||
* setByPath(data, "waypoints[0].position.x", 100);
|
||||
* // data.waypoints[0].position.x === 100
|
||||
*/
|
||||
export function setByPath(data: unknown, path: string, value: unknown): boolean {
|
||||
if (!path) return false;
|
||||
|
||||
const parts = parsePath(path);
|
||||
let current: unknown = data;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (part.type) {
|
||||
case 'property':
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
current = (current as Record<string, unknown>)[part.name!];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
if (Array.isArray(current)) {
|
||||
current = current[part.index!];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'wildcard':
|
||||
// Cannot set on wildcard
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the final value
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (lastPart.type) {
|
||||
case 'property':
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
(current as Record<string, unknown>)[lastPart.name!] = value;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
if (Array.isArray(current)) {
|
||||
current[lastPart.index!] = value;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查路径是否存在
|
||||
* @en Check if path exists
|
||||
*/
|
||||
export function hasPath(data: unknown, path: string): boolean {
|
||||
return getByPath(data, path) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 删除路径上的数据
|
||||
* @en Delete data at path
|
||||
*/
|
||||
export function deleteByPath(data: unknown, path: string): boolean {
|
||||
if (!path) return false;
|
||||
|
||||
const parts = parsePath(path);
|
||||
let current: unknown = data;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (part.type) {
|
||||
case 'property':
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
current = (current as Record<string, unknown>)[part.name!];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
if (Array.isArray(current)) {
|
||||
current = current[part.index!];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (lastPart.type) {
|
||||
case 'property':
|
||||
if (typeof current === 'object' && current !== null) {
|
||||
delete (current as Record<string, unknown>)[lastPart.name!];
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
if (Array.isArray(current)) {
|
||||
current.splice(lastPart.index!, 1);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Array Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 数组操作类型
|
||||
* @en Array operation type
|
||||
*/
|
||||
export type ArrayOperation = 'insert' | 'remove' | 'move';
|
||||
|
||||
/**
|
||||
* @zh 当数组元素变化时,更新路径中的索引
|
||||
* @en Update indices in path when array elements change
|
||||
*
|
||||
* @param path - Original path (原始路径)
|
||||
* @param arrayPath - Array base path (数组基础路径)
|
||||
* @param operation - Operation type (操作类型)
|
||||
* @param index - Target index (目标索引)
|
||||
* @param toIndex - Move destination (移动目标,仅 move 操作)
|
||||
* @returns Updated path or empty string if path becomes invalid (更新后的路径,如果路径失效则返回空字符串)
|
||||
*/
|
||||
export function updatePathOnArrayChange(
|
||||
path: string,
|
||||
arrayPath: string,
|
||||
operation: ArrayOperation,
|
||||
index: number,
|
||||
toIndex?: number
|
||||
): string {
|
||||
// Check if path starts with arrayPath[
|
||||
if (!path.startsWith(arrayPath + '[')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const address = parsePortPath(path);
|
||||
|
||||
if (address.indices.length === 0) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const currentIndex = address.indices[0];
|
||||
|
||||
switch (operation) {
|
||||
case 'insert':
|
||||
if (currentIndex >= index) {
|
||||
address.indices[0]++;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
if (currentIndex === index) {
|
||||
return ''; // Path becomes invalid
|
||||
}
|
||||
if (currentIndex > index) {
|
||||
address.indices[0]--;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'move':
|
||||
if (toIndex !== undefined) {
|
||||
if (currentIndex === index) {
|
||||
address.indices[0] = toIndex;
|
||||
} else if (index < toIndex) {
|
||||
// Moving down
|
||||
if (currentIndex > index && currentIndex <= toIndex) {
|
||||
address.indices[0]--;
|
||||
}
|
||||
} else {
|
||||
// Moving up
|
||||
if (currentIndex >= toIndex && currentIndex < index) {
|
||||
address.indices[0]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return buildPortPath(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 展开带通配符的路径
|
||||
* @en Expand path with wildcards
|
||||
*
|
||||
* @example
|
||||
* const data = { items: [{ x: 1 }, { x: 2 }, { x: 3 }] };
|
||||
* expandWildcardPath("items[*].x", data)
|
||||
* // => ["items[0].x", "items[1].x", "items[2].x"]
|
||||
*/
|
||||
export function expandWildcardPath(path: string, data: unknown): string[] {
|
||||
const parts = parsePath(path);
|
||||
const results: string[] = [];
|
||||
|
||||
function expand(currentParts: PathPart[], currentData: unknown, index: number): void {
|
||||
if (index >= parts.length) {
|
||||
results.push(buildPath(currentParts));
|
||||
return;
|
||||
}
|
||||
|
||||
const part = parts[index];
|
||||
|
||||
if (part.type === 'wildcard') {
|
||||
if (Array.isArray(currentData)) {
|
||||
for (let i = 0; i < currentData.length; i++) {
|
||||
const newParts = [...currentParts, { type: 'index' as const, index: i }];
|
||||
expand(newParts, currentData[i], index + 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const newParts = [...currentParts, part];
|
||||
let nextData: unknown;
|
||||
|
||||
if (part.type === 'property' && typeof currentData === 'object' && currentData !== null) {
|
||||
nextData = (currentData as Record<string, unknown>)[part.name!];
|
||||
} else if (part.type === 'index' && Array.isArray(currentData)) {
|
||||
nextData = currentData[part.index!];
|
||||
}
|
||||
|
||||
expand(newParts, nextData, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
expand([], data, 0);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查路径是否包含通配符
|
||||
* @en Check if path contains wildcards
|
||||
*/
|
||||
export function hasWildcard(path: string): boolean {
|
||||
return path.includes('[*]');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取路径的父路径
|
||||
* @en Get parent path
|
||||
*
|
||||
* @example
|
||||
* getParentPath("items[0].position.x") // => "items[0].position"
|
||||
* getParentPath("items[0]") // => "items"
|
||||
* getParentPath("items") // => ""
|
||||
*/
|
||||
export function getParentPath(path: string): string {
|
||||
const parts = parsePath(path);
|
||||
if (parts.length <= 1) {
|
||||
return '';
|
||||
}
|
||||
return buildPath(parts.slice(0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取路径的最后一部分名称
|
||||
* @en Get the last part name of path
|
||||
*
|
||||
* @example
|
||||
* getPathLastName("items[0].position.x") // => "x"
|
||||
* getPathLastName("items[0]") // => "[0]"
|
||||
*/
|
||||
export function getPathLastName(path: string): string {
|
||||
const parts = parsePath(path);
|
||||
if (parts.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const last = parts[parts.length - 1];
|
||||
if (last.type === 'property') {
|
||||
return last.name!;
|
||||
} else if (last.type === 'index') {
|
||||
return `[${last.index}]`;
|
||||
} else {
|
||||
return '[*]';
|
||||
}
|
||||
}
|
||||
611
packages/framework/blueprint/src/types/schema.ts
Normal file
611
packages/framework/blueprint/src/types/schema.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* @zh 蓝图属性 Schema 系统
|
||||
* @en Blueprint Property Schema System
|
||||
*
|
||||
* @zh 提供递归类型定义,支持原始类型、数组、对象、枚举等复杂数据结构
|
||||
* @en Provides recursive type definitions supporting primitives, arrays, objects, enums, etc.
|
||||
*/
|
||||
|
||||
import { BlueprintPinType } from './pins';
|
||||
|
||||
// ============================================================================
|
||||
// Schema Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 属性 Schema - 递归定义数据结构
|
||||
* @en Property Schema - recursive data structure definition
|
||||
*/
|
||||
export type PropertySchema =
|
||||
| PrimitiveSchema
|
||||
| ArraySchema
|
||||
| ObjectSchema
|
||||
| EnumSchema
|
||||
| RefSchema;
|
||||
|
||||
/**
|
||||
* @zh 原始类型 Schema
|
||||
* @en Primitive type schema
|
||||
*/
|
||||
export interface PrimitiveSchema {
|
||||
type: 'primitive';
|
||||
primitive: BlueprintPinType;
|
||||
defaultValue?: unknown;
|
||||
|
||||
// Constraints | 约束
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
multiline?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 数组类型 Schema
|
||||
* @en Array type schema
|
||||
*/
|
||||
export interface ArraySchema {
|
||||
type: 'array';
|
||||
items: PropertySchema;
|
||||
defaultValue?: unknown[];
|
||||
|
||||
// Constraints | 约束
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
|
||||
// UI Behavior | UI 行为
|
||||
reorderable?: boolean;
|
||||
collapsible?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
itemLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 对象类型 Schema
|
||||
* @en Object type schema
|
||||
*/
|
||||
export interface ObjectSchema {
|
||||
type: 'object';
|
||||
properties: Record<string, PropertySchema>;
|
||||
required?: string[];
|
||||
|
||||
// UI Behavior | UI 行为
|
||||
collapsible?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 枚举类型 Schema
|
||||
* @en Enum type schema
|
||||
*/
|
||||
export interface EnumSchema {
|
||||
type: 'enum';
|
||||
options: EnumOption[];
|
||||
defaultValue?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 枚举选项
|
||||
* @en Enum option
|
||||
*/
|
||||
export interface EnumOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 引用类型 Schema
|
||||
* @en Reference type schema
|
||||
*
|
||||
* @zh 引用 SchemaRegistry 中已注册的 Schema
|
||||
* @en References a schema registered in SchemaRegistry
|
||||
*/
|
||||
export interface RefSchema {
|
||||
type: 'ref';
|
||||
ref: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Schema Registry
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh Schema 注册表
|
||||
* @en Schema Registry
|
||||
*
|
||||
* @zh 用于注册和复用常用的 Schema 定义
|
||||
* @en Used to register and reuse common Schema definitions
|
||||
*/
|
||||
export class SchemaRegistry {
|
||||
private static schemas = new Map<string, PropertySchema>();
|
||||
|
||||
/**
|
||||
* @zh 注册 Schema
|
||||
* @en Register a schema
|
||||
*/
|
||||
static register(id: string, schema: PropertySchema): void {
|
||||
this.schemas.set(id, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 Schema
|
||||
* @en Get a schema
|
||||
*/
|
||||
static get(id: string): PropertySchema | undefined {
|
||||
return this.schemas.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解析引用,返回实际 Schema
|
||||
* @en Resolve reference, return actual schema
|
||||
*/
|
||||
static resolve(schema: PropertySchema): PropertySchema {
|
||||
if (schema.type === 'ref') {
|
||||
const resolved = this.schemas.get(schema.ref);
|
||||
if (!resolved) {
|
||||
console.warn(`[SchemaRegistry] Schema not found: ${schema.ref}`);
|
||||
return { type: 'primitive', primitive: 'any' };
|
||||
}
|
||||
return this.resolve(resolved);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查 Schema 是否已注册
|
||||
* @en Check if schema is registered
|
||||
*/
|
||||
static has(id: string): boolean {
|
||||
return this.schemas.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有已注册的 Schema ID
|
||||
* @en Get all registered schema IDs
|
||||
*/
|
||||
static keys(): string[] {
|
||||
return Array.from(this.schemas.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清空注册表
|
||||
* @en Clear registry
|
||||
*/
|
||||
static clear(): void {
|
||||
this.schemas.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Schema Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取 Schema 的默认值
|
||||
* @en Get default value for a schema
|
||||
*/
|
||||
export function getSchemaDefaultValue(schema: PropertySchema): unknown {
|
||||
const resolved = SchemaRegistry.resolve(schema);
|
||||
|
||||
switch (resolved.type) {
|
||||
case 'primitive':
|
||||
if (resolved.defaultValue !== undefined) return resolved.defaultValue;
|
||||
return getPrimitiveDefaultValue(resolved.primitive);
|
||||
|
||||
case 'array':
|
||||
if (resolved.defaultValue !== undefined) return [...resolved.defaultValue];
|
||||
return [];
|
||||
|
||||
case 'object': {
|
||||
const obj: Record<string, unknown> = {};
|
||||
for (const [key, propSchema] of Object.entries(resolved.properties)) {
|
||||
obj[key] = getSchemaDefaultValue(propSchema);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
case 'enum':
|
||||
if (resolved.defaultValue !== undefined) return resolved.defaultValue;
|
||||
return resolved.options[0]?.value;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取原始类型的默认值
|
||||
* @en Get default value for primitive type
|
||||
*/
|
||||
export function getPrimitiveDefaultValue(primitive: BlueprintPinType): unknown {
|
||||
switch (primitive) {
|
||||
case 'bool': return false;
|
||||
case 'int': return 0;
|
||||
case 'float': return 0.0;
|
||||
case 'string': return '';
|
||||
case 'vector2': return { x: 0, y: 0 };
|
||||
case 'vector3': return { x: 0, y: 0, z: 0 };
|
||||
case 'color': return { r: 255, g: 255, b: 255, a: 255 };
|
||||
case 'entity': return null;
|
||||
case 'component': return null;
|
||||
case 'object': return null;
|
||||
case 'array': return [];
|
||||
case 'any': return null;
|
||||
case 'exec': return undefined;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 根据 Schema 获取对应的 PinType
|
||||
* @en Get corresponding PinType from Schema
|
||||
*/
|
||||
export function schemaToPinType(schema: PropertySchema): BlueprintPinType {
|
||||
const resolved = SchemaRegistry.resolve(schema);
|
||||
|
||||
switch (resolved.type) {
|
||||
case 'primitive':
|
||||
return resolved.primitive;
|
||||
case 'array':
|
||||
return 'array';
|
||||
case 'object':
|
||||
return 'object';
|
||||
case 'enum':
|
||||
return typeof resolved.options[0]?.value === 'number' ? 'int' : 'string';
|
||||
default:
|
||||
return 'any';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 验证数据是否符合 Schema
|
||||
* @en Validate data against schema
|
||||
*/
|
||||
export function validateSchema(
|
||||
schema: PropertySchema,
|
||||
data: unknown,
|
||||
path: string = ''
|
||||
): ValidationResult {
|
||||
const resolved = SchemaRegistry.resolve(schema);
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
switch (resolved.type) {
|
||||
case 'primitive':
|
||||
validatePrimitive(resolved, data, path, errors);
|
||||
break;
|
||||
|
||||
case 'array':
|
||||
validateArray(resolved, data, path, errors);
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
validateObject(resolved, data, path, errors);
|
||||
break;
|
||||
|
||||
case 'enum':
|
||||
validateEnum(resolved, data, path, errors);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 验证结果
|
||||
* @en Validation result
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: ValidationError[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 验证错误
|
||||
* @en Validation error
|
||||
*/
|
||||
export interface ValidationError {
|
||||
path: string;
|
||||
message: string;
|
||||
expected?: string;
|
||||
received?: string;
|
||||
}
|
||||
|
||||
function validatePrimitive(
|
||||
schema: PrimitiveSchema,
|
||||
data: unknown,
|
||||
path: string,
|
||||
errors: ValidationError[]
|
||||
): void {
|
||||
if (data === null || data === undefined) {
|
||||
return; // Allow null/undefined for optional fields
|
||||
}
|
||||
|
||||
const expectedType = getPrimitiveJsType(schema.primitive);
|
||||
const actualType = typeof data;
|
||||
|
||||
if (expectedType === 'object') {
|
||||
if (typeof data !== 'object') {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Expected ${schema.primitive}, got ${actualType}`,
|
||||
expected: schema.primitive,
|
||||
received: actualType
|
||||
});
|
||||
}
|
||||
} else if (expectedType !== 'any' && actualType !== expectedType) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Expected ${expectedType}, got ${actualType}`,
|
||||
expected: expectedType,
|
||||
received: actualType
|
||||
});
|
||||
}
|
||||
|
||||
// Numeric constraints
|
||||
if ((schema.primitive === 'int' || schema.primitive === 'float') && typeof data === 'number') {
|
||||
if (schema.min !== undefined && data < schema.min) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Value ${data} is less than minimum ${schema.min}`
|
||||
});
|
||||
}
|
||||
if (schema.max !== undefined && data > schema.max) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Value ${data} is greater than maximum ${schema.max}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateArray(
|
||||
schema: ArraySchema,
|
||||
data: unknown,
|
||||
path: string,
|
||||
errors: ValidationError[]
|
||||
): void {
|
||||
if (!Array.isArray(data)) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Expected array, got ${typeof data}`,
|
||||
expected: 'array',
|
||||
received: typeof data
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (schema.minItems !== undefined && data.length < schema.minItems) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Array has ${data.length} items, minimum is ${schema.minItems}`
|
||||
});
|
||||
}
|
||||
|
||||
if (schema.maxItems !== undefined && data.length > schema.maxItems) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Array has ${data.length} items, maximum is ${schema.maxItems}`
|
||||
});
|
||||
}
|
||||
|
||||
// Validate each item
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const itemResult = validateSchema(schema.items, data[i], `${path}[${i}]`);
|
||||
errors.push(...itemResult.errors);
|
||||
}
|
||||
}
|
||||
|
||||
function validateObject(
|
||||
schema: ObjectSchema,
|
||||
data: unknown,
|
||||
path: string,
|
||||
errors: ValidationError[]
|
||||
): void {
|
||||
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Expected object, got ${Array.isArray(data) ? 'array' : typeof data}`,
|
||||
expected: 'object',
|
||||
received: Array.isArray(data) ? 'array' : typeof data
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = data as Record<string, unknown>;
|
||||
|
||||
// Check required fields
|
||||
if (schema.required) {
|
||||
for (const key of schema.required) {
|
||||
if (!(key in obj)) {
|
||||
errors.push({
|
||||
path: path ? `${path}.${key}` : key,
|
||||
message: `Missing required field: ${key}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each property
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
if (key in obj) {
|
||||
const propPath = path ? `${path}.${key}` : key;
|
||||
const propResult = validateSchema(propSchema, obj[key], propPath);
|
||||
errors.push(...propResult.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnum(
|
||||
schema: EnumSchema,
|
||||
data: unknown,
|
||||
path: string,
|
||||
errors: ValidationError[]
|
||||
): void {
|
||||
if (data === null || data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validValues = schema.options.map(o => o.value);
|
||||
if (!validValues.includes(data as string | number)) {
|
||||
errors.push({
|
||||
path,
|
||||
message: `Invalid enum value: ${data}`,
|
||||
expected: validValues.join(' | '),
|
||||
received: String(data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimitiveJsType(primitive: BlueprintPinType): string {
|
||||
switch (primitive) {
|
||||
case 'bool': return 'boolean';
|
||||
case 'int':
|
||||
case 'float': return 'number';
|
||||
case 'string': return 'string';
|
||||
case 'vector2':
|
||||
case 'vector3':
|
||||
case 'color':
|
||||
case 'entity':
|
||||
case 'component':
|
||||
case 'object':
|
||||
case 'array': return 'object';
|
||||
case 'any': return 'any';
|
||||
case 'exec': return 'undefined';
|
||||
default: return 'any';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 深度克隆 Schema
|
||||
* @en Deep clone schema
|
||||
*/
|
||||
export function cloneSchema(schema: PropertySchema): PropertySchema {
|
||||
return JSON.parse(JSON.stringify(schema));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 合并两个 ObjectSchema
|
||||
* @en Merge two ObjectSchemas
|
||||
*/
|
||||
export function mergeObjectSchemas(
|
||||
base: ObjectSchema,
|
||||
override: Partial<ObjectSchema>
|
||||
): ObjectSchema {
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
properties: {
|
||||
...base.properties,
|
||||
...(override.properties || {})
|
||||
},
|
||||
required: [
|
||||
...(base.required || []),
|
||||
...(override.required || [])
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Schema Builder (Fluent API)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh Schema 构建器
|
||||
* @en Schema Builder
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const waypointSchema = Schema.object({
|
||||
* position: Schema.vector2(),
|
||||
* waitTime: Schema.float({ min: 0, defaultValue: 1.0 }),
|
||||
* action: Schema.enum([
|
||||
* { value: 'idle', label: 'Idle' },
|
||||
* { value: 'patrol', label: 'Patrol' }
|
||||
* ])
|
||||
* });
|
||||
*
|
||||
* const pathSchema = Schema.array(waypointSchema, {
|
||||
* minItems: 2,
|
||||
* reorderable: true,
|
||||
* itemLabel: 'Point {index1}'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const Schema = {
|
||||
// Primitives
|
||||
bool(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'bool', ...options };
|
||||
},
|
||||
|
||||
int(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'int', ...options };
|
||||
},
|
||||
|
||||
float(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'float', ...options };
|
||||
},
|
||||
|
||||
string(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'string', ...options };
|
||||
},
|
||||
|
||||
vector2(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'vector2', ...options };
|
||||
},
|
||||
|
||||
vector3(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'vector3', ...options };
|
||||
},
|
||||
|
||||
color(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'color', ...options };
|
||||
},
|
||||
|
||||
entity(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'entity', ...options };
|
||||
},
|
||||
|
||||
component(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'component', ...options };
|
||||
},
|
||||
|
||||
object_ref(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'object', ...options };
|
||||
},
|
||||
|
||||
any(options?: Partial<Omit<PrimitiveSchema, 'type' | 'primitive'>>): PrimitiveSchema {
|
||||
return { type: 'primitive', primitive: 'any', ...options };
|
||||
},
|
||||
|
||||
// Complex types
|
||||
array(
|
||||
items: PropertySchema,
|
||||
options?: Partial<Omit<ArraySchema, 'type' | 'items'>>
|
||||
): ArraySchema {
|
||||
return { type: 'array', items, ...options };
|
||||
},
|
||||
|
||||
object(
|
||||
properties: Record<string, PropertySchema>,
|
||||
options?: Partial<Omit<ObjectSchema, 'type' | 'properties'>>
|
||||
): ObjectSchema {
|
||||
return { type: 'object', properties, ...options };
|
||||
},
|
||||
|
||||
enum(
|
||||
options: EnumOption[],
|
||||
extra?: Partial<Omit<EnumSchema, 'type' | 'options'>>
|
||||
): EnumSchema {
|
||||
return { type: 'enum', options, ...extra };
|
||||
},
|
||||
|
||||
ref(id: string): RefSchema {
|
||||
return { type: 'ref', ref: id };
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,26 @@
|
||||
# @esengine/fsm
|
||||
|
||||
## 9.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e66bd8`](https://github.com/esengine/esengine/commit/4e66bd8e2be80b366a7723dcc48b99df0457aed4)]:
|
||||
- @esengine/blueprint@4.5.0
|
||||
|
||||
## 8.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`0d33cf0`](https://github.com/esengine/esengine/commit/0d33cf00977d16e6282931aba2cf771ec2c84c6b)]:
|
||||
- @esengine/blueprint@4.4.0
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
|
||||
- @esengine/blueprint@4.3.0
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/fsm",
|
||||
"version": "6.0.0",
|
||||
"version": "9.0.0",
|
||||
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
42
packages/framework/math/CHANGELOG.md
Normal file
42
packages/framework/math/CHANGELOG.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# @esengine/ecs-framework-math
|
||||
|
||||
## 2.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e66bd8`](https://github.com/esengine/esengine/commit/4e66bd8e2be80b366a7723dcc48b99df0457aed4)]:
|
||||
- @esengine/blueprint@4.5.0
|
||||
|
||||
## 2.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#444](https://github.com/esengine/esengine/pull/444) [`fa593a3`](https://github.com/esengine/esengine/commit/fa593a3c69292207800750f8106f418465cb7c0f) Thanks [@esengine](https://github.com/esengine)! - feat(math): add blueprint nodes for math library
|
||||
- Add Vector2 blueprint nodes (Make, Break, Add, Sub, Mul, Length, Normalize, Dot, Cross, Distance, Lerp, Rotate, FromAngle)
|
||||
- Add Fixed32 blueprint nodes (FromFloat, FromInt, ToFloat, ToInt, arithmetic operations, Abs, Sqrt, Floor, Ceil, Round, Sign, Min, Max, Clamp, Lerp)
|
||||
- Add FixedVector2 blueprint nodes (Make, Break, Add, Sub, Mul, Negate, Length, Normalize, Dot, Cross, Distance, Lerp)
|
||||
- Add Color blueprint nodes (Make, Break, FromHex, ToHex, FromHSL, ToHSL, Lerp, Lighten, Darken, Saturate, Desaturate, Invert, Grayscale, Luminance, constants)
|
||||
- Add documentation for math blueprint nodes (Chinese and English)
|
||||
|
||||
## 2.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#442](https://github.com/esengine/esengine/pull/442) [`bffe90b`](https://github.com/esengine/esengine/commit/bffe90b6a17563cc90709faf339b229dc3abd22d) Thanks [@esengine](https://github.com/esengine)! - feat(math): add blueprint nodes for math library
|
||||
- Add Vector2 blueprint nodes (Make, Break, Add, Sub, Mul, Length, Normalize, Dot, Cross, Distance, Lerp, Rotate, FromAngle)
|
||||
- Add Fixed32 blueprint nodes (FromFloat, FromInt, ToFloat, ToInt, arithmetic operations, Abs, Sqrt, Floor, Ceil, Round, Sign, Min, Max, Clamp, Lerp)
|
||||
- Add FixedVector2 blueprint nodes (Make, Break, Add, Sub, Mul, Negate, Length, Normalize, Dot, Cross, Distance, Lerp)
|
||||
- Add Color blueprint nodes (Make, Break, FromHex, ToHex, FromHSL, ToHSL, Lerp, Lighten, Darken, Saturate, Desaturate, Invert, Grayscale, Luminance, constants)
|
||||
- Add documentation for math blueprint nodes (Chinese and English)
|
||||
|
||||
## 2.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#440](https://github.com/esengine/esengine/pull/440) [`30173f0`](https://github.com/esengine/esengine/commit/30173f076415c9770a429b236b8bab95a2fdc498) Thanks [@esengine](https://github.com/esengine)! - feat(math): 添加定点数数学库 | Add fixed-point math library
|
||||
|
||||
**@esengine/ecs-framework-math** - 新增定点数支持 | Add fixed-point number support
|
||||
- 新增 `Fixed32` 类:Q16.16 定点数实现 | Add `Fixed32` class: Q16.16 fixed-point implementation
|
||||
- 新增 `FixedMath` 工具类:定点数数学运算 | Add `FixedMath` utility: fixed-point math operations
|
||||
- 新增 `FixedVector2` 类:定点数二维向量 | Add `FixedVector2` class: fixed-point 2D vector
|
||||
- 支持基本算术运算、三角函数、向量运算 | Support basic arithmetic, trigonometry, vector operations
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework-math",
|
||||
"version": "1.0.5",
|
||||
"version": "2.10.1",
|
||||
"description": "ECS框架2D数学库 - 提供向量、矩阵、几何形状和碰撞检测功能",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
@@ -40,6 +40,9 @@
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/blueprint": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
|
||||
450
packages/framework/math/src/Fixed32.ts
Normal file
450
packages/framework/math/src/Fixed32.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* @zh Q16.16 定点数,用于确定性计算(帧同步)
|
||||
* @en Q16.16 fixed-point number for deterministic calculations (lockstep)
|
||||
*
|
||||
* @zh 使用 16 位整数部分 + 16 位小数部分,范围 ±32767.99998
|
||||
* @en Uses 16-bit integer + 16-bit fraction, range ±32767.99998
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const a = Fixed32.from(3.14);
|
||||
* const b = Fixed32.from(2);
|
||||
* const c = a.mul(b); // 6.28
|
||||
* console.log(c.toNumber());
|
||||
* ```
|
||||
*/
|
||||
export class Fixed32 {
|
||||
/**
|
||||
* @zh 内部原始值(32位整数)
|
||||
* @en Internal raw value (32-bit integer)
|
||||
*/
|
||||
readonly raw: number;
|
||||
|
||||
/**
|
||||
* @zh 小数位数
|
||||
* @en Fraction bits
|
||||
*/
|
||||
static readonly FRACTION_BITS = 16;
|
||||
|
||||
/**
|
||||
* @zh 缩放因子 (2^16 = 65536)
|
||||
* @en Scale factor (2^16 = 65536)
|
||||
*/
|
||||
static readonly SCALE = 65536;
|
||||
|
||||
/**
|
||||
* @zh 最大值 (约 32767.99998)
|
||||
* @en Maximum value (approximately 32767.99998)
|
||||
*/
|
||||
static readonly MAX_VALUE = 0x7FFFFFFF;
|
||||
|
||||
/**
|
||||
* @zh 最小值 (约 -32768)
|
||||
* @en Minimum value (approximately -32768)
|
||||
*/
|
||||
static readonly MIN_VALUE = -0x80000000;
|
||||
|
||||
/**
|
||||
* @zh 精度 (1/65536 ≈ 0.0000153)
|
||||
* @en Precision (1/65536 ≈ 0.0000153)
|
||||
*/
|
||||
static readonly EPSILON = 1;
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
/** @zh 零 @en Zero */
|
||||
static readonly ZERO = new Fixed32(0);
|
||||
|
||||
/** @zh 一 @en One */
|
||||
static readonly ONE = new Fixed32(Fixed32.SCALE);
|
||||
|
||||
/** @zh 负一 @en Negative one */
|
||||
static readonly NEG_ONE = new Fixed32(-Fixed32.SCALE);
|
||||
|
||||
/** @zh 二分之一 @en One half */
|
||||
static readonly HALF = new Fixed32(Fixed32.SCALE >> 1);
|
||||
|
||||
/** @zh 圆周率 π @en Pi */
|
||||
static readonly PI = new Fixed32(205887); // π * 65536
|
||||
|
||||
/** @zh 2π @en Two Pi */
|
||||
static readonly TWO_PI = new Fixed32(411775); // 2π * 65536
|
||||
|
||||
/** @zh π/2 @en Pi divided by 2 */
|
||||
static readonly HALF_PI = new Fixed32(102944); // π/2 * 65536
|
||||
|
||||
/** @zh 弧度转角度系数 (180/π) @en Radians to degrees factor */
|
||||
static readonly RAD_TO_DEG = new Fixed32(3754936); // (180/π) * 65536
|
||||
|
||||
/** @zh 角度转弧度系数 (π/180) @en Degrees to radians factor */
|
||||
static readonly DEG_TO_RAD = new Fixed32(1144); // (π/180) * 65536
|
||||
|
||||
// ==================== 构造 ====================
|
||||
|
||||
/**
|
||||
* @zh 私有构造函数,使用静态方法创建实例
|
||||
* @en Private constructor, use static methods to create instances
|
||||
*/
|
||||
private constructor(raw: number) {
|
||||
// 确保是 32 位有符号整数
|
||||
this.raw = raw | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从浮点数创建定点数
|
||||
* @en Create fixed-point from floating-point number
|
||||
* @param n - @zh 浮点数值 @en Floating-point value
|
||||
*/
|
||||
static from(n: number): Fixed32 {
|
||||
return new Fixed32(Math.round(n * Fixed32.SCALE));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从原始整数值创建定点数
|
||||
* @en Create fixed-point from raw integer value
|
||||
* @param raw - @zh 原始值 @en Raw value
|
||||
*/
|
||||
static fromRaw(raw: number): Fixed32 {
|
||||
return new Fixed32(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从整数创建定点数(无精度损失)
|
||||
* @en Create fixed-point from integer (no precision loss)
|
||||
* @param n - @zh 整数值 @en Integer value
|
||||
*/
|
||||
static fromInt(n: number): Fixed32 {
|
||||
return new Fixed32((n | 0) << Fixed32.FRACTION_BITS);
|
||||
}
|
||||
|
||||
// ==================== 转换 ====================
|
||||
|
||||
/**
|
||||
* @zh 转换为浮点数
|
||||
* @en Convert to floating-point number
|
||||
*/
|
||||
toNumber(): number {
|
||||
return this.raw / Fixed32.SCALE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取原始整数值
|
||||
* @en Get raw integer value
|
||||
*/
|
||||
toRaw(): number {
|
||||
return this.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为整数(向下取整)
|
||||
* @en Convert to integer (floor)
|
||||
*/
|
||||
toInt(): number {
|
||||
return this.raw >> Fixed32.FRACTION_BITS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为字符串
|
||||
* @en Convert to string
|
||||
*/
|
||||
toString(): string {
|
||||
return `Fixed32(${this.toNumber().toFixed(5)})`;
|
||||
}
|
||||
|
||||
// ==================== 基础运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 加法
|
||||
* @en Addition
|
||||
*/
|
||||
add(other: Fixed32): Fixed32 {
|
||||
return new Fixed32(this.raw + other.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 减法
|
||||
* @en Subtraction
|
||||
*/
|
||||
sub(other: Fixed32): Fixed32 {
|
||||
return new Fixed32(this.raw - other.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 乘法(使用 64 位中间结果防止溢出)
|
||||
* @en Multiplication (uses 64-bit intermediate to prevent overflow)
|
||||
*/
|
||||
mul(other: Fixed32): Fixed32 {
|
||||
// 拆分为高低 16 位进行乘法,避免溢出
|
||||
const a = this.raw;
|
||||
const b = other.raw;
|
||||
|
||||
// 使用 BigInt 确保精度(JS 数字在大数时会丢失精度)
|
||||
// 或者使用拆分法
|
||||
const aLow = a & 0xFFFF;
|
||||
const aHigh = a >> 16;
|
||||
const bLow = b & 0xFFFF;
|
||||
const bHigh = b >> 16;
|
||||
|
||||
// (aHigh * 2^16 + aLow) * (bHigh * 2^16 + bLow) / 2^16
|
||||
// = aHigh * bHigh * 2^16 + aHigh * bLow + aLow * bHigh + aLow * bLow / 2^16
|
||||
const lowLow = (aLow * bLow) >>> 16;
|
||||
const lowHigh = aLow * bHigh;
|
||||
const highLow = aHigh * bLow;
|
||||
const highHigh = aHigh * bHigh;
|
||||
|
||||
const result = highHigh * Fixed32.SCALE + lowHigh + highLow + lowLow;
|
||||
return new Fixed32(result | 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 除法
|
||||
* @en Division
|
||||
* @throws @zh 除数为零时抛出错误 @en Throws when dividing by zero
|
||||
*/
|
||||
div(other: Fixed32): Fixed32 {
|
||||
if (other.raw === 0) {
|
||||
throw new Error('Fixed32: Division by zero');
|
||||
}
|
||||
// 先左移再除,保持精度
|
||||
const result = ((this.raw * Fixed32.SCALE) / other.raw) | 0;
|
||||
return new Fixed32(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取模运算
|
||||
* @en Modulo operation
|
||||
*/
|
||||
mod(other: Fixed32): Fixed32 {
|
||||
return new Fixed32(this.raw % other.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取反
|
||||
* @en Negation
|
||||
*/
|
||||
neg(): Fixed32 {
|
||||
return new Fixed32(-this.raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 绝对值
|
||||
* @en Absolute value
|
||||
*/
|
||||
abs(): Fixed32 {
|
||||
return this.raw >= 0 ? this : new Fixed32(-this.raw);
|
||||
}
|
||||
|
||||
// ==================== 比较运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 等于
|
||||
* @en Equal to
|
||||
*/
|
||||
eq(other: Fixed32): boolean {
|
||||
return this.raw === other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 不等于
|
||||
* @en Not equal to
|
||||
*/
|
||||
ne(other: Fixed32): boolean {
|
||||
return this.raw !== other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 小于
|
||||
* @en Less than
|
||||
*/
|
||||
lt(other: Fixed32): boolean {
|
||||
return this.raw < other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 小于等于
|
||||
* @en Less than or equal to
|
||||
*/
|
||||
le(other: Fixed32): boolean {
|
||||
return this.raw <= other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 大于
|
||||
* @en Greater than
|
||||
*/
|
||||
gt(other: Fixed32): boolean {
|
||||
return this.raw > other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 大于等于
|
||||
* @en Greater than or equal to
|
||||
*/
|
||||
ge(other: Fixed32): boolean {
|
||||
return this.raw >= other.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为零
|
||||
* @en Check if zero
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.raw === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为正数
|
||||
* @en Check if positive
|
||||
*/
|
||||
isPositive(): boolean {
|
||||
return this.raw > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为负数
|
||||
* @en Check if negative
|
||||
*/
|
||||
isNegative(): boolean {
|
||||
return this.raw < 0;
|
||||
}
|
||||
|
||||
// ==================== 数学函数 ====================
|
||||
|
||||
/**
|
||||
* @zh 平方根(牛顿迭代法,确定性)
|
||||
* @en Square root (Newton's method, deterministic)
|
||||
*/
|
||||
static sqrt(x: Fixed32): Fixed32 {
|
||||
if (x.raw <= 0) return Fixed32.ZERO;
|
||||
|
||||
// 牛顿迭代法
|
||||
let guess = x.raw;
|
||||
let prev = 0;
|
||||
|
||||
// 固定迭代次数确保确定性
|
||||
for (let i = 0; i < 16; i++) {
|
||||
prev = guess;
|
||||
guess = ((guess + ((x.raw * Fixed32.SCALE) / guess) | 0) >> 1) | 0;
|
||||
if (guess === prev) break;
|
||||
}
|
||||
|
||||
return new Fixed32(guess);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向下取整
|
||||
* @en Floor
|
||||
*/
|
||||
static floor(x: Fixed32): Fixed32 {
|
||||
return new Fixed32(x.raw & ~(Fixed32.SCALE - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向上取整
|
||||
* @en Ceiling
|
||||
*/
|
||||
static ceil(x: Fixed32): Fixed32 {
|
||||
const frac = x.raw & (Fixed32.SCALE - 1);
|
||||
if (frac === 0) return x;
|
||||
return new Fixed32((x.raw & ~(Fixed32.SCALE - 1)) + Fixed32.SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 四舍五入
|
||||
* @en Round
|
||||
*/
|
||||
static round(x: Fixed32): Fixed32 {
|
||||
return new Fixed32((x.raw + (Fixed32.SCALE >> 1)) & ~(Fixed32.SCALE - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 最小值
|
||||
* @en Minimum
|
||||
*/
|
||||
static min(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.raw < b.raw ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 最大值
|
||||
* @en Maximum
|
||||
*/
|
||||
static max(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.raw > b.raw ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 限制范围
|
||||
* @en Clamp to range
|
||||
*/
|
||||
static clamp(x: Fixed32, min: Fixed32, max: Fixed32): Fixed32 {
|
||||
if (x.raw < min.raw) return min;
|
||||
if (x.raw > max.raw) return max;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 线性插值
|
||||
* @en Linear interpolation
|
||||
* @param a - @zh 起始值 @en Start value
|
||||
* @param b - @zh 结束值 @en End value
|
||||
* @param t - @zh 插值参数 (0-1) @en Interpolation parameter (0-1)
|
||||
*/
|
||||
static lerp(a: Fixed32, b: Fixed32, t: Fixed32): Fixed32 {
|
||||
// a + (b - a) * t
|
||||
return a.add(b.sub(a).mul(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 符号函数
|
||||
* @en Sign function
|
||||
* @returns @zh -1, 0, 或 1 @en -1, 0, or 1
|
||||
*/
|
||||
static sign(x: Fixed32): Fixed32 {
|
||||
if (x.raw > 0) return Fixed32.ONE;
|
||||
if (x.raw < 0) return Fixed32.NEG_ONE;
|
||||
return Fixed32.ZERO;
|
||||
}
|
||||
|
||||
// ==================== 静态运算(便捷方法) ====================
|
||||
|
||||
/**
|
||||
* @zh 加法(静态)
|
||||
* @en Addition (static)
|
||||
*/
|
||||
static add(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.add(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 减法(静态)
|
||||
* @en Subtraction (static)
|
||||
*/
|
||||
static sub(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.sub(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 乘法(静态)
|
||||
* @en Multiplication (static)
|
||||
*/
|
||||
static mul(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.mul(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 除法(静态)
|
||||
* @en Division (static)
|
||||
*/
|
||||
static div(a: Fixed32, b: Fixed32): Fixed32 {
|
||||
return a.div(b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Fixed32 数据接口,用于序列化
|
||||
* @en Fixed32 data interface for serialization
|
||||
*/
|
||||
export interface IFixed32 {
|
||||
raw: number;
|
||||
}
|
||||
298
packages/framework/math/src/FixedMath.ts
Normal file
298
packages/framework/math/src/FixedMath.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { Fixed32 } from './Fixed32';
|
||||
|
||||
/**
|
||||
* @zh 定点数数学函数库,使用查表法确保确定性
|
||||
* @en Fixed-point math functions using lookup tables for determinism
|
||||
*
|
||||
* @zh 所有三角函数使用预计算的查找表,确保在所有平台上结果一致
|
||||
* @en All trigonometric functions use precomputed lookup tables to ensure consistent results across all platforms
|
||||
*/
|
||||
export class FixedMath {
|
||||
/**
|
||||
* @zh 正弦表大小(每 90 度的采样点数)
|
||||
* @en Sine table size (samples per 90 degrees)
|
||||
*/
|
||||
private static readonly SIN_TABLE_SIZE = 1024;
|
||||
|
||||
/**
|
||||
* @zh 正弦查找表(0 到 90 度)
|
||||
* @en Sine lookup table (0 to 90 degrees)
|
||||
*/
|
||||
private static readonly SIN_TABLE: Int32Array = FixedMath.generateSinTable();
|
||||
|
||||
/**
|
||||
* @zh 生成正弦查找表
|
||||
* @en Generate sine lookup table
|
||||
*/
|
||||
private static generateSinTable(): Int32Array {
|
||||
const table = new Int32Array(FixedMath.SIN_TABLE_SIZE + 1);
|
||||
for (let i = 0; i <= FixedMath.SIN_TABLE_SIZE; i++) {
|
||||
const angle = (i * Math.PI) / (2 * FixedMath.SIN_TABLE_SIZE);
|
||||
table[i] = Math.round(Math.sin(angle) * Fixed32.SCALE);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 正弦函数(确定性)
|
||||
* @en Sine function (deterministic)
|
||||
* @param angle - @zh 角度(弧度,定点数) @en Angle in radians (fixed-point)
|
||||
*/
|
||||
static sin(angle: Fixed32): Fixed32 {
|
||||
// 将角度规范化到 [0, 2π)
|
||||
let raw = angle.raw % Fixed32.TWO_PI.raw;
|
||||
if (raw < 0) raw += Fixed32.TWO_PI.raw;
|
||||
|
||||
const halfPi = Fixed32.HALF_PI.raw;
|
||||
const pi = Fixed32.PI.raw;
|
||||
const threeHalfPi = halfPi * 3;
|
||||
|
||||
let tableAngle: number;
|
||||
let negative = false;
|
||||
|
||||
if (raw <= halfPi) {
|
||||
// 第一象限: [0, π/2]
|
||||
tableAngle = raw;
|
||||
} else if (raw <= pi) {
|
||||
// 第二象限: (π/2, π]
|
||||
tableAngle = pi - raw;
|
||||
} else if (raw <= threeHalfPi) {
|
||||
// 第三象限: (π, 3π/2]
|
||||
tableAngle = raw - pi;
|
||||
negative = true;
|
||||
} else {
|
||||
// 第四象限: (3π/2, 2π)
|
||||
tableAngle = Fixed32.TWO_PI.raw - raw;
|
||||
negative = true;
|
||||
}
|
||||
|
||||
// 计算表索引 (tableAngle 范围是 [0, π/2])
|
||||
const tableIndex = Math.min(
|
||||
((tableAngle * FixedMath.SIN_TABLE_SIZE) / halfPi) | 0,
|
||||
FixedMath.SIN_TABLE_SIZE
|
||||
);
|
||||
|
||||
const result = FixedMath.SIN_TABLE[tableIndex];
|
||||
return Fixed32.fromRaw(negative ? -result : result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 余弦函数(确定性)
|
||||
* @en Cosine function (deterministic)
|
||||
* @param angle - @zh 角度(弧度,定点数) @en Angle in radians (fixed-point)
|
||||
*/
|
||||
static cos(angle: Fixed32): Fixed32 {
|
||||
// cos(x) = sin(x + π/2)
|
||||
return FixedMath.sin(angle.add(Fixed32.HALF_PI));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 正切函数(确定性)
|
||||
* @en Tangent function (deterministic)
|
||||
* @param angle - @zh 角度(弧度,定点数) @en Angle in radians (fixed-point)
|
||||
*/
|
||||
static tan(angle: Fixed32): Fixed32 {
|
||||
const cosVal = FixedMath.cos(angle);
|
||||
if (cosVal.isZero()) {
|
||||
// 返回最大值表示无穷大
|
||||
return Fixed32.fromRaw(Fixed32.MAX_VALUE);
|
||||
}
|
||||
return FixedMath.sin(angle).div(cosVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反正切函数 atan2(确定性)
|
||||
* @en Arc tangent of y/x (deterministic)
|
||||
* @param y - @zh Y 坐标 @en Y coordinate
|
||||
* @param x - @zh X 坐标 @en X coordinate
|
||||
* @returns @zh 角度(弧度,范围 -π 到 π)@en Angle in radians (range -π to π)
|
||||
*/
|
||||
static atan2(y: Fixed32, x: Fixed32): Fixed32 {
|
||||
const yRaw = y.raw;
|
||||
const xRaw = x.raw;
|
||||
|
||||
if (xRaw === 0 && yRaw === 0) {
|
||||
return Fixed32.ZERO;
|
||||
}
|
||||
|
||||
// 使用 CORDIC 算法的简化版本
|
||||
const absY = Math.abs(yRaw);
|
||||
const absX = Math.abs(xRaw);
|
||||
|
||||
let angle: number;
|
||||
|
||||
if (absX >= absY) {
|
||||
// |y/x| <= 1,使用泰勒展开近似
|
||||
angle = FixedMath.atanApprox(absY, absX);
|
||||
} else {
|
||||
// |y/x| > 1,使用恒等式 atan(y/x) = π/2 - atan(x/y)
|
||||
angle = Fixed32.HALF_PI.raw - FixedMath.atanApprox(absX, absY);
|
||||
}
|
||||
|
||||
// 根据象限调整
|
||||
if (xRaw < 0) {
|
||||
angle = Fixed32.PI.raw - angle;
|
||||
}
|
||||
if (yRaw < 0) {
|
||||
angle = -angle;
|
||||
}
|
||||
|
||||
return Fixed32.fromRaw(angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh atan 近似计算(内部使用)
|
||||
* @en Approximate atan calculation (internal use)
|
||||
*/
|
||||
private static atanApprox(num: number, den: number): number {
|
||||
if (den === 0) return Fixed32.HALF_PI.raw;
|
||||
|
||||
// 使用多项式近似: atan(x) ≈ x - x³/3 + x⁵/5
|
||||
// 对于 |x| <= 1 精度足够
|
||||
const ratio = ((num * Fixed32.SCALE) / den) | 0;
|
||||
|
||||
// 简化的多项式: atan(x) ≈ x * (1 - x²/3)
|
||||
// 更精确的版本: atan(x) ≈ x / (1 + 0.28125 * x²)
|
||||
const x2 = ((ratio * ratio) / Fixed32.SCALE) | 0;
|
||||
const factor = Fixed32.SCALE + ((x2 * 18432) / Fixed32.SCALE | 0); // 0.28125 * 65536 ≈ 18432
|
||||
const result = ((ratio * Fixed32.SCALE) / factor) | 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反正弦函数(确定性)
|
||||
* @en Arc sine function (deterministic)
|
||||
* @param x - @zh 值(范围 -1 到 1)@en Value (range -1 to 1)
|
||||
*/
|
||||
static asin(x: Fixed32): Fixed32 {
|
||||
// asin(x) = atan2(x, sqrt(1 - x²))
|
||||
const one = Fixed32.ONE;
|
||||
const x2 = x.mul(x);
|
||||
const sqrt = Fixed32.sqrt(one.sub(x2));
|
||||
return FixedMath.atan2(x, sqrt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反余弦函数(确定性)
|
||||
* @en Arc cosine function (deterministic)
|
||||
* @param x - @zh 值(范围 -1 到 1)@en Value (range -1 to 1)
|
||||
*/
|
||||
static acos(x: Fixed32): Fixed32 {
|
||||
// acos(x) = π/2 - asin(x)
|
||||
return Fixed32.HALF_PI.sub(FixedMath.asin(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度规范化到 [-π, π]
|
||||
* @en Normalize angle to [-π, π]
|
||||
*/
|
||||
static normalizeAngle(angle: Fixed32): Fixed32 {
|
||||
let raw = angle.raw % Fixed32.TWO_PI.raw;
|
||||
|
||||
if (raw > Fixed32.PI.raw) {
|
||||
raw -= Fixed32.TWO_PI.raw;
|
||||
} else if (raw < -Fixed32.PI.raw) {
|
||||
raw += Fixed32.TWO_PI.raw;
|
||||
}
|
||||
|
||||
return Fixed32.fromRaw(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度差值(最短路径)
|
||||
* @en Angle difference (shortest path)
|
||||
*/
|
||||
static angleDelta(from: Fixed32, to: Fixed32): Fixed32 {
|
||||
return FixedMath.normalizeAngle(to.sub(from));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度线性插值(最短路径)
|
||||
* @en Angle linear interpolation (shortest path)
|
||||
*/
|
||||
static lerpAngle(from: Fixed32, to: Fixed32, t: Fixed32): Fixed32 {
|
||||
const delta = FixedMath.angleDelta(from, to);
|
||||
return from.add(delta.mul(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 弧度转角度
|
||||
* @en Radians to degrees
|
||||
*/
|
||||
static radToDeg(rad: Fixed32): Fixed32 {
|
||||
return rad.mul(Fixed32.RAD_TO_DEG);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 角度转弧度
|
||||
* @en Degrees to radians
|
||||
*/
|
||||
static degToRad(deg: Fixed32): Fixed32 {
|
||||
return deg.mul(Fixed32.DEG_TO_RAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 幂函数(整数次幂)
|
||||
* @en Power function (integer exponent)
|
||||
*/
|
||||
static pow(base: Fixed32, exp: number): Fixed32 {
|
||||
if (exp === 0) return Fixed32.ONE;
|
||||
if (exp < 0) {
|
||||
base = Fixed32.ONE.div(base);
|
||||
exp = -exp;
|
||||
}
|
||||
|
||||
let result = Fixed32.ONE;
|
||||
while (exp > 0) {
|
||||
if (exp & 1) {
|
||||
result = result.mul(base);
|
||||
}
|
||||
base = base.mul(base);
|
||||
exp >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 指数函数近似(e^x)
|
||||
* @en Exponential function approximation (e^x)
|
||||
*/
|
||||
static exp(x: Fixed32): Fixed32 {
|
||||
// 使用泰勒展开: e^x ≈ 1 + x + x²/2 + x³/6 + x⁴/24
|
||||
const one = Fixed32.ONE;
|
||||
const x2 = x.mul(x);
|
||||
const x3 = x2.mul(x);
|
||||
const x4 = x3.mul(x);
|
||||
|
||||
return one
|
||||
.add(x)
|
||||
.add(x2.div(Fixed32.from(2)))
|
||||
.add(x3.div(Fixed32.from(6)))
|
||||
.add(x4.div(Fixed32.from(24)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 自然对数近似
|
||||
* @en Natural logarithm approximation
|
||||
*/
|
||||
static ln(x: Fixed32): Fixed32 {
|
||||
if (x.raw <= 0) {
|
||||
throw new Error('FixedMath.ln: argument must be positive');
|
||||
}
|
||||
|
||||
// 使用牛顿迭代法: y_{n+1} = y_n + 2 * (x - exp(y_n)) / (x + exp(y_n))
|
||||
let y = Fixed32.ZERO;
|
||||
const two = Fixed32.from(2);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const expY = FixedMath.exp(y);
|
||||
const diff = x.sub(expY);
|
||||
const sum = x.add(expY);
|
||||
y = y.add(two.mul(diff).div(sum));
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
}
|
||||
504
packages/framework/math/src/FixedVector2.ts
Normal file
504
packages/framework/math/src/FixedVector2.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
import { Fixed32, type IFixed32 } from './Fixed32';
|
||||
import { FixedMath } from './FixedMath';
|
||||
|
||||
/**
|
||||
* @zh 定点数 2D 向量数据接口
|
||||
* @en Fixed-point 2D vector data interface
|
||||
*/
|
||||
export interface IFixedVector2 {
|
||||
x: IFixed32;
|
||||
y: IFixed32;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定点数 2D 向量类,用于确定性计算(帧同步)
|
||||
* @en Fixed-point 2D vector class for deterministic calculations (lockstep)
|
||||
*
|
||||
* @zh 所有运算返回新实例,保证不可变性
|
||||
* @en All operations return new instances, ensuring immutability
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const a = FixedVector2.from(3, 4);
|
||||
* const b = FixedVector2.from(1, 2);
|
||||
* const c = a.add(b); // (4, 6)
|
||||
* const len = a.length(); // 5
|
||||
* ```
|
||||
*/
|
||||
export class FixedVector2 {
|
||||
/** @zh X 分量 @en X component */
|
||||
readonly x: Fixed32;
|
||||
|
||||
/** @zh Y 分量 @en Y component */
|
||||
readonly y: Fixed32;
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
/** @zh 零向量 (0, 0) @en Zero vector */
|
||||
static readonly ZERO = new FixedVector2(Fixed32.ZERO, Fixed32.ZERO);
|
||||
|
||||
/** @zh 单位向量 (1, 1) @en One vector */
|
||||
static readonly ONE = new FixedVector2(Fixed32.ONE, Fixed32.ONE);
|
||||
|
||||
/** @zh 右方向 (1, 0) @en Right direction */
|
||||
static readonly RIGHT = new FixedVector2(Fixed32.ONE, Fixed32.ZERO);
|
||||
|
||||
/** @zh 左方向 (-1, 0) @en Left direction */
|
||||
static readonly LEFT = new FixedVector2(Fixed32.NEG_ONE, Fixed32.ZERO);
|
||||
|
||||
/** @zh 上方向 (0, 1) @en Up direction */
|
||||
static readonly UP = new FixedVector2(Fixed32.ZERO, Fixed32.ONE);
|
||||
|
||||
/** @zh 下方向 (0, -1) @en Down direction */
|
||||
static readonly DOWN = new FixedVector2(Fixed32.ZERO, Fixed32.NEG_ONE);
|
||||
|
||||
// ==================== 构造 ====================
|
||||
|
||||
/**
|
||||
* @zh 创建定点数向量
|
||||
* @en Create fixed-point vector
|
||||
*/
|
||||
constructor(x: Fixed32, y: Fixed32) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从浮点数创建向量
|
||||
* @en Create vector from floating-point numbers
|
||||
*/
|
||||
static from(x: number, y: number): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.from(x), Fixed32.from(y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从原始整数值创建向量
|
||||
* @en Create vector from raw integer values
|
||||
*/
|
||||
static fromRaw(rawX: number, rawY: number): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.fromRaw(rawX), Fixed32.fromRaw(rawY));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从整数创建向量
|
||||
* @en Create vector from integers
|
||||
*/
|
||||
static fromInt(x: number, y: number): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.fromInt(x), Fixed32.fromInt(y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从普通向量接口创建
|
||||
* @en Create from plain vector interface
|
||||
*/
|
||||
static fromObject(obj: { x: number; y: number }): FixedVector2 {
|
||||
return FixedVector2.from(obj.x, obj.y);
|
||||
}
|
||||
|
||||
// ==================== 转换 ====================
|
||||
|
||||
/**
|
||||
* @zh 转换为浮点数对象(用于渲染)
|
||||
* @en Convert to floating-point object (for rendering)
|
||||
*/
|
||||
toObject(): { x: number; y: number } {
|
||||
return {
|
||||
x: this.x.toNumber(),
|
||||
y: this.y.toNumber()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为数组
|
||||
* @en Convert to array
|
||||
*/
|
||||
toArray(): [number, number] {
|
||||
return [this.x.toNumber(), this.y.toNumber()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取原始值对象(用于网络传输)
|
||||
* @en Get raw values object (for network transmission)
|
||||
*/
|
||||
toRawObject(): { x: number; y: number } {
|
||||
return {
|
||||
x: this.x.toRaw(),
|
||||
y: this.y.toRaw()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为字符串
|
||||
* @en Convert to string
|
||||
*/
|
||||
toString(): string {
|
||||
return `FixedVector2(${this.x.toNumber().toFixed(3)}, ${this.y.toNumber().toFixed(3)})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 克隆向量
|
||||
* @en Clone vector
|
||||
*/
|
||||
clone(): FixedVector2 {
|
||||
return new FixedVector2(this.x, this.y);
|
||||
}
|
||||
|
||||
// ==================== 基础运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 向量加法
|
||||
* @en Vector addition
|
||||
*/
|
||||
add(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.add(other.x), this.y.add(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向量减法
|
||||
* @en Vector subtraction
|
||||
*/
|
||||
sub(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.sub(other.x), this.y.sub(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标量乘法
|
||||
* @en Scalar multiplication
|
||||
*/
|
||||
mul(scalar: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(this.x.mul(scalar), this.y.mul(scalar));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标量除法
|
||||
* @en Scalar division
|
||||
*/
|
||||
div(scalar: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(this.x.div(scalar), this.y.div(scalar));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 分量乘法
|
||||
* @en Component-wise multiplication
|
||||
*/
|
||||
mulComponents(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.mul(other.x), this.y.mul(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 分量除法
|
||||
* @en Component-wise division
|
||||
*/
|
||||
divComponents(other: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(this.x.div(other.x), this.y.div(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 取反
|
||||
* @en Negate
|
||||
*/
|
||||
neg(): FixedVector2 {
|
||||
return new FixedVector2(this.x.neg(), this.y.neg());
|
||||
}
|
||||
|
||||
// ==================== 向量运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 点积
|
||||
* @en Dot product
|
||||
*/
|
||||
dot(other: FixedVector2): Fixed32 {
|
||||
return this.x.mul(other.x).add(this.y.mul(other.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 叉积(2D 返回标量)
|
||||
* @en Cross product (returns scalar in 2D)
|
||||
*/
|
||||
cross(other: FixedVector2): Fixed32 {
|
||||
return this.x.mul(other.y).sub(this.y.mul(other.x));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 长度的平方
|
||||
* @en Length squared
|
||||
*/
|
||||
lengthSquared(): Fixed32 {
|
||||
return this.dot(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 长度(模)
|
||||
* @en Length (magnitude)
|
||||
*/
|
||||
length(): Fixed32 {
|
||||
return Fixed32.sqrt(this.lengthSquared());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 归一化(转换为单位向量)
|
||||
* @en Normalize (convert to unit vector)
|
||||
*/
|
||||
normalize(): FixedVector2 {
|
||||
const len = this.length();
|
||||
if (len.isZero()) {
|
||||
return FixedVector2.ZERO;
|
||||
}
|
||||
return this.div(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 到另一个向量的距离平方
|
||||
* @en Distance squared to another vector
|
||||
*/
|
||||
distanceSquaredTo(other: FixedVector2): Fixed32 {
|
||||
const dx = this.x.sub(other.x);
|
||||
const dy = this.y.sub(other.y);
|
||||
return dx.mul(dx).add(dy.mul(dy));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 到另一个向量的距离
|
||||
* @en Distance to another vector
|
||||
*/
|
||||
distanceTo(other: FixedVector2): Fixed32 {
|
||||
return Fixed32.sqrt(this.distanceSquaredTo(other));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取垂直向量(顺时针旋转90度)
|
||||
* @en Get perpendicular vector (clockwise 90 degrees)
|
||||
*/
|
||||
perpendicular(): FixedVector2 {
|
||||
return new FixedVector2(this.y, this.x.neg());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取垂直向量(逆时针旋转90度)
|
||||
* @en Get perpendicular vector (counter-clockwise 90 degrees)
|
||||
*/
|
||||
perpendicularCCW(): FixedVector2 {
|
||||
return new FixedVector2(this.y.neg(), this.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 投影到另一个向量上
|
||||
* @en Project onto another vector
|
||||
*/
|
||||
projectOnto(onto: FixedVector2): FixedVector2 {
|
||||
const dot = this.dot(onto);
|
||||
const lenSq = onto.lengthSquared();
|
||||
if (lenSq.isZero()) {
|
||||
return FixedVector2.ZERO;
|
||||
}
|
||||
return onto.mul(dot.div(lenSq));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 反射向量(关于法线)
|
||||
* @en Reflect vector (about normal)
|
||||
*/
|
||||
reflect(normal: FixedVector2): FixedVector2 {
|
||||
const dot = this.dot(normal);
|
||||
const two = Fixed32.from(2);
|
||||
return this.sub(normal.mul(two.mul(dot)));
|
||||
}
|
||||
|
||||
// ==================== 旋转和角度 ====================
|
||||
|
||||
/**
|
||||
* @zh 旋转向量(顺时针为正,左手坐标系)
|
||||
* @en Rotate vector (clockwise positive, left-hand coordinate system)
|
||||
* @param angle - @zh 旋转角度(弧度)@en Rotation angle in radians
|
||||
*/
|
||||
rotate(angle: Fixed32): FixedVector2 {
|
||||
const cos = FixedMath.cos(angle);
|
||||
const sin = FixedMath.sin(angle);
|
||||
// 顺时针旋转: x' = x*cos + y*sin, y' = -x*sin + y*cos
|
||||
return new FixedVector2(
|
||||
this.x.mul(cos).add(this.y.mul(sin)),
|
||||
this.x.neg().mul(sin).add(this.y.mul(cos))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 围绕一个点旋转
|
||||
* @en Rotate around a point
|
||||
*/
|
||||
rotateAround(center: FixedVector2, angle: Fixed32): FixedVector2 {
|
||||
return this.sub(center).rotate(angle).add(center);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取向量角度(弧度)
|
||||
* @en Get vector angle in radians
|
||||
*/
|
||||
angle(): Fixed32 {
|
||||
return FixedMath.atan2(this.y, this.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取与另一个向量的夹角
|
||||
* @en Get angle between this and another vector
|
||||
*/
|
||||
angleTo(other: FixedVector2): Fixed32 {
|
||||
const cross = this.cross(other);
|
||||
const dot = this.dot(other);
|
||||
return FixedMath.atan2(cross, dot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从极坐标创建向量
|
||||
* @en Create vector from polar coordinates
|
||||
*/
|
||||
static fromPolar(length: Fixed32, angle: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(
|
||||
length.mul(FixedMath.cos(angle)),
|
||||
length.mul(FixedMath.sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从角度创建单位向量
|
||||
* @en Create unit vector from angle
|
||||
*/
|
||||
static fromAngle(angle: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(FixedMath.cos(angle), FixedMath.sin(angle));
|
||||
}
|
||||
|
||||
// ==================== 比较运算 ====================
|
||||
|
||||
/**
|
||||
* @zh 检查是否相等
|
||||
* @en Check equality
|
||||
*/
|
||||
equals(other: FixedVector2): boolean {
|
||||
return this.x.eq(other.x) && this.y.eq(other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否为零向量
|
||||
* @en Check if zero vector
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return this.x.isZero() && this.y.isZero();
|
||||
}
|
||||
|
||||
// ==================== 限制和插值 ====================
|
||||
|
||||
/**
|
||||
* @zh 限制长度
|
||||
* @en Clamp length
|
||||
*/
|
||||
clampLength(maxLength: Fixed32): FixedVector2 {
|
||||
const lenSq = this.lengthSquared();
|
||||
const maxLenSq = maxLength.mul(maxLength);
|
||||
if (lenSq.gt(maxLenSq)) {
|
||||
return this.normalize().mul(maxLength);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 限制分量范围
|
||||
* @en Clamp components
|
||||
*/
|
||||
clamp(min: FixedVector2, max: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(
|
||||
Fixed32.clamp(this.x, min.x, max.x),
|
||||
Fixed32.clamp(this.y, min.y, max.y)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 线性插值
|
||||
* @en Linear interpolation
|
||||
*/
|
||||
lerp(target: FixedVector2, t: Fixed32): FixedVector2 {
|
||||
return new FixedVector2(
|
||||
Fixed32.lerp(this.x, target.x, t),
|
||||
Fixed32.lerp(this.y, target.y, t)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向目标移动固定距离
|
||||
* @en Move towards target by fixed distance
|
||||
*/
|
||||
moveTowards(target: FixedVector2, maxDistance: Fixed32): FixedVector2 {
|
||||
const diff = target.sub(this);
|
||||
const dist = diff.length();
|
||||
|
||||
if (dist.isZero() || dist.le(maxDistance)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
return this.add(diff.div(dist).mul(maxDistance));
|
||||
}
|
||||
|
||||
// ==================== 静态方法 ====================
|
||||
|
||||
/**
|
||||
* @zh 向量加法(静态)
|
||||
* @en Vector addition (static)
|
||||
*/
|
||||
static add(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return a.add(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 向量减法(静态)
|
||||
* @en Vector subtraction (static)
|
||||
*/
|
||||
static sub(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return a.sub(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 点积(静态)
|
||||
* @en Dot product (static)
|
||||
*/
|
||||
static dot(a: FixedVector2, b: FixedVector2): Fixed32 {
|
||||
return a.dot(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 叉积(静态)
|
||||
* @en Cross product (static)
|
||||
*/
|
||||
static cross(a: FixedVector2, b: FixedVector2): Fixed32 {
|
||||
return a.cross(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 距离(静态)
|
||||
* @en Distance (static)
|
||||
*/
|
||||
static distance(a: FixedVector2, b: FixedVector2): Fixed32 {
|
||||
return a.distanceTo(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 线性插值(静态)
|
||||
* @en Linear interpolation (static)
|
||||
*/
|
||||
static lerp(a: FixedVector2, b: FixedVector2, t: Fixed32): FixedVector2 {
|
||||
return a.lerp(b, t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取两个向量的最小分量
|
||||
* @en Get minimum components of two vectors
|
||||
*/
|
||||
static min(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.min(a.x, b.x), Fixed32.min(a.y, b.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取两个向量的最大分量
|
||||
* @en Get maximum components of two vectors
|
||||
*/
|
||||
static max(a: FixedVector2, b: FixedVector2): FixedVector2 {
|
||||
return new FixedVector2(Fixed32.max(a.x, b.x), Fixed32.max(a.y, b.y));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* 2D数学库,为游戏开发提供完整的数学工具
|
||||
* - 基础数学类(向量、矩阵、几何形状)
|
||||
* - 定点数数学(用于帧同步确定性计算)
|
||||
* - 碰撞检测算法
|
||||
* - 动画插值和缓动函数
|
||||
* - 数学工具函数
|
||||
@@ -16,6 +17,11 @@ export { Matrix3 } from './Matrix3';
|
||||
export { Rectangle } from './Rectangle';
|
||||
export { Circle } from './Circle';
|
||||
|
||||
// 定点数数学(帧同步确定性计算)
|
||||
export { Fixed32, type IFixed32 } from './Fixed32';
|
||||
export { FixedVector2, type IFixedVector2 } from './FixedVector2';
|
||||
export { FixedMath } from './FixedMath';
|
||||
|
||||
// 数学工具
|
||||
export { MathUtils } from './MathUtils';
|
||||
|
||||
@@ -27,3 +33,6 @@ export * from './Collision';
|
||||
|
||||
// 动画和插值
|
||||
export * from './Animation';
|
||||
|
||||
// 蓝图节点
|
||||
export * from './nodes';
|
||||
|
||||
463
packages/framework/math/src/nodes/ColorNodes.ts
Normal file
463
packages/framework/math/src/nodes/ColorNodes.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* @zh 颜色蓝图节点
|
||||
* @en Color Blueprint Nodes
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
import { Color } from '../Color';
|
||||
|
||||
interface ColorContext {
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
}
|
||||
|
||||
// Make Color from RGBA
|
||||
export const MakeColorTemplate: BlueprintNodeTemplate = {
|
||||
type: 'MakeColor',
|
||||
title: 'Make Color',
|
||||
category: 'math',
|
||||
description: 'Creates a Color from RGBA',
|
||||
keywords: ['make', 'create', 'color', 'rgba'],
|
||||
menuPath: ['Math', 'Color', 'Make Color'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'r', displayName: 'R (0-255)', type: 'int', defaultValue: 255 },
|
||||
{ name: 'g', displayName: 'G (0-255)', type: 'int', defaultValue: 255 },
|
||||
{ name: 'b', displayName: 'B (0-255)', type: 'int', defaultValue: 255 },
|
||||
{ name: 'a', displayName: 'A (0-1)', type: 'float', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class MakeColorExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const r = Number(ctx.evaluateInput(node.id, 'r', 255));
|
||||
const g = Number(ctx.evaluateInput(node.id, 'g', 255));
|
||||
const b = Number(ctx.evaluateInput(node.id, 'b', 255));
|
||||
const a = Number(ctx.evaluateInput(node.id, 'a', 1));
|
||||
return { outputs: { color: new Color(r, g, b, a) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Break Color
|
||||
export const BreakColorTemplate: BlueprintNodeTemplate = {
|
||||
type: 'BreakColor',
|
||||
title: 'Break Color',
|
||||
category: 'math',
|
||||
description: 'Breaks a Color into RGBA',
|
||||
keywords: ['break', 'split', 'color', 'rgba'],
|
||||
menuPath: ['Math', 'Color', 'Break Color'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'r', displayName: 'R', type: 'int' },
|
||||
{ name: 'g', displayName: 'G', type: 'int' },
|
||||
{ name: 'b', displayName: 'B', type: 'int' },
|
||||
{ name: 'a', displayName: 'A', type: 'float' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class BreakColorExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.WHITE) as Color;
|
||||
const c = color ?? Color.WHITE;
|
||||
return { outputs: { r: c.r, g: c.g, b: c.b, a: c.a } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color from Hex
|
||||
export const ColorFromHexTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorFromHex',
|
||||
title: 'Color From Hex',
|
||||
category: 'math',
|
||||
description: 'Creates a Color from hex string',
|
||||
keywords: ['color', 'hex', 'from', 'create'],
|
||||
menuPath: ['Math', 'Color', 'From Hex'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'hex', displayName: 'Hex', type: 'string', defaultValue: '#FFFFFF' },
|
||||
{ name: 'alpha', displayName: 'Alpha', type: 'float', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorFromHexExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const hex = String(ctx.evaluateInput(node.id, 'hex', '#FFFFFF'));
|
||||
const alpha = Number(ctx.evaluateInput(node.id, 'alpha', 1));
|
||||
return { outputs: { color: Color.fromHex(hex, alpha) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color to Hex
|
||||
export const ColorToHexTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorToHex',
|
||||
title: 'Color To Hex',
|
||||
category: 'math',
|
||||
description: 'Converts a Color to hex string',
|
||||
keywords: ['color', 'hex', 'to', 'convert'],
|
||||
menuPath: ['Math', 'Color', 'To Hex'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'hex', displayName: 'Hex', type: 'string' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorToHexExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.WHITE) as Color;
|
||||
return { outputs: { hex: (color ?? Color.WHITE).toHex() } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color from HSL
|
||||
export const ColorFromHSLTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorFromHSL',
|
||||
title: 'Color From HSL',
|
||||
category: 'math',
|
||||
description: 'Creates a Color from HSL values',
|
||||
keywords: ['color', 'hsl', 'hue', 'saturation', 'lightness'],
|
||||
menuPath: ['Math', 'Color', 'From HSL'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'h', displayName: 'H (0-360)', type: 'float', defaultValue: 0 },
|
||||
{ name: 's', displayName: 'S (0-1)', type: 'float', defaultValue: 1 },
|
||||
{ name: 'l', displayName: 'L (0-1)', type: 'float', defaultValue: 0.5 },
|
||||
{ name: 'a', displayName: 'A (0-1)', type: 'float', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorFromHSLExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const h = Number(ctx.evaluateInput(node.id, 'h', 0));
|
||||
const s = Number(ctx.evaluateInput(node.id, 's', 1));
|
||||
const l = Number(ctx.evaluateInput(node.id, 'l', 0.5));
|
||||
const a = Number(ctx.evaluateInput(node.id, 'a', 1));
|
||||
return { outputs: { color: Color.fromHSL(h, s, l, a) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color to HSL
|
||||
export const ColorToHSLTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorToHSL',
|
||||
title: 'Color To HSL',
|
||||
category: 'math',
|
||||
description: 'Converts a Color to HSL values',
|
||||
keywords: ['color', 'hsl', 'convert'],
|
||||
menuPath: ['Math', 'Color', 'To HSL'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'h', displayName: 'H', type: 'float' },
|
||||
{ name: 's', displayName: 'S', type: 'float' },
|
||||
{ name: 'l', displayName: 'L', type: 'float' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorToHSLExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.WHITE) as Color;
|
||||
const hsl = (color ?? Color.WHITE).toHSL();
|
||||
return { outputs: { h: hsl.h, s: hsl.s, l: hsl.l } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Lerp
|
||||
export const ColorLerpTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorLerp',
|
||||
title: 'Color Lerp',
|
||||
category: 'math',
|
||||
description: 'Linear interpolation between two colors',
|
||||
keywords: ['color', 'lerp', 'interpolate', 'blend'],
|
||||
menuPath: ['Math', 'Color', 'Lerp'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'from', displayName: 'From', type: 'color' },
|
||||
{ name: 'to', displayName: 'To', type: 'color' },
|
||||
{ name: 't', displayName: 'T', type: 'float', defaultValue: 0.5 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorLerpExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const from = ctx.evaluateInput(node.id, 'from', Color.BLACK) as Color;
|
||||
const to = ctx.evaluateInput(node.id, 'to', Color.WHITE) as Color;
|
||||
const t = Number(ctx.evaluateInput(node.id, 't', 0.5));
|
||||
return { outputs: { result: Color.lerp(from ?? Color.BLACK, to ?? Color.WHITE, t) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Lighten
|
||||
export const ColorLightenTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorLighten',
|
||||
title: 'Color Lighten',
|
||||
category: 'math',
|
||||
description: 'Lightens a color',
|
||||
keywords: ['color', 'lighten', 'bright'],
|
||||
menuPath: ['Math', 'Color', 'Lighten'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' },
|
||||
{ name: 'amount', displayName: 'Amount', type: 'float', defaultValue: 0.1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorLightenExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.GRAY) as Color;
|
||||
const amount = Number(ctx.evaluateInput(node.id, 'amount', 0.1));
|
||||
return { outputs: { result: Color.lighten(color ?? Color.GRAY, amount) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Darken
|
||||
export const ColorDarkenTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorDarken',
|
||||
title: 'Color Darken',
|
||||
category: 'math',
|
||||
description: 'Darkens a color',
|
||||
keywords: ['color', 'darken', 'dark'],
|
||||
menuPath: ['Math', 'Color', 'Darken'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' },
|
||||
{ name: 'amount', displayName: 'Amount', type: 'float', defaultValue: 0.1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorDarkenExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.GRAY) as Color;
|
||||
const amount = Number(ctx.evaluateInput(node.id, 'amount', 0.1));
|
||||
return { outputs: { result: Color.darken(color ?? Color.GRAY, amount) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Saturate
|
||||
export const ColorSaturateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorSaturate',
|
||||
title: 'Color Saturate',
|
||||
category: 'math',
|
||||
description: 'Increases color saturation',
|
||||
keywords: ['color', 'saturate', 'saturation'],
|
||||
menuPath: ['Math', 'Color', 'Saturate'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' },
|
||||
{ name: 'amount', displayName: 'Amount', type: 'float', defaultValue: 0.1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorSaturateExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.GRAY) as Color;
|
||||
const amount = Number(ctx.evaluateInput(node.id, 'amount', 0.1));
|
||||
return { outputs: { result: Color.saturate(color ?? Color.GRAY, amount) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Desaturate
|
||||
export const ColorDesaturateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorDesaturate',
|
||||
title: 'Color Desaturate',
|
||||
category: 'math',
|
||||
description: 'Decreases color saturation',
|
||||
keywords: ['color', 'desaturate', 'saturation'],
|
||||
menuPath: ['Math', 'Color', 'Desaturate'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' },
|
||||
{ name: 'amount', displayName: 'Amount', type: 'float', defaultValue: 0.1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorDesaturateExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.GRAY) as Color;
|
||||
const amount = Number(ctx.evaluateInput(node.id, 'amount', 0.1));
|
||||
return { outputs: { result: Color.desaturate(color ?? Color.GRAY, amount) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Invert
|
||||
export const ColorInvertTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorInvert',
|
||||
title: 'Color Invert',
|
||||
category: 'math',
|
||||
description: 'Inverts a color',
|
||||
keywords: ['color', 'invert', 'inverse'],
|
||||
menuPath: ['Math', 'Color', 'Invert'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorInvertExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.WHITE) as Color;
|
||||
return { outputs: { result: Color.invert(color ?? Color.WHITE) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Grayscale
|
||||
export const ColorGrayscaleTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorGrayscale',
|
||||
title: 'Color Grayscale',
|
||||
category: 'math',
|
||||
description: 'Converts color to grayscale',
|
||||
keywords: ['color', 'grayscale', 'gray', 'grey'],
|
||||
menuPath: ['Math', 'Color', 'Grayscale'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorGrayscaleExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.WHITE) as Color;
|
||||
return { outputs: { result: Color.grayscale(color ?? Color.WHITE) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Luminance
|
||||
export const ColorLuminanceTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorLuminance',
|
||||
title: 'Color Luminance',
|
||||
category: 'math',
|
||||
description: 'Gets perceived brightness (0-1)',
|
||||
keywords: ['color', 'luminance', 'brightness'],
|
||||
menuPath: ['Math', 'Color', 'Luminance'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'color', displayName: 'Color', type: 'color' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'luminance', displayName: 'Luminance', type: 'float' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorLuminanceExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ColorContext;
|
||||
const color = ctx.evaluateInput(node.id, 'color', Color.WHITE) as Color;
|
||||
return { outputs: { luminance: Color.luminance(color ?? Color.WHITE) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Color Constants
|
||||
export const ColorConstantsTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ColorConstants',
|
||||
title: 'Color Constants',
|
||||
category: 'math',
|
||||
description: 'Common color constants',
|
||||
keywords: ['color', 'constant', 'red', 'green', 'blue', 'white', 'black'],
|
||||
menuPath: ['Math', 'Color', 'Constants'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'white', displayName: 'White', type: 'color' },
|
||||
{ name: 'black', displayName: 'Black', type: 'color' },
|
||||
{ name: 'red', displayName: 'Red', type: 'color' },
|
||||
{ name: 'green', displayName: 'Green', type: 'color' },
|
||||
{ name: 'blue', displayName: 'Blue', type: 'color' },
|
||||
{ name: 'transparent', displayName: 'Transparent', type: 'color' }
|
||||
],
|
||||
color: '#E91E63'
|
||||
};
|
||||
|
||||
export class ColorConstantsExecutor implements INodeExecutor {
|
||||
execute(): ExecutionResult {
|
||||
return {
|
||||
outputs: {
|
||||
white: Color.WHITE,
|
||||
black: Color.BLACK,
|
||||
red: Color.RED,
|
||||
green: Color.GREEN,
|
||||
blue: Color.BLUE,
|
||||
transparent: Color.TRANSPARENT
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Node definitions collection
|
||||
export const ColorNodeDefinitions = [
|
||||
{ template: MakeColorTemplate, executor: new MakeColorExecutor() },
|
||||
{ template: BreakColorTemplate, executor: new BreakColorExecutor() },
|
||||
{ template: ColorFromHexTemplate, executor: new ColorFromHexExecutor() },
|
||||
{ template: ColorToHexTemplate, executor: new ColorToHexExecutor() },
|
||||
{ template: ColorFromHSLTemplate, executor: new ColorFromHSLExecutor() },
|
||||
{ template: ColorToHSLTemplate, executor: new ColorToHSLExecutor() },
|
||||
{ template: ColorLerpTemplate, executor: new ColorLerpExecutor() },
|
||||
{ template: ColorLightenTemplate, executor: new ColorLightenExecutor() },
|
||||
{ template: ColorDarkenTemplate, executor: new ColorDarkenExecutor() },
|
||||
{ template: ColorSaturateTemplate, executor: new ColorSaturateExecutor() },
|
||||
{ template: ColorDesaturateTemplate, executor: new ColorDesaturateExecutor() },
|
||||
{ template: ColorInvertTemplate, executor: new ColorInvertExecutor() },
|
||||
{ template: ColorGrayscaleTemplate, executor: new ColorGrayscaleExecutor() },
|
||||
{ template: ColorLuminanceTemplate, executor: new ColorLuminanceExecutor() },
|
||||
{ template: ColorConstantsTemplate, executor: new ColorConstantsExecutor() }
|
||||
];
|
||||
662
packages/framework/math/src/nodes/FixedNodes.ts
Normal file
662
packages/framework/math/src/nodes/FixedNodes.ts
Normal file
@@ -0,0 +1,662 @@
|
||||
/**
|
||||
* @zh Fixed32 定点数蓝图节点
|
||||
* @en Fixed32 Blueprint Nodes
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
import { Fixed32 } from '../Fixed32';
|
||||
|
||||
interface FixedContext {
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
}
|
||||
|
||||
// Make Fixed32 from float
|
||||
export const Fixed32FromTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32From',
|
||||
title: 'Fixed32 From Float',
|
||||
category: 'math',
|
||||
description: 'Creates Fixed32 from float',
|
||||
keywords: ['fixed', 'fixed32', 'from', 'create', 'deterministic'],
|
||||
menuPath: ['Math', 'Fixed', 'From Float'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'float', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'fixed', displayName: 'Fixed32', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32FromExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = Number(ctx.evaluateInput(node.id, 'value', 0));
|
||||
return { outputs: { fixed: Fixed32.from(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Make Fixed32 from int
|
||||
export const Fixed32FromIntTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32FromInt',
|
||||
title: 'Fixed32 From Int',
|
||||
category: 'math',
|
||||
description: 'Creates Fixed32 from integer (no precision loss)',
|
||||
keywords: ['fixed', 'fixed32', 'from', 'int', 'integer'],
|
||||
menuPath: ['Math', 'Fixed', 'From Int'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'int', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'fixed', displayName: 'Fixed32', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32FromIntExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = Math.floor(Number(ctx.evaluateInput(node.id, 'value', 0)));
|
||||
return { outputs: { fixed: Fixed32.fromInt(value) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 to float
|
||||
export const Fixed32ToFloatTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32ToFloat',
|
||||
title: 'Fixed32 To Float',
|
||||
category: 'math',
|
||||
description: 'Converts Fixed32 to float',
|
||||
keywords: ['fixed', 'fixed32', 'to', 'float', 'convert'],
|
||||
menuPath: ['Math', 'Fixed', 'To Float'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'fixed', displayName: 'Fixed32', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'float' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32ToFloatExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const fixed = ctx.evaluateInput(node.id, 'fixed', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { value: fixed?.toNumber() ?? 0 } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 to int
|
||||
export const Fixed32ToIntTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32ToInt',
|
||||
title: 'Fixed32 To Int',
|
||||
category: 'math',
|
||||
description: 'Converts Fixed32 to integer (floor)',
|
||||
keywords: ['fixed', 'fixed32', 'to', 'int', 'integer'],
|
||||
menuPath: ['Math', 'Fixed', 'To Int'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'fixed', displayName: 'Fixed32', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'int' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32ToIntExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const fixed = ctx.evaluateInput(node.id, 'fixed', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { value: fixed?.toInt() ?? 0 } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Add
|
||||
export const Fixed32AddTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Add',
|
||||
title: 'Fixed32 +',
|
||||
category: 'math',
|
||||
description: 'Adds two Fixed32 values',
|
||||
keywords: ['fixed', 'add', 'plus', '+'],
|
||||
menuPath: ['Math', 'Fixed', 'Add'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32AddExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: (a ?? Fixed32.ZERO).add(b ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Subtract
|
||||
export const Fixed32SubtractTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Subtract',
|
||||
title: 'Fixed32 -',
|
||||
category: 'math',
|
||||
description: 'Subtracts B from A',
|
||||
keywords: ['fixed', 'subtract', 'minus', '-'],
|
||||
menuPath: ['Math', 'Fixed', 'Subtract'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32SubtractExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: (a ?? Fixed32.ZERO).sub(b ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Multiply
|
||||
export const Fixed32MultiplyTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Multiply',
|
||||
title: 'Fixed32 *',
|
||||
category: 'math',
|
||||
description: 'Multiplies two Fixed32 values',
|
||||
keywords: ['fixed', 'multiply', 'times', '*'],
|
||||
menuPath: ['Math', 'Fixed', 'Multiply'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32MultiplyExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ONE) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ONE) as Fixed32;
|
||||
return { outputs: { result: (a ?? Fixed32.ONE).mul(b ?? Fixed32.ONE) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Divide
|
||||
export const Fixed32DivideTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Divide',
|
||||
title: 'Fixed32 /',
|
||||
category: 'math',
|
||||
description: 'Divides A by B',
|
||||
keywords: ['fixed', 'divide', '/'],
|
||||
menuPath: ['Math', 'Fixed', 'Divide'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32DivideExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ONE) as Fixed32;
|
||||
const divisor = b ?? Fixed32.ONE;
|
||||
if (divisor.isZero()) {
|
||||
return { outputs: { result: Fixed32.ZERO } };
|
||||
}
|
||||
return { outputs: { result: (a ?? Fixed32.ZERO).div(divisor) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Negate
|
||||
export const Fixed32NegateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Negate',
|
||||
title: 'Fixed32 Negate',
|
||||
category: 'math',
|
||||
description: 'Negates a Fixed32 value',
|
||||
keywords: ['fixed', 'negate', '-'],
|
||||
menuPath: ['Math', 'Fixed', 'Negate'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32NegateExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: (value ?? Fixed32.ZERO).neg() } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Abs
|
||||
export const Fixed32AbsTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Abs',
|
||||
title: 'Fixed32 Abs',
|
||||
category: 'math',
|
||||
description: 'Absolute value of Fixed32',
|
||||
keywords: ['fixed', 'abs', 'absolute'],
|
||||
menuPath: ['Math', 'Fixed', 'Abs'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32AbsExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: (value ?? Fixed32.ZERO).abs() } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Sqrt
|
||||
export const Fixed32SqrtTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Sqrt',
|
||||
title: 'Fixed32 Sqrt',
|
||||
category: 'math',
|
||||
description: 'Square root (deterministic)',
|
||||
keywords: ['fixed', 'sqrt', 'square', 'root'],
|
||||
menuPath: ['Math', 'Fixed', 'Sqrt'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32SqrtExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.sqrt(value ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Floor
|
||||
export const Fixed32FloorTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Floor',
|
||||
title: 'Fixed32 Floor',
|
||||
category: 'math',
|
||||
description: 'Floor of Fixed32',
|
||||
keywords: ['fixed', 'floor', 'round', 'down'],
|
||||
menuPath: ['Math', 'Fixed', 'Floor'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32FloorExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.floor(value ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Ceil
|
||||
export const Fixed32CeilTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Ceil',
|
||||
title: 'Fixed32 Ceil',
|
||||
category: 'math',
|
||||
description: 'Ceiling of Fixed32',
|
||||
keywords: ['fixed', 'ceil', 'ceiling', 'round', 'up'],
|
||||
menuPath: ['Math', 'Fixed', 'Ceil'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32CeilExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.ceil(value ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Round
|
||||
export const Fixed32RoundTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Round',
|
||||
title: 'Fixed32 Round',
|
||||
category: 'math',
|
||||
description: 'Rounds Fixed32 to nearest integer',
|
||||
keywords: ['fixed', 'round'],
|
||||
menuPath: ['Math', 'Fixed', 'Round'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32RoundExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.round(value ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Sign
|
||||
export const Fixed32SignTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Sign',
|
||||
title: 'Fixed32 Sign',
|
||||
category: 'math',
|
||||
description: 'Sign of Fixed32 (-1, 0, or 1)',
|
||||
keywords: ['fixed', 'sign'],
|
||||
menuPath: ['Math', 'Fixed', 'Sign'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32SignExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.sign(value ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Min
|
||||
export const Fixed32MinTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Min',
|
||||
title: 'Fixed32 Min',
|
||||
category: 'math',
|
||||
description: 'Minimum of two Fixed32 values',
|
||||
keywords: ['fixed', 'min', 'minimum'],
|
||||
menuPath: ['Math', 'Fixed', 'Min'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32MinExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.min(a ?? Fixed32.ZERO, b ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Max
|
||||
export const Fixed32MaxTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Max',
|
||||
title: 'Fixed32 Max',
|
||||
category: 'math',
|
||||
description: 'Maximum of two Fixed32 values',
|
||||
keywords: ['fixed', 'max', 'maximum'],
|
||||
menuPath: ['Math', 'Fixed', 'Max'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32MaxExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ZERO) as Fixed32;
|
||||
return { outputs: { result: Fixed32.max(a ?? Fixed32.ZERO, b ?? Fixed32.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Clamp
|
||||
export const Fixed32ClampTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Clamp',
|
||||
title: 'Fixed32 Clamp',
|
||||
category: 'math',
|
||||
description: 'Clamps Fixed32 to range',
|
||||
keywords: ['fixed', 'clamp', 'limit', 'range'],
|
||||
menuPath: ['Math', 'Fixed', 'Clamp'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' },
|
||||
{ name: 'min', displayName: 'Min', type: 'object' },
|
||||
{ name: 'max', displayName: 'Max', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32ClampExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
const min = ctx.evaluateInput(node.id, 'min', Fixed32.ZERO) as Fixed32;
|
||||
const max = ctx.evaluateInput(node.id, 'max', Fixed32.ONE) as Fixed32;
|
||||
return { outputs: { result: Fixed32.clamp(value ?? Fixed32.ZERO, min ?? Fixed32.ZERO, max ?? Fixed32.ONE) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Lerp
|
||||
export const Fixed32LerpTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Lerp',
|
||||
title: 'Fixed32 Lerp',
|
||||
category: 'math',
|
||||
description: 'Linear interpolation between A and B',
|
||||
keywords: ['fixed', 'lerp', 'interpolate', 'blend'],
|
||||
menuPath: ['Math', 'Fixed', 'Lerp'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' },
|
||||
{ name: 't', displayName: 'T', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32LerpExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ONE) as Fixed32;
|
||||
const t = ctx.evaluateInput(node.id, 't', Fixed32.HALF) as Fixed32;
|
||||
return { outputs: { result: Fixed32.lerp(a ?? Fixed32.ZERO, b ?? Fixed32.ONE, t ?? Fixed32.HALF) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Compare
|
||||
export const Fixed32CompareTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Compare',
|
||||
title: 'Fixed32 Compare',
|
||||
category: 'math',
|
||||
description: 'Compares two Fixed32 values',
|
||||
keywords: ['fixed', 'compare', 'equal', 'less', 'greater'],
|
||||
menuPath: ['Math', 'Fixed', 'Compare'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'equal', displayName: 'A == B', type: 'bool' },
|
||||
{ name: 'less', displayName: 'A < B', type: 'bool' },
|
||||
{ name: 'greater', displayName: 'A > B', type: 'bool' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32CompareExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Fixed32.ZERO) as Fixed32;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Fixed32.ZERO) as Fixed32;
|
||||
const aVal = a ?? Fixed32.ZERO;
|
||||
const bVal = b ?? Fixed32.ZERO;
|
||||
return {
|
||||
outputs: {
|
||||
equal: aVal.eq(bVal),
|
||||
less: aVal.lt(bVal),
|
||||
greater: aVal.gt(bVal)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 IsZero
|
||||
export const Fixed32IsZeroTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32IsZero',
|
||||
title: 'Fixed32 Is Zero',
|
||||
category: 'math',
|
||||
description: 'Checks if Fixed32 is zero, positive, or negative',
|
||||
keywords: ['fixed', 'zero', 'check'],
|
||||
menuPath: ['Math', 'Fixed', 'Is Zero'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'value', displayName: 'Value', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'isZero', displayName: 'Is Zero', type: 'bool' },
|
||||
{ name: 'isPositive', displayName: 'Is Positive', type: 'bool' },
|
||||
{ name: 'isNegative', displayName: 'Is Negative', type: 'bool' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32IsZeroExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedContext;
|
||||
const value = ctx.evaluateInput(node.id, 'value', Fixed32.ZERO) as Fixed32;
|
||||
const val = value ?? Fixed32.ZERO;
|
||||
return {
|
||||
outputs: {
|
||||
isZero: val.isZero(),
|
||||
isPositive: val.isPositive(),
|
||||
isNegative: val.isNegative()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed32 Constants
|
||||
export const Fixed32ConstantsTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Fixed32Constants',
|
||||
title: 'Fixed32 Constants',
|
||||
category: 'math',
|
||||
description: 'Common Fixed32 constants',
|
||||
keywords: ['fixed', 'constant', 'pi', 'zero', 'one'],
|
||||
menuPath: ['Math', 'Fixed', 'Constants'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'zero', displayName: '0', type: 'object' },
|
||||
{ name: 'one', displayName: '1', type: 'object' },
|
||||
{ name: 'half', displayName: '0.5', type: 'object' },
|
||||
{ name: 'pi', displayName: 'PI', type: 'object' },
|
||||
{ name: 'twoPi', displayName: '2PI', type: 'object' }
|
||||
],
|
||||
color: '#9C27B0'
|
||||
};
|
||||
|
||||
export class Fixed32ConstantsExecutor implements INodeExecutor {
|
||||
execute(): ExecutionResult {
|
||||
return {
|
||||
outputs: {
|
||||
zero: Fixed32.ZERO,
|
||||
one: Fixed32.ONE,
|
||||
half: Fixed32.HALF,
|
||||
pi: Fixed32.PI,
|
||||
twoPi: Fixed32.TWO_PI
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Node definitions collection
|
||||
export const FixedNodeDefinitions = [
|
||||
{ template: Fixed32FromTemplate, executor: new Fixed32FromExecutor() },
|
||||
{ template: Fixed32FromIntTemplate, executor: new Fixed32FromIntExecutor() },
|
||||
{ template: Fixed32ToFloatTemplate, executor: new Fixed32ToFloatExecutor() },
|
||||
{ template: Fixed32ToIntTemplate, executor: new Fixed32ToIntExecutor() },
|
||||
{ template: Fixed32AddTemplate, executor: new Fixed32AddExecutor() },
|
||||
{ template: Fixed32SubtractTemplate, executor: new Fixed32SubtractExecutor() },
|
||||
{ template: Fixed32MultiplyTemplate, executor: new Fixed32MultiplyExecutor() },
|
||||
{ template: Fixed32DivideTemplate, executor: new Fixed32DivideExecutor() },
|
||||
{ template: Fixed32NegateTemplate, executor: new Fixed32NegateExecutor() },
|
||||
{ template: Fixed32AbsTemplate, executor: new Fixed32AbsExecutor() },
|
||||
{ template: Fixed32SqrtTemplate, executor: new Fixed32SqrtExecutor() },
|
||||
{ template: Fixed32FloorTemplate, executor: new Fixed32FloorExecutor() },
|
||||
{ template: Fixed32CeilTemplate, executor: new Fixed32CeilExecutor() },
|
||||
{ template: Fixed32RoundTemplate, executor: new Fixed32RoundExecutor() },
|
||||
{ template: Fixed32SignTemplate, executor: new Fixed32SignExecutor() },
|
||||
{ template: Fixed32MinTemplate, executor: new Fixed32MinExecutor() },
|
||||
{ template: Fixed32MaxTemplate, executor: new Fixed32MaxExecutor() },
|
||||
{ template: Fixed32ClampTemplate, executor: new Fixed32ClampExecutor() },
|
||||
{ template: Fixed32LerpTemplate, executor: new Fixed32LerpExecutor() },
|
||||
{ template: Fixed32CompareTemplate, executor: new Fixed32CompareExecutor() },
|
||||
{ template: Fixed32IsZeroTemplate, executor: new Fixed32IsZeroExecutor() },
|
||||
{ template: Fixed32ConstantsTemplate, executor: new Fixed32ConstantsExecutor() }
|
||||
];
|
||||
360
packages/framework/math/src/nodes/FixedVectorNodes.ts
Normal file
360
packages/framework/math/src/nodes/FixedVectorNodes.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* @zh FixedVector2 定点向量蓝图节点
|
||||
* @en FixedVector2 Blueprint Nodes
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
import { FixedVector2 } from '../FixedVector2';
|
||||
import { Fixed32 } from '../Fixed32';
|
||||
|
||||
interface FixedVectorContext {
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
}
|
||||
|
||||
// Make FixedVector2
|
||||
export const MakeFixedVector2Template: BlueprintNodeTemplate = {
|
||||
type: 'MakeFixedVector2',
|
||||
title: 'Make FixedVector2',
|
||||
category: 'math',
|
||||
description: 'Creates FixedVector2 from floats',
|
||||
keywords: ['make', 'create', 'fixed', 'vector', 'deterministic'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Make FixedVector2'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float', defaultValue: 0 },
|
||||
{ name: 'y', displayName: 'Y', type: 'float', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class MakeFixedVector2Executor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const x = Number(ctx.evaluateInput(node.id, 'x', 0));
|
||||
const y = Number(ctx.evaluateInput(node.id, 'y', 0));
|
||||
return { outputs: { vector: FixedVector2.from(x, y) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Break FixedVector2
|
||||
export const BreakFixedVector2Template: BlueprintNodeTemplate = {
|
||||
type: 'BreakFixedVector2',
|
||||
title: 'Break FixedVector2',
|
||||
category: 'math',
|
||||
description: 'Breaks FixedVector2 into X and Y floats',
|
||||
keywords: ['break', 'split', 'fixed', 'vector'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Break FixedVector2'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float' },
|
||||
{ name: 'y', displayName: 'Y', type: 'float' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class BreakFixedVector2Executor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', FixedVector2.ZERO) as FixedVector2;
|
||||
const v = vector ?? FixedVector2.ZERO;
|
||||
return { outputs: { x: v.x.toNumber(), y: v.y.toNumber() } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Add
|
||||
export const FixedVector2AddTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Add',
|
||||
title: 'FixedVector2 +',
|
||||
category: 'math',
|
||||
description: 'Adds two fixed vectors',
|
||||
keywords: ['fixed', 'vector', 'add', '+'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Add'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2AddExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', FixedVector2.ZERO) as FixedVector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { result: (a ?? FixedVector2.ZERO).add(b ?? FixedVector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Subtract
|
||||
export const FixedVector2SubtractTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Subtract',
|
||||
title: 'FixedVector2 -',
|
||||
category: 'math',
|
||||
description: 'Subtracts B from A',
|
||||
keywords: ['fixed', 'vector', 'subtract', '-'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Subtract'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2SubtractExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', FixedVector2.ZERO) as FixedVector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { result: (a ?? FixedVector2.ZERO).sub(b ?? FixedVector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Scale
|
||||
export const FixedVector2ScaleTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Scale',
|
||||
title: 'FixedVector2 *',
|
||||
category: 'math',
|
||||
description: 'Scales vector by Fixed32 scalar',
|
||||
keywords: ['fixed', 'vector', 'scale', '*'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Scale'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'object' },
|
||||
{ name: 'scalar', displayName: 'Scalar', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2ScaleExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', FixedVector2.ZERO) as FixedVector2;
|
||||
const scalar = ctx.evaluateInput(node.id, 'scalar', Fixed32.ONE) as Fixed32;
|
||||
return { outputs: { result: (vector ?? FixedVector2.ZERO).mul(scalar ?? Fixed32.ONE) } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Negate
|
||||
export const FixedVector2NegateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Negate',
|
||||
title: 'FixedVector2 Negate',
|
||||
category: 'math',
|
||||
description: 'Negates a fixed vector',
|
||||
keywords: ['fixed', 'vector', 'negate', '-'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Negate'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2NegateExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { result: (vector ?? FixedVector2.ZERO).neg() } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Length
|
||||
export const FixedVector2LengthTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Length',
|
||||
title: 'FixedVector2 Length',
|
||||
category: 'math',
|
||||
description: 'Gets the length of a fixed vector',
|
||||
keywords: ['fixed', 'vector', 'length', 'magnitude'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Length'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'length', displayName: 'Length', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2LengthExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { length: (vector ?? FixedVector2.ZERO).length() } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Normalize
|
||||
export const FixedVector2NormalizeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Normalize',
|
||||
title: 'FixedVector2 Normalize',
|
||||
category: 'math',
|
||||
description: 'Normalizes a fixed vector',
|
||||
keywords: ['fixed', 'vector', 'normalize', 'unit'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Normalize'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2NormalizeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { result: (vector ?? FixedVector2.ZERO).normalize() } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Dot
|
||||
export const FixedVector2DotTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Dot',
|
||||
title: 'FixedVector2 Dot',
|
||||
category: 'math',
|
||||
description: 'Calculates dot product',
|
||||
keywords: ['fixed', 'vector', 'dot', 'product'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Dot Product'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2DotExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', FixedVector2.ZERO) as FixedVector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { result: FixedVector2.dot(a ?? FixedVector2.ZERO, b ?? FixedVector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Cross
|
||||
export const FixedVector2CrossTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Cross',
|
||||
title: 'FixedVector2 Cross',
|
||||
category: 'math',
|
||||
description: '2D cross product (returns Fixed32)',
|
||||
keywords: ['fixed', 'vector', 'cross', 'product'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Cross Product'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2CrossExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', FixedVector2.ZERO) as FixedVector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { result: FixedVector2.cross(a ?? FixedVector2.ZERO, b ?? FixedVector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Distance
|
||||
export const FixedVector2DistanceTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Distance',
|
||||
title: 'FixedVector2 Distance',
|
||||
category: 'math',
|
||||
description: 'Distance between two points',
|
||||
keywords: ['fixed', 'vector', 'distance'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Distance'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'distance', displayName: 'Distance', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2DistanceExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', FixedVector2.ZERO) as FixedVector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', FixedVector2.ZERO) as FixedVector2;
|
||||
return { outputs: { distance: FixedVector2.distance(a ?? FixedVector2.ZERO, b ?? FixedVector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// FixedVector2 Lerp
|
||||
export const FixedVector2LerpTemplate: BlueprintNodeTemplate = {
|
||||
type: 'FixedVector2Lerp',
|
||||
title: 'FixedVector2 Lerp',
|
||||
category: 'math',
|
||||
description: 'Linear interpolation between two vectors',
|
||||
keywords: ['fixed', 'vector', 'lerp', 'interpolate'],
|
||||
menuPath: ['Math', 'Fixed Vector', 'Lerp'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'object' },
|
||||
{ name: 'b', displayName: 'B', type: 'object' },
|
||||
{ name: 't', displayName: 'T', type: 'object' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'object' }
|
||||
],
|
||||
color: '#673AB7'
|
||||
};
|
||||
|
||||
export class FixedVector2LerpExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as FixedVectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', FixedVector2.ZERO) as FixedVector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', FixedVector2.ZERO) as FixedVector2;
|
||||
const t = ctx.evaluateInput(node.id, 't', Fixed32.HALF) as Fixed32;
|
||||
return { outputs: { result: FixedVector2.lerp(a ?? FixedVector2.ZERO, b ?? FixedVector2.ZERO, t ?? Fixed32.HALF) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Node definitions collection
|
||||
export const FixedVectorNodeDefinitions = [
|
||||
{ template: MakeFixedVector2Template, executor: new MakeFixedVector2Executor() },
|
||||
{ template: BreakFixedVector2Template, executor: new BreakFixedVector2Executor() },
|
||||
{ template: FixedVector2AddTemplate, executor: new FixedVector2AddExecutor() },
|
||||
{ template: FixedVector2SubtractTemplate, executor: new FixedVector2SubtractExecutor() },
|
||||
{ template: FixedVector2ScaleTemplate, executor: new FixedVector2ScaleExecutor() },
|
||||
{ template: FixedVector2NegateTemplate, executor: new FixedVector2NegateExecutor() },
|
||||
{ template: FixedVector2LengthTemplate, executor: new FixedVector2LengthExecutor() },
|
||||
{ template: FixedVector2NormalizeTemplate, executor: new FixedVector2NormalizeExecutor() },
|
||||
{ template: FixedVector2DotTemplate, executor: new FixedVector2DotExecutor() },
|
||||
{ template: FixedVector2CrossTemplate, executor: new FixedVector2CrossExecutor() },
|
||||
{ template: FixedVector2DistanceTemplate, executor: new FixedVector2DistanceExecutor() },
|
||||
{ template: FixedVector2LerpTemplate, executor: new FixedVector2LerpExecutor() }
|
||||
];
|
||||
387
packages/framework/math/src/nodes/VectorNodes.ts
Normal file
387
packages/framework/math/src/nodes/VectorNodes.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* @zh Vector2 蓝图节点
|
||||
* @en Vector2 Blueprint Nodes
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
import { Vector2 } from '../Vector2';
|
||||
|
||||
interface VectorContext {
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
}
|
||||
|
||||
// Make Vector2
|
||||
export const MakeVector2Template: BlueprintNodeTemplate = {
|
||||
type: 'MakeVector2',
|
||||
title: 'Make Vector2',
|
||||
category: 'math',
|
||||
description: 'Creates a Vector2 from X and Y',
|
||||
keywords: ['make', 'create', 'vector', 'vector2'],
|
||||
menuPath: ['Math', 'Vector', 'Make Vector2'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float', defaultValue: 0 },
|
||||
{ name: 'y', displayName: 'Y', type: 'float', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class MakeVector2Executor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const x = Number(ctx.evaluateInput(node.id, 'x', 0));
|
||||
const y = Number(ctx.evaluateInput(node.id, 'y', 0));
|
||||
return { outputs: { vector: new Vector2(x, y) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Break Vector2
|
||||
export const BreakVector2Template: BlueprintNodeTemplate = {
|
||||
type: 'BreakVector2',
|
||||
title: 'Break Vector2',
|
||||
category: 'math',
|
||||
description: 'Breaks a Vector2 into X and Y',
|
||||
keywords: ['break', 'split', 'vector', 'vector2'],
|
||||
menuPath: ['Math', 'Vector', 'Break Vector2'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'x', displayName: 'X', type: 'float' },
|
||||
{ name: 'y', displayName: 'Y', type: 'float' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class BreakVector2Executor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { x: vector?.x ?? 0, y: vector?.y ?? 0 } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Add
|
||||
export const Vector2AddTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Add',
|
||||
title: 'Vector2 +',
|
||||
category: 'math',
|
||||
description: 'Adds two vectors',
|
||||
keywords: ['add', 'plus', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Add'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'vector2' },
|
||||
{ name: 'b', displayName: 'B', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2AddExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Vector2.ZERO) as Vector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { result: Vector2.add(a ?? Vector2.ZERO, b ?? Vector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Subtract
|
||||
export const Vector2SubtractTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Subtract',
|
||||
title: 'Vector2 -',
|
||||
category: 'math',
|
||||
description: 'Subtracts B from A',
|
||||
keywords: ['subtract', 'minus', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Subtract'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'vector2' },
|
||||
{ name: 'b', displayName: 'B', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2SubtractExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Vector2.ZERO) as Vector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { result: Vector2.subtract(a ?? Vector2.ZERO, b ?? Vector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Scale
|
||||
export const Vector2ScaleTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Scale',
|
||||
title: 'Vector2 *',
|
||||
category: 'math',
|
||||
description: 'Scales a vector by a scalar',
|
||||
keywords: ['scale', 'multiply', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Scale'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' },
|
||||
{ name: 'scalar', displayName: 'Scalar', type: 'float', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2ScaleExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', Vector2.ZERO) as Vector2;
|
||||
const scalar = Number(ctx.evaluateInput(node.id, 'scalar', 1));
|
||||
return { outputs: { result: Vector2.multiply(vector ?? Vector2.ZERO, scalar) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Length
|
||||
export const Vector2LengthTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Length',
|
||||
title: 'Vector2 Length',
|
||||
category: 'math',
|
||||
description: 'Gets the length of a vector',
|
||||
keywords: ['length', 'magnitude', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Length'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'length', displayName: 'Length', type: 'float' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2LengthExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { length: (vector ?? Vector2.ZERO).length } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Normalize
|
||||
export const Vector2NormalizeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Normalize',
|
||||
title: 'Vector2 Normalize',
|
||||
category: 'math',
|
||||
description: 'Normalizes a vector to unit length',
|
||||
keywords: ['normalize', 'unit', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Normalize'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2NormalizeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { result: (vector ?? Vector2.ZERO).normalized() } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Dot
|
||||
export const Vector2DotTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Dot',
|
||||
title: 'Vector2 Dot',
|
||||
category: 'math',
|
||||
description: 'Calculates dot product',
|
||||
keywords: ['dot', 'product', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Dot Product'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'vector2' },
|
||||
{ name: 'b', displayName: 'B', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'float' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2DotExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Vector2.ZERO) as Vector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { result: Vector2.dot(a ?? Vector2.ZERO, b ?? Vector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Cross
|
||||
export const Vector2CrossTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Cross',
|
||||
title: 'Vector2 Cross',
|
||||
category: 'math',
|
||||
description: '2D cross product (returns scalar)',
|
||||
keywords: ['cross', 'product', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Cross Product'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'vector2' },
|
||||
{ name: 'b', displayName: 'B', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'float' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2CrossExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Vector2.ZERO) as Vector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { result: Vector2.cross(a ?? Vector2.ZERO, b ?? Vector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Distance
|
||||
export const Vector2DistanceTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Distance',
|
||||
title: 'Vector2 Distance',
|
||||
category: 'math',
|
||||
description: 'Distance between two points',
|
||||
keywords: ['distance', 'length', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Distance'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'vector2' },
|
||||
{ name: 'b', displayName: 'B', type: 'vector2' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'distance', displayName: 'Distance', type: 'float' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2DistanceExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Vector2.ZERO) as Vector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Vector2.ZERO) as Vector2;
|
||||
return { outputs: { distance: Vector2.distance(a ?? Vector2.ZERO, b ?? Vector2.ZERO) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Lerp
|
||||
export const Vector2LerpTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Lerp',
|
||||
title: 'Vector2 Lerp',
|
||||
category: 'math',
|
||||
description: 'Linear interpolation between two vectors',
|
||||
keywords: ['lerp', 'interpolate', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Lerp'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', displayName: 'A', type: 'vector2' },
|
||||
{ name: 'b', displayName: 'B', type: 'vector2' },
|
||||
{ name: 't', displayName: 'T', type: 'float', defaultValue: 0.5 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2LerpExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const a = ctx.evaluateInput(node.id, 'a', Vector2.ZERO) as Vector2;
|
||||
const b = ctx.evaluateInput(node.id, 'b', Vector2.ZERO) as Vector2;
|
||||
const t = Number(ctx.evaluateInput(node.id, 't', 0.5));
|
||||
return { outputs: { result: Vector2.lerp(a ?? Vector2.ZERO, b ?? Vector2.ZERO, t) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 Rotate
|
||||
export const Vector2RotateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2Rotate',
|
||||
title: 'Vector2 Rotate',
|
||||
category: 'math',
|
||||
description: 'Rotates a vector by angle (radians)',
|
||||
keywords: ['rotate', 'turn', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'Rotate'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' },
|
||||
{ name: 'angle', displayName: 'Angle (rad)', type: 'float', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', displayName: 'Result', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2RotateExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const vector = ctx.evaluateInput(node.id, 'vector', Vector2.ZERO) as Vector2;
|
||||
const angle = Number(ctx.evaluateInput(node.id, 'angle', 0));
|
||||
return { outputs: { result: (vector ?? Vector2.ZERO).rotated(angle) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Vector2 From Angle
|
||||
export const Vector2FromAngleTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Vector2FromAngle',
|
||||
title: 'Vector2 From Angle',
|
||||
category: 'math',
|
||||
description: 'Creates unit vector from angle (radians)',
|
||||
keywords: ['from', 'angle', 'direction', 'vector'],
|
||||
menuPath: ['Math', 'Vector', 'From Angle'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'angle', displayName: 'Angle (rad)', type: 'float', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'vector', displayName: 'Vector', type: 'vector2' }
|
||||
],
|
||||
color: '#2196F3'
|
||||
};
|
||||
|
||||
export class Vector2FromAngleExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as VectorContext;
|
||||
const angle = Number(ctx.evaluateInput(node.id, 'angle', 0));
|
||||
return { outputs: { vector: Vector2.fromAngle(angle) } };
|
||||
}
|
||||
}
|
||||
|
||||
// Node definitions collection
|
||||
export const VectorNodeDefinitions = [
|
||||
{ template: MakeVector2Template, executor: new MakeVector2Executor() },
|
||||
{ template: BreakVector2Template, executor: new BreakVector2Executor() },
|
||||
{ template: Vector2AddTemplate, executor: new Vector2AddExecutor() },
|
||||
{ template: Vector2SubtractTemplate, executor: new Vector2SubtractExecutor() },
|
||||
{ template: Vector2ScaleTemplate, executor: new Vector2ScaleExecutor() },
|
||||
{ template: Vector2LengthTemplate, executor: new Vector2LengthExecutor() },
|
||||
{ template: Vector2NormalizeTemplate, executor: new Vector2NormalizeExecutor() },
|
||||
{ template: Vector2DotTemplate, executor: new Vector2DotExecutor() },
|
||||
{ template: Vector2CrossTemplate, executor: new Vector2CrossExecutor() },
|
||||
{ template: Vector2DistanceTemplate, executor: new Vector2DistanceExecutor() },
|
||||
{ template: Vector2LerpTemplate, executor: new Vector2LerpExecutor() },
|
||||
{ template: Vector2RotateTemplate, executor: new Vector2RotateExecutor() },
|
||||
{ template: Vector2FromAngleTemplate, executor: new Vector2FromAngleExecutor() }
|
||||
];
|
||||
29
packages/framework/math/src/nodes/index.ts
Normal file
29
packages/framework/math/src/nodes/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @zh 数学库蓝图节点
|
||||
* @en Math Library Blueprint Nodes
|
||||
*
|
||||
* @zh 导出所有数学相关的蓝图节点
|
||||
* @en Exports all math-related blueprint nodes
|
||||
*/
|
||||
|
||||
export * from './VectorNodes';
|
||||
export * from './FixedNodes';
|
||||
export * from './FixedVectorNodes';
|
||||
export * from './ColorNodes';
|
||||
|
||||
// Re-export node definition collections
|
||||
import { VectorNodeDefinitions } from './VectorNodes';
|
||||
import { FixedNodeDefinitions } from './FixedNodes';
|
||||
import { FixedVectorNodeDefinitions } from './FixedVectorNodes';
|
||||
import { ColorNodeDefinitions } from './ColorNodes';
|
||||
|
||||
/**
|
||||
* @zh 所有数学库蓝图节点定义
|
||||
* @en All math library blueprint node definitions
|
||||
*/
|
||||
export const MathNodeDefinitions = [
|
||||
...VectorNodeDefinitions,
|
||||
...FixedNodeDefinitions,
|
||||
...FixedVectorNodeDefinitions,
|
||||
...ColorNodeDefinitions
|
||||
];
|
||||
225
packages/framework/math/tests/Fixed32.test.ts
Normal file
225
packages/framework/math/tests/Fixed32.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Fixed32 } from '../src/Fixed32';
|
||||
import { FixedMath } from '../src/FixedMath';
|
||||
|
||||
describe('Fixed32', () => {
|
||||
describe('创建和转换', () => {
|
||||
test('from 应正确从浮点数创建', () => {
|
||||
const a = Fixed32.from(3.5);
|
||||
expect(a.toNumber()).toBeCloseTo(3.5, 4);
|
||||
});
|
||||
|
||||
test('fromInt 应正确从整数创建', () => {
|
||||
const a = Fixed32.fromInt(42);
|
||||
expect(a.toInt()).toBe(42);
|
||||
expect(a.toNumber()).toBe(42);
|
||||
});
|
||||
|
||||
test('fromRaw 应正确从原始值创建', () => {
|
||||
const raw = 65536 * 2; // 2.0
|
||||
const a = Fixed32.fromRaw(raw);
|
||||
expect(a.toNumber()).toBe(2);
|
||||
});
|
||||
|
||||
test('常量应正确', () => {
|
||||
expect(Fixed32.ZERO.toNumber()).toBe(0);
|
||||
expect(Fixed32.ONE.toNumber()).toBe(1);
|
||||
expect(Fixed32.HALF.toNumber()).toBe(0.5);
|
||||
expect(Fixed32.PI.toNumber()).toBeCloseTo(Math.PI, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('基础运算', () => {
|
||||
test('add 应正确计算', () => {
|
||||
const a = Fixed32.from(2.5);
|
||||
const b = Fixed32.from(1.5);
|
||||
expect(a.add(b).toNumber()).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('sub 应正确计算', () => {
|
||||
const a = Fixed32.from(5);
|
||||
const b = Fixed32.from(3);
|
||||
expect(a.sub(b).toNumber()).toBeCloseTo(2, 4);
|
||||
});
|
||||
|
||||
test('mul 应正确计算', () => {
|
||||
const a = Fixed32.from(3);
|
||||
const b = Fixed32.from(4);
|
||||
expect(a.mul(b).toNumber()).toBeCloseTo(12, 4);
|
||||
});
|
||||
|
||||
test('mul 应正确处理小数', () => {
|
||||
const a = Fixed32.from(2.5);
|
||||
const b = Fixed32.from(1.5);
|
||||
expect(a.mul(b).toNumber()).toBeCloseTo(3.75, 4);
|
||||
});
|
||||
|
||||
test('div 应正确计算', () => {
|
||||
const a = Fixed32.from(10);
|
||||
const b = Fixed32.from(4);
|
||||
expect(a.div(b).toNumber()).toBeCloseTo(2.5, 4);
|
||||
});
|
||||
|
||||
test('div 应抛出除零错误', () => {
|
||||
const a = Fixed32.from(10);
|
||||
expect(() => a.div(Fixed32.ZERO)).toThrow('Division by zero');
|
||||
});
|
||||
|
||||
test('neg 应正确取反', () => {
|
||||
const a = Fixed32.from(5);
|
||||
expect(a.neg().toNumber()).toBeCloseTo(-5, 4);
|
||||
});
|
||||
|
||||
test('abs 应正确取绝对值', () => {
|
||||
const a = Fixed32.from(-5);
|
||||
expect(a.abs().toNumber()).toBeCloseTo(5, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('比较运算', () => {
|
||||
test('eq 应正确比较', () => {
|
||||
const a = Fixed32.from(5);
|
||||
const b = Fixed32.from(5);
|
||||
const c = Fixed32.from(6);
|
||||
expect(a.eq(b)).toBe(true);
|
||||
expect(a.eq(c)).toBe(false);
|
||||
});
|
||||
|
||||
test('lt/le/gt/ge 应正确比较', () => {
|
||||
const a = Fixed32.from(3);
|
||||
const b = Fixed32.from(5);
|
||||
expect(a.lt(b)).toBe(true);
|
||||
expect(a.le(b)).toBe(true);
|
||||
expect(b.gt(a)).toBe(true);
|
||||
expect(b.ge(a)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('数学函数', () => {
|
||||
test('sqrt 应正确计算', () => {
|
||||
const a = Fixed32.from(16);
|
||||
expect(Fixed32.sqrt(a).toNumber()).toBeCloseTo(4, 3);
|
||||
|
||||
const b = Fixed32.from(2);
|
||||
expect(Fixed32.sqrt(b).toNumber()).toBeCloseTo(Math.sqrt(2), 3);
|
||||
});
|
||||
|
||||
test('floor/ceil/round 应正确计算', () => {
|
||||
const a = Fixed32.from(3.7);
|
||||
expect(Fixed32.floor(a).toNumber()).toBeCloseTo(3, 4);
|
||||
expect(Fixed32.ceil(a).toNumber()).toBeCloseTo(4, 4);
|
||||
expect(Fixed32.round(a).toNumber()).toBeCloseTo(4, 4);
|
||||
|
||||
const b = Fixed32.from(3.2);
|
||||
expect(Fixed32.round(b).toNumber()).toBeCloseTo(3, 4);
|
||||
});
|
||||
|
||||
test('min/max/clamp 应正确计算', () => {
|
||||
const a = Fixed32.from(3);
|
||||
const b = Fixed32.from(5);
|
||||
expect(Fixed32.min(a, b).toNumber()).toBe(3);
|
||||
expect(Fixed32.max(a, b).toNumber()).toBe(5);
|
||||
|
||||
const x = Fixed32.from(7);
|
||||
expect(Fixed32.clamp(x, a, b).toNumber()).toBe(5);
|
||||
});
|
||||
|
||||
test('lerp 应正确插值', () => {
|
||||
const a = Fixed32.from(0);
|
||||
const b = Fixed32.from(10);
|
||||
const t = Fixed32.from(0.5);
|
||||
expect(Fixed32.lerp(a, b, t).toNumber()).toBeCloseTo(5, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('确定性', () => {
|
||||
test('相同输入应产生相同输出', () => {
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const a = Fixed32.from(3.14159);
|
||||
const b = Fixed32.from(2.71828);
|
||||
const result = a.mul(b).add(Fixed32.sqrt(a)).toRaw();
|
||||
results.push(result);
|
||||
}
|
||||
// 所有结果应该完全相同
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FixedMath', () => {
|
||||
describe('三角函数', () => {
|
||||
test('sin 应正确计算', () => {
|
||||
expect(FixedMath.sin(Fixed32.ZERO).toNumber()).toBeCloseTo(0, 3);
|
||||
expect(FixedMath.sin(Fixed32.HALF_PI).toNumber()).toBeCloseTo(1, 3);
|
||||
expect(FixedMath.sin(Fixed32.PI).toNumber()).toBeCloseTo(0, 2);
|
||||
});
|
||||
|
||||
test('cos 应正确计算', () => {
|
||||
expect(FixedMath.cos(Fixed32.ZERO).toNumber()).toBeCloseTo(1, 3);
|
||||
expect(FixedMath.cos(Fixed32.HALF_PI).toNumber()).toBeCloseTo(0, 2);
|
||||
expect(FixedMath.cos(Fixed32.PI).toNumber()).toBeCloseTo(-1, 3);
|
||||
});
|
||||
|
||||
test('sin²x + cos²x = 1', () => {
|
||||
const angles = [0, 0.5, 1, 1.5, 2, 2.5, 3];
|
||||
for (const a of angles) {
|
||||
const angle = Fixed32.from(a);
|
||||
const sin = FixedMath.sin(angle);
|
||||
const cos = FixedMath.cos(angle);
|
||||
const sum = sin.mul(sin).add(cos.mul(cos));
|
||||
expect(sum.toNumber()).toBeCloseTo(1, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('atan2 应正确计算', () => {
|
||||
// atan2(0, 1) = 0
|
||||
expect(FixedMath.atan2(Fixed32.ZERO, Fixed32.ONE).toNumber()).toBeCloseTo(0, 3);
|
||||
|
||||
// atan2(1, 0) = π/2
|
||||
expect(FixedMath.atan2(Fixed32.ONE, Fixed32.ZERO).toNumber()).toBeCloseTo(Math.PI / 2, 2);
|
||||
|
||||
// atan2(1, 1) = π/4
|
||||
expect(FixedMath.atan2(Fixed32.ONE, Fixed32.ONE).toNumber()).toBeCloseTo(Math.PI / 4, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('角度函数', () => {
|
||||
test('radToDeg/degToRad 应正确转换', () => {
|
||||
const rad = Fixed32.PI;
|
||||
const deg = FixedMath.radToDeg(rad);
|
||||
expect(deg.toNumber()).toBeCloseTo(180, 1);
|
||||
|
||||
const deg90 = Fixed32.from(90);
|
||||
const rad90 = FixedMath.degToRad(deg90);
|
||||
expect(rad90.toNumber()).toBeCloseTo(Math.PI / 2, 2);
|
||||
});
|
||||
|
||||
test('normalizeAngle 应正确规范化', () => {
|
||||
const angle1 = Fixed32.from(Math.PI * 3); // 3π -> π
|
||||
expect(Math.abs(FixedMath.normalizeAngle(angle1).toNumber())).toBeLessThanOrEqual(Math.PI + 0.1);
|
||||
|
||||
const angle2 = Fixed32.from(-Math.PI * 3); // -3π -> -π
|
||||
expect(Math.abs(FixedMath.normalizeAngle(angle2).toNumber())).toBeLessThanOrEqual(Math.PI + 0.1);
|
||||
});
|
||||
|
||||
test('lerpAngle 应走最短路径', () => {
|
||||
const from = Fixed32.from(0.1);
|
||||
const to = Fixed32.from(-0.1);
|
||||
const t = Fixed32.HALF;
|
||||
const result = FixedMath.lerpAngle(from, to, t);
|
||||
expect(result.toNumber()).toBeCloseTo(0, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('确定性', () => {
|
||||
test('三角函数应产生确定性结果', () => {
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const angle = Fixed32.from(1.234);
|
||||
const result = FixedMath.sin(angle).toRaw();
|
||||
results.push(result);
|
||||
}
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
242
packages/framework/math/tests/FixedVector2.test.ts
Normal file
242
packages/framework/math/tests/FixedVector2.test.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { Fixed32 } from '../src/Fixed32';
|
||||
import { FixedVector2 } from '../src/FixedVector2';
|
||||
|
||||
describe('FixedVector2', () => {
|
||||
describe('创建和转换', () => {
|
||||
test('from 应正确从浮点数创建', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
const obj = v.toObject();
|
||||
expect(obj.x).toBeCloseTo(3, 4);
|
||||
expect(obj.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('fromInt 应正确从整数创建', () => {
|
||||
const v = FixedVector2.fromInt(5, 6);
|
||||
expect(v.x.toInt()).toBe(5);
|
||||
expect(v.y.toInt()).toBe(6);
|
||||
});
|
||||
|
||||
test('常量应正确', () => {
|
||||
expect(FixedVector2.ZERO.isZero()).toBe(true);
|
||||
expect(FixedVector2.ONE.x.toNumber()).toBe(1);
|
||||
expect(FixedVector2.ONE.y.toNumber()).toBe(1);
|
||||
expect(FixedVector2.RIGHT.x.toNumber()).toBe(1);
|
||||
expect(FixedVector2.RIGHT.y.toNumber()).toBe(0);
|
||||
});
|
||||
|
||||
test('toRawObject 应返回原始值', () => {
|
||||
const v = FixedVector2.from(1, 2);
|
||||
const raw = v.toRawObject();
|
||||
expect(raw.x).toBe(Fixed32.from(1).toRaw());
|
||||
expect(raw.y).toBe(Fixed32.from(2).toRaw());
|
||||
});
|
||||
});
|
||||
|
||||
describe('基础运算', () => {
|
||||
test('add 应正确计算', () => {
|
||||
const a = FixedVector2.from(1, 2);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
const result = a.add(b).toObject();
|
||||
expect(result.x).toBeCloseTo(4, 4);
|
||||
expect(result.y).toBeCloseTo(6, 4);
|
||||
});
|
||||
|
||||
test('sub 应正确计算', () => {
|
||||
const a = FixedVector2.from(5, 7);
|
||||
const b = FixedVector2.from(2, 3);
|
||||
const result = a.sub(b).toObject();
|
||||
expect(result.x).toBeCloseTo(3, 4);
|
||||
expect(result.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('mul 应正确计算标量乘法', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
const result = v.mul(Fixed32.from(2)).toObject();
|
||||
expect(result.x).toBeCloseTo(6, 4);
|
||||
expect(result.y).toBeCloseTo(8, 4);
|
||||
});
|
||||
|
||||
test('div 应正确计算标量除法', () => {
|
||||
const v = FixedVector2.from(6, 8);
|
||||
const result = v.div(Fixed32.from(2)).toObject();
|
||||
expect(result.x).toBeCloseTo(3, 4);
|
||||
expect(result.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
|
||||
test('neg 应正确取反', () => {
|
||||
const v = FixedVector2.from(3, -4);
|
||||
const result = v.neg().toObject();
|
||||
expect(result.x).toBeCloseTo(-3, 4);
|
||||
expect(result.y).toBeCloseTo(4, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('向量运算', () => {
|
||||
test('dot 应正确计算点积', () => {
|
||||
const a = FixedVector2.from(1, 2);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
// 1*3 + 2*4 = 11
|
||||
expect(a.dot(b).toNumber()).toBeCloseTo(11, 4);
|
||||
});
|
||||
|
||||
test('cross 应正确计算叉积', () => {
|
||||
const a = FixedVector2.from(1, 0);
|
||||
const b = FixedVector2.from(0, 1);
|
||||
// 1*1 - 0*0 = 1
|
||||
expect(a.cross(b).toNumber()).toBeCloseTo(1, 4);
|
||||
});
|
||||
|
||||
test('length 应正确计算', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
expect(v.length().toNumber()).toBeCloseTo(5, 3);
|
||||
});
|
||||
|
||||
test('lengthSquared 应正确计算', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
expect(v.lengthSquared().toNumber()).toBeCloseTo(25, 4);
|
||||
});
|
||||
|
||||
test('normalize 应正确归一化', () => {
|
||||
const v = FixedVector2.from(3, 4);
|
||||
const n = v.normalize();
|
||||
expect(n.length().toNumber()).toBeCloseTo(1, 2);
|
||||
expect(n.x.toNumber()).toBeCloseTo(0.6, 2);
|
||||
expect(n.y.toNumber()).toBeCloseTo(0.8, 2);
|
||||
});
|
||||
|
||||
test('normalize 零向量应返回零向量', () => {
|
||||
const v = FixedVector2.ZERO;
|
||||
const n = v.normalize();
|
||||
expect(n.isZero()).toBe(true);
|
||||
});
|
||||
|
||||
test('distanceTo 应正确计算', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
expect(a.distanceTo(b).toNumber()).toBeCloseTo(5, 3);
|
||||
});
|
||||
|
||||
test('perpendicular 应正确计算', () => {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
const perp = v.perpendicular();
|
||||
// 顺时针 90 度: (1, 0) -> (0, -1)
|
||||
expect(perp.x.toNumber()).toBeCloseTo(0, 4);
|
||||
expect(perp.y.toNumber()).toBeCloseTo(-1, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('旋转和角度', () => {
|
||||
test('rotate 应正确旋转', () => {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
const angle = Fixed32.HALF_PI; // 90 度
|
||||
const rotated = v.rotate(angle);
|
||||
// 顺时针旋转 90 度: (1, 0) -> (0, -1)
|
||||
expect(rotated.x.toNumber()).toBeCloseTo(0, 2);
|
||||
expect(rotated.y.toNumber()).toBeCloseTo(-1, 2);
|
||||
});
|
||||
|
||||
test('angle 应正确计算', () => {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
expect(v.angle().toNumber()).toBeCloseTo(0, 3);
|
||||
|
||||
const v2 = FixedVector2.from(0, 1);
|
||||
expect(v2.angle().toNumber()).toBeCloseTo(Math.PI / 2, 2);
|
||||
});
|
||||
|
||||
test('fromAngle 应正确创建', () => {
|
||||
const v = FixedVector2.fromAngle(Fixed32.ZERO);
|
||||
expect(v.x.toNumber()).toBeCloseTo(1, 3);
|
||||
expect(v.y.toNumber()).toBeCloseTo(0, 3);
|
||||
});
|
||||
|
||||
test('fromPolar 应正确创建', () => {
|
||||
const v = FixedVector2.fromPolar(Fixed32.from(5), Fixed32.ZERO);
|
||||
expect(v.x.toNumber()).toBeCloseTo(5, 3);
|
||||
expect(v.y.toNumber()).toBeCloseTo(0, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('插值和限制', () => {
|
||||
test('lerp 应正确插值', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(10, 20);
|
||||
const result = a.lerp(b, Fixed32.HALF).toObject();
|
||||
expect(result.x).toBeCloseTo(5, 4);
|
||||
expect(result.y).toBeCloseTo(10, 4);
|
||||
});
|
||||
|
||||
test('clampLength 应正确限制长度', () => {
|
||||
const v = FixedVector2.from(6, 8); // 长度 10
|
||||
const clamped = v.clampLength(Fixed32.from(5));
|
||||
expect(clamped.length().toNumber()).toBeCloseTo(5, 2);
|
||||
});
|
||||
|
||||
test('moveTowards 应正确移动', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(10, 0);
|
||||
const result = a.moveTowards(b, Fixed32.from(3));
|
||||
expect(result.x.toNumber()).toBeCloseTo(3, 3);
|
||||
expect(result.y.toNumber()).toBeCloseTo(0, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('比较运算', () => {
|
||||
test('equals 应正确比较', () => {
|
||||
const a = FixedVector2.from(3, 4);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
const c = FixedVector2.from(3, 5);
|
||||
expect(a.equals(b)).toBe(true);
|
||||
expect(a.equals(c)).toBe(false);
|
||||
});
|
||||
|
||||
test('isZero 应正确判断', () => {
|
||||
expect(FixedVector2.ZERO.isZero()).toBe(true);
|
||||
expect(FixedVector2.ONE.isZero()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('确定性', () => {
|
||||
test('向量运算应产生确定性结果', () => {
|
||||
const results: string[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const a = FixedVector2.from(3.14159, 2.71828);
|
||||
const b = FixedVector2.from(1.41421, 1.73205);
|
||||
const result = a.add(b).mul(Fixed32.from(0.5)).normalize();
|
||||
results.push(`${result.x.toRaw()},${result.y.toRaw()}`);
|
||||
}
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
|
||||
test('旋转应产生确定性结果', () => {
|
||||
const results: string[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const v = FixedVector2.from(1, 0);
|
||||
const angle = Fixed32.from(0.7853981634); // π/4
|
||||
const rotated = v.rotate(angle);
|
||||
results.push(`${rotated.x.toRaw()},${rotated.y.toRaw()}`);
|
||||
}
|
||||
expect(new Set(results).size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('静态方法', () => {
|
||||
test('distance 应正确计算', () => {
|
||||
const a = FixedVector2.from(0, 0);
|
||||
const b = FixedVector2.from(3, 4);
|
||||
expect(FixedVector2.distance(a, b).toNumber()).toBeCloseTo(5, 3);
|
||||
});
|
||||
|
||||
test('min/max 应正确计算', () => {
|
||||
const a = FixedVector2.from(1, 5);
|
||||
const b = FixedVector2.from(3, 2);
|
||||
|
||||
const min = FixedVector2.min(a, b);
|
||||
expect(min.x.toNumber()).toBeCloseTo(1, 4);
|
||||
expect(min.y.toNumber()).toBeCloseTo(2, 4);
|
||||
|
||||
const max = FixedVector2.max(a, b);
|
||||
expect(max.x.toNumber()).toBeCloseTo(3, 4);
|
||||
expect(max.y.toNumber()).toBeCloseTo(5, 4);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,57 @@
|
||||
# @esengine/network
|
||||
|
||||
## 13.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e66bd8`](https://github.com/esengine/esengine/commit/4e66bd8e2be80b366a7723dcc48b99df0457aed4)]:
|
||||
- @esengine/blueprint@4.5.0
|
||||
- @esengine/ecs-framework-math@2.10.1
|
||||
|
||||
## 12.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fa593a3`](https://github.com/esengine/esengine/commit/fa593a3c69292207800750f8106f418465cb7c0f)]:
|
||||
- @esengine/ecs-framework-math@2.10.0
|
||||
|
||||
## 11.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`bffe90b`](https://github.com/esengine/esengine/commit/bffe90b6a17563cc90709faf339b229dc3abd22d)]:
|
||||
- @esengine/ecs-framework-math@2.9.0
|
||||
|
||||
## 10.0.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#440](https://github.com/esengine/esengine/pull/440) [`30173f0`](https://github.com/esengine/esengine/commit/30173f076415c9770a429b236b8bab95a2fdc498) Thanks [@esengine](https://github.com/esengine)! - feat(network): 添加定点数网络同步支持 | Add fixed-point network sync support
|
||||
|
||||
**@esengine/network** - 新增定点数同步模块 | Add fixed-point sync module
|
||||
- 新增 `FixedSnapshotBuffer`:定点数快照缓冲区 | Add `FixedSnapshotBuffer`: fixed-point snapshot buffer
|
||||
- 新增 `FixedClientPrediction`:定点数客户端预测 | Add `FixedClientPrediction`: fixed-point client prediction
|
||||
- 支持确定性帧同步和状态回滚 | Support deterministic lockstep and state rollback
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`30173f0`](https://github.com/esengine/esengine/commit/30173f076415c9770a429b236b8bab95a2fdc498)]:
|
||||
- @esengine/ecs-framework-math@2.8.0
|
||||
|
||||
## 9.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`0d33cf0`](https://github.com/esengine/esengine/commit/0d33cf00977d16e6282931aba2cf771ec2c84c6b)]:
|
||||
- @esengine/blueprint@4.4.0
|
||||
|
||||
## 8.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
|
||||
- @esengine/blueprint@4.3.0
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/network",
|
||||
"version": "7.0.0",
|
||||
"version": "13.0.0",
|
||||
"description": "Network synchronization for multiplayer games",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/blueprint": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -42,6 +43,7 @@
|
||||
"devDependencies": {
|
||||
"@esengine/blueprint": "workspace:*",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
|
||||
@@ -138,6 +138,11 @@ export type {
|
||||
ComponentSyncEvent,
|
||||
ComponentSyncEventListener,
|
||||
ComponentSyncConfig,
|
||||
// Fixed-point sync types
|
||||
IFixedTransformStateRaw,
|
||||
IFixedTransformStateWithVelocityRaw,
|
||||
IFixedInterpolator,
|
||||
IFixedExtrapolator,
|
||||
} from './sync'
|
||||
|
||||
export {
|
||||
@@ -158,6 +163,15 @@ export {
|
||||
// Component sync
|
||||
ComponentSyncSystem,
|
||||
createComponentSyncSystem,
|
||||
// Fixed-point sync (Deterministic Lockstep)
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
createZeroFixedTransformState,
|
||||
createZeroFixedTransformStateWithVelocity,
|
||||
FixedTransformInterpolator,
|
||||
FixedHermiteTransformInterpolator,
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator,
|
||||
} from './sync'
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* @zh 定点数客户端预测
|
||||
* @en Fixed-point Client Prediction
|
||||
*
|
||||
* @zh 用于帧同步的确定性客户端预测和回滚
|
||||
* @en Deterministic client prediction and rollback for lockstep
|
||||
*/
|
||||
|
||||
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// =============================================================================
|
||||
// 定点数输入快照接口 | Fixed Input Snapshot Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数输入快照
|
||||
* @en Fixed-point input snapshot
|
||||
*/
|
||||
export interface IFixedInputSnapshot<TInput> {
|
||||
/**
|
||||
* @zh 输入帧号
|
||||
* @en Input frame number
|
||||
*/
|
||||
readonly frame: number;
|
||||
|
||||
/**
|
||||
* @zh 输入数据
|
||||
* @en Input data
|
||||
*/
|
||||
readonly input: TInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定点数预测状态
|
||||
* @en Fixed-point predicted state
|
||||
*/
|
||||
export interface IFixedPredictedState<TState> {
|
||||
/**
|
||||
* @zh 状态数据
|
||||
* @en State data
|
||||
*/
|
||||
readonly state: TState;
|
||||
|
||||
/**
|
||||
* @zh 对应的帧号
|
||||
* @en Corresponding frame number
|
||||
*/
|
||||
readonly frame: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 定点数预测器接口 | Fixed Predictor Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数状态预测器接口
|
||||
* @en Fixed-point state predictor interface
|
||||
*
|
||||
* @zh 必须使用定点数运算确保确定性
|
||||
* @en Must use fixed-point arithmetic to ensure determinism
|
||||
*/
|
||||
export interface IFixedPredictor<TState, TInput> {
|
||||
/**
|
||||
* @zh 根据当前状态和输入预测下一状态
|
||||
* @en Predict next state based on current state and input
|
||||
*
|
||||
* @param state - @zh 当前状态 @en Current state
|
||||
* @param input - @zh 输入 @en Input
|
||||
* @param deltaTime - @zh 固定时间步长(定点数)@en Fixed delta time (fixed-point)
|
||||
* @returns @zh 预测的状态 @en Predicted state
|
||||
*/
|
||||
predict(state: TState, input: TInput, deltaTime: Fixed32): TState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 状态位置提取器接口
|
||||
* @en State position extractor interface
|
||||
*/
|
||||
export interface IFixedStatePositionExtractor<TState> {
|
||||
/**
|
||||
* @zh 从状态中提取位置
|
||||
* @en Extract position from state
|
||||
*/
|
||||
getPosition(state: TState): FixedVector2;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 定点数客户端预测配置 | Fixed Client Prediction Config
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数客户端预测配置
|
||||
* @en Fixed-point client prediction configuration
|
||||
*/
|
||||
export interface FixedClientPredictionConfig {
|
||||
/**
|
||||
* @zh 最大未确认输入数量
|
||||
* @en Maximum unacknowledged inputs
|
||||
*/
|
||||
maxUnacknowledgedInputs: number;
|
||||
|
||||
/**
|
||||
* @zh 固定时间步长(定点数)
|
||||
* @en Fixed delta time (fixed-point)
|
||||
*/
|
||||
fixedDeltaTime: Fixed32;
|
||||
|
||||
/**
|
||||
* @zh 校正阈值(定点数,超过此值才进行校正)
|
||||
* @en Reconciliation threshold (fixed-point, correction only above this value)
|
||||
*/
|
||||
reconciliationThreshold: Fixed32;
|
||||
|
||||
/**
|
||||
* @zh 是否启用平滑校正(帧同步通常关闭)
|
||||
* @en Enable smooth reconciliation (usually disabled for lockstep)
|
||||
*/
|
||||
enableSmoothReconciliation: boolean;
|
||||
|
||||
/**
|
||||
* @zh 平滑校正速度(定点数)
|
||||
* @en Smooth reconciliation speed (fixed-point)
|
||||
*/
|
||||
reconciliationSpeed: Fixed32;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 定点数客户端预测管理器 | Fixed Client Prediction Manager
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数客户端预测管理器
|
||||
* @en Fixed-point client prediction manager
|
||||
*
|
||||
* @zh 提供确定性的客户端预测和服务器状态回滚校正
|
||||
* @en Provides deterministic client prediction and server state rollback reconciliation
|
||||
*/
|
||||
export class FixedClientPrediction<TState, TInput> {
|
||||
private readonly _predictor: IFixedPredictor<TState, TInput>;
|
||||
private readonly _config: FixedClientPredictionConfig;
|
||||
private readonly _pendingInputs: IFixedInputSnapshot<TInput>[] = [];
|
||||
private _lastAcknowledgedFrame: number = 0;
|
||||
private _currentFrame: number = 0;
|
||||
private _lastServerState: TState | null = null;
|
||||
private _predictedState: TState | null = null;
|
||||
private _correctionOffset: FixedVector2 = FixedVector2.ZERO;
|
||||
private _stateHistory: Map<number, TState> = new Map();
|
||||
private readonly _maxHistorySize: number = 120;
|
||||
|
||||
constructor(
|
||||
predictor: IFixedPredictor<TState, TInput>,
|
||||
config?: Partial<FixedClientPredictionConfig>
|
||||
) {
|
||||
this._predictor = predictor;
|
||||
this._config = {
|
||||
maxUnacknowledgedInputs: 60,
|
||||
fixedDeltaTime: Fixed32.from(1 / 60),
|
||||
reconciliationThreshold: Fixed32.from(0.001),
|
||||
enableSmoothReconciliation: false,
|
||||
reconciliationSpeed: Fixed32.from(10),
|
||||
...config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前预测状态
|
||||
* @en Get current predicted state
|
||||
*/
|
||||
get predictedState(): TState | null {
|
||||
return this._predictedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取校正偏移(用于渲染平滑)
|
||||
* @en Get correction offset (for render smoothing)
|
||||
*/
|
||||
get correctionOffset(): FixedVector2 {
|
||||
return this._correctionOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取待确认输入数量
|
||||
* @en Get pending input count
|
||||
*/
|
||||
get pendingInputCount(): number {
|
||||
return this._pendingInputs.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前帧号
|
||||
* @en Get current frame number
|
||||
*/
|
||||
get currentFrame(): number {
|
||||
return this._currentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取最后确认帧号
|
||||
* @en Get last acknowledged frame
|
||||
*/
|
||||
get lastAcknowledgedFrame(): number {
|
||||
return this._lastAcknowledgedFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 记录并预测输入
|
||||
* @en Record and predict input
|
||||
*
|
||||
* @param input - @zh 输入数据 @en Input data
|
||||
* @param currentState - @zh 当前状态 @en Current state
|
||||
* @returns @zh 预测的状态 @en Predicted state
|
||||
*/
|
||||
recordInput(input: TInput, currentState: TState): TState {
|
||||
this._currentFrame++;
|
||||
|
||||
const inputSnapshot: IFixedInputSnapshot<TInput> = {
|
||||
frame: this._currentFrame,
|
||||
input
|
||||
};
|
||||
|
||||
this._pendingInputs.push(inputSnapshot);
|
||||
|
||||
while (this._pendingInputs.length > this._config.maxUnacknowledgedInputs) {
|
||||
this._pendingInputs.shift();
|
||||
}
|
||||
|
||||
this._predictedState = this._predictor.predict(
|
||||
currentState,
|
||||
input,
|
||||
this._config.fixedDeltaTime
|
||||
);
|
||||
|
||||
this._stateHistory.set(this._currentFrame, this._predictedState);
|
||||
this._cleanupHistory();
|
||||
|
||||
return this._predictedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取指定帧的输入
|
||||
* @en Get input at specific frame
|
||||
*/
|
||||
getInputAtFrame(frame: number): IFixedInputSnapshot<TInput> | null {
|
||||
return this._pendingInputs.find(i => i.frame === frame) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有待确认输入
|
||||
* @en Get all pending inputs
|
||||
*/
|
||||
getPendingInputs(): readonly IFixedInputSnapshot<TInput>[] {
|
||||
return this._pendingInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理服务器状态并进行回滚校正
|
||||
* @en Process server state and perform rollback reconciliation
|
||||
*
|
||||
* @param serverState - @zh 服务器权威状态 @en Server authoritative state
|
||||
* @param serverFrame - @zh 服务器状态对应的帧号 @en Server state frame number
|
||||
* @param positionExtractor - @zh 状态位置提取器 @en State position extractor
|
||||
* @returns @zh 校正后的状态 @en Reconciled state
|
||||
*/
|
||||
reconcile(
|
||||
serverState: TState,
|
||||
serverFrame: number,
|
||||
positionExtractor: IFixedStatePositionExtractor<TState>
|
||||
): TState {
|
||||
this._lastServerState = serverState;
|
||||
this._lastAcknowledgedFrame = serverFrame;
|
||||
|
||||
while (this._pendingInputs.length > 0 && this._pendingInputs[0].frame <= serverFrame) {
|
||||
this._pendingInputs.shift();
|
||||
}
|
||||
|
||||
const localStateAtServerFrame = this._stateHistory.get(serverFrame);
|
||||
|
||||
if (localStateAtServerFrame) {
|
||||
const serverPos = positionExtractor.getPosition(serverState);
|
||||
const localPos = positionExtractor.getPosition(localStateAtServerFrame);
|
||||
const error = serverPos.sub(localPos);
|
||||
const errorMagnitude = error.length();
|
||||
|
||||
if (errorMagnitude.gt(this._config.reconciliationThreshold)) {
|
||||
if (this._config.enableSmoothReconciliation) {
|
||||
const t = Fixed32.min(
|
||||
Fixed32.ONE,
|
||||
this._config.reconciliationSpeed.mul(this._config.fixedDeltaTime)
|
||||
);
|
||||
this._correctionOffset = this._correctionOffset.add(error.mul(t));
|
||||
|
||||
const decayRate = Fixed32.from(0.9);
|
||||
this._correctionOffset = this._correctionOffset.mul(decayRate);
|
||||
} else {
|
||||
this._correctionOffset = FixedVector2.ZERO;
|
||||
}
|
||||
|
||||
let state = serverState;
|
||||
for (const inputSnapshot of this._pendingInputs) {
|
||||
state = this._predictor.predict(
|
||||
state,
|
||||
inputSnapshot.input,
|
||||
this._config.fixedDeltaTime
|
||||
);
|
||||
this._stateHistory.set(inputSnapshot.frame, state);
|
||||
}
|
||||
this._predictedState = state;
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
let state = serverState;
|
||||
for (const inputSnapshot of this._pendingInputs) {
|
||||
state = this._predictor.predict(
|
||||
state,
|
||||
inputSnapshot.input,
|
||||
this._config.fixedDeltaTime
|
||||
);
|
||||
}
|
||||
this._predictedState = state;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 回滚到指定帧并重新模拟
|
||||
* @en Rollback to specific frame and re-simulate
|
||||
*
|
||||
* @param targetFrame - @zh 目标帧号 @en Target frame number
|
||||
* @param authoritativeState - @zh 权威状态 @en Authoritative state
|
||||
* @returns @zh 重新模拟后的当前状态 @en Re-simulated current state
|
||||
*/
|
||||
rollbackAndResimulate(targetFrame: number, authoritativeState: TState): TState {
|
||||
this._stateHistory.set(targetFrame, authoritativeState);
|
||||
|
||||
let state = authoritativeState;
|
||||
const inputsToResimulate = this._pendingInputs.filter(i => i.frame > targetFrame);
|
||||
|
||||
for (const inputSnapshot of inputsToResimulate) {
|
||||
state = this._predictor.predict(
|
||||
state,
|
||||
inputSnapshot.input,
|
||||
this._config.fixedDeltaTime
|
||||
);
|
||||
this._stateHistory.set(inputSnapshot.frame, state);
|
||||
}
|
||||
|
||||
this._predictedState = state;
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取历史状态
|
||||
* @en Get historical state
|
||||
*/
|
||||
getStateAtFrame(frame: number): TState | null {
|
||||
return this._stateHistory.get(frame) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清空预测状态
|
||||
* @en Clear prediction state
|
||||
*/
|
||||
clear(): void {
|
||||
this._pendingInputs.length = 0;
|
||||
this._lastAcknowledgedFrame = 0;
|
||||
this._currentFrame = 0;
|
||||
this._lastServerState = null;
|
||||
this._predictedState = null;
|
||||
this._correctionOffset = FixedVector2.ZERO;
|
||||
this._stateHistory.clear();
|
||||
}
|
||||
|
||||
private _cleanupHistory(): void {
|
||||
if (this._stateHistory.size > this._maxHistorySize) {
|
||||
const sortedFrames = Array.from(this._stateHistory.keys()).sort((a, b) => a - b);
|
||||
const framesToRemove = sortedFrames.slice(
|
||||
0,
|
||||
this._stateHistory.size - this._maxHistorySize
|
||||
);
|
||||
for (const frame of framesToRemove) {
|
||||
this._stateHistory.delete(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建定点数客户端预测管理器
|
||||
* @en Create fixed-point client prediction manager
|
||||
*/
|
||||
export function createFixedClientPrediction<TState, TInput>(
|
||||
predictor: IFixedPredictor<TState, TInput>,
|
||||
config?: Partial<FixedClientPredictionConfig>
|
||||
): FixedClientPrediction<TState, TInput> {
|
||||
return new FixedClientPrediction(predictor, config);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 预设预测器 | Preset Predictors
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 移动输入类型
|
||||
* @en Movement input type
|
||||
*/
|
||||
export interface IFixedMovementInput {
|
||||
/**
|
||||
* @zh X方向输入 (-1, 0, 1)
|
||||
* @en X direction input (-1, 0, 1)
|
||||
*/
|
||||
readonly dx: number;
|
||||
|
||||
/**
|
||||
* @zh Y方向输入 (-1, 0, 1)
|
||||
* @en Y direction input (-1, 0, 1)
|
||||
*/
|
||||
readonly dy: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移动状态类型
|
||||
* @en Movement state type
|
||||
*/
|
||||
export interface IFixedMovementState {
|
||||
/**
|
||||
* @zh 位置
|
||||
* @en Position
|
||||
*/
|
||||
readonly position: FixedVector2;
|
||||
|
||||
/**
|
||||
* @zh 速度
|
||||
* @en Velocity
|
||||
*/
|
||||
readonly velocity: FixedVector2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建简单移动预测器
|
||||
* @en Create simple movement predictor
|
||||
*
|
||||
* @param speed - @zh 移动速度(定点数)@en Movement speed (fixed-point)
|
||||
*/
|
||||
export function createFixedMovementPredictor(
|
||||
speed: Fixed32
|
||||
): IFixedPredictor<IFixedMovementState, IFixedMovementInput> {
|
||||
return {
|
||||
predict(
|
||||
state: IFixedMovementState,
|
||||
input: IFixedMovementInput,
|
||||
deltaTime: Fixed32
|
||||
): IFixedMovementState {
|
||||
const inputVec = FixedVector2.from(input.dx, input.dy);
|
||||
const normalizedInput =
|
||||
inputVec.lengthSquared().gt(Fixed32.ZERO) ? inputVec.normalize() : inputVec;
|
||||
|
||||
const velocity = normalizedInput.mul(speed);
|
||||
const displacement = velocity.mul(deltaTime);
|
||||
const newPosition = state.position.add(displacement);
|
||||
|
||||
return {
|
||||
position: newPosition,
|
||||
velocity
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建移动状态位置提取器
|
||||
* @en Create movement state position extractor
|
||||
*/
|
||||
export function createFixedMovementPositionExtractor(): IFixedStatePositionExtractor<IFixedMovementState> {
|
||||
return {
|
||||
getPosition(state: IFixedMovementState): FixedVector2 {
|
||||
return state.position;
|
||||
}
|
||||
};
|
||||
}
|
||||
304
packages/framework/network/src/sync/fixed/FixedSnapshotBuffer.ts
Normal file
304
packages/framework/network/src/sync/fixed/FixedSnapshotBuffer.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* @zh 定点数快照缓冲区
|
||||
* @en Fixed-point Snapshot Buffer
|
||||
*
|
||||
* @zh 用于帧同步确定性计算的快照缓冲区
|
||||
* @en Snapshot buffer for deterministic lockstep calculations
|
||||
*/
|
||||
|
||||
import { Fixed32 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// =============================================================================
|
||||
// 定点数快照接口 | Fixed Snapshot Interfaces
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数状态快照
|
||||
* @en Fixed-point state snapshot
|
||||
*/
|
||||
export interface IFixedStateSnapshot<T> {
|
||||
/**
|
||||
* @zh 帧号(定点数时间戳)
|
||||
* @en Frame number (fixed-point timestamp)
|
||||
*/
|
||||
readonly frame: number;
|
||||
|
||||
/**
|
||||
* @zh 状态数据
|
||||
* @en State data
|
||||
*/
|
||||
readonly state: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定点数快照缓冲区配置
|
||||
* @en Fixed-point snapshot buffer configuration
|
||||
*/
|
||||
export interface IFixedSnapshotBufferConfig {
|
||||
/**
|
||||
* @zh 最大快照数量
|
||||
* @en Maximum snapshot count
|
||||
*/
|
||||
maxSize: number;
|
||||
|
||||
/**
|
||||
* @zh 插值延迟帧数
|
||||
* @en Interpolation delay in frames
|
||||
*/
|
||||
interpolationDelayFrames: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插值结果
|
||||
* @en Interpolation result
|
||||
*/
|
||||
export interface IFixedInterpolationResult<T> {
|
||||
/**
|
||||
* @zh 前一个快照
|
||||
* @en Previous snapshot
|
||||
*/
|
||||
readonly from: IFixedStateSnapshot<T>;
|
||||
|
||||
/**
|
||||
* @zh 后一个快照
|
||||
* @en Next snapshot
|
||||
*/
|
||||
readonly to: IFixedStateSnapshot<T>;
|
||||
|
||||
/**
|
||||
* @zh 插值因子 (0-1)
|
||||
* @en Interpolation factor (0-1)
|
||||
*/
|
||||
readonly t: Fixed32;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 定点数快照缓冲区实现 | Fixed Snapshot Buffer Implementation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数快照缓冲区
|
||||
* @en Fixed-point snapshot buffer
|
||||
*
|
||||
* @zh 使用帧号而非毫秒时间戳,确保跨平台确定性
|
||||
* @en Uses frame numbers instead of millisecond timestamps for cross-platform determinism
|
||||
*/
|
||||
export class FixedSnapshotBuffer<T> {
|
||||
private readonly _buffer: IFixedStateSnapshot<T>[] = [];
|
||||
private readonly _maxSize: number;
|
||||
private readonly _interpolationDelayFrames: number;
|
||||
|
||||
constructor(config: IFixedSnapshotBufferConfig) {
|
||||
this._maxSize = config.maxSize;
|
||||
this._interpolationDelayFrames = config.interpolationDelayFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取缓冲区大小
|
||||
* @en Get buffer size
|
||||
*/
|
||||
get size(): number {
|
||||
return this._buffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取插值延迟帧数
|
||||
* @en Get interpolation delay in frames
|
||||
*/
|
||||
get interpolationDelayFrames(): number {
|
||||
return this._interpolationDelayFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加快照
|
||||
* @en Add snapshot
|
||||
*
|
||||
* @param snapshot - @zh 状态快照 @en State snapshot
|
||||
*/
|
||||
push(snapshot: IFixedStateSnapshot<T>): void {
|
||||
let insertIndex = this._buffer.length;
|
||||
for (let i = this._buffer.length - 1; i >= 0; i--) {
|
||||
if (this._buffer[i].frame <= snapshot.frame) {
|
||||
insertIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
if (i === 0) {
|
||||
insertIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this._buffer.splice(insertIndex, 0, snapshot);
|
||||
|
||||
while (this._buffer.length > this._maxSize) {
|
||||
this._buffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 根据帧号获取插值快照
|
||||
* @en Get interpolation snapshots by frame number
|
||||
*
|
||||
* @param currentFrame - @zh 当前帧号 @en Current frame number
|
||||
* @returns @zh 插值结果(包含定点数插值因子)或 null @en Interpolation result with fixed-point factor or null
|
||||
*/
|
||||
getInterpolationSnapshots(currentFrame: number): IFixedInterpolationResult<T> | null {
|
||||
if (this._buffer.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFrame = currentFrame - this._interpolationDelayFrames;
|
||||
|
||||
for (let i = 0; i < this._buffer.length - 1; i++) {
|
||||
const prev = this._buffer[i];
|
||||
const next = this._buffer[i + 1];
|
||||
|
||||
if (prev.frame <= targetFrame && next.frame >= targetFrame) {
|
||||
const duration = next.frame - prev.frame;
|
||||
let t: Fixed32;
|
||||
if (duration > 0) {
|
||||
const elapsed = targetFrame - prev.frame;
|
||||
t = Fixed32.from(elapsed).div(Fixed32.from(duration));
|
||||
t = Fixed32.clamp(t, Fixed32.ZERO, Fixed32.ONE);
|
||||
} else {
|
||||
t = Fixed32.ZERO;
|
||||
}
|
||||
return { from: prev, to: next, t };
|
||||
}
|
||||
}
|
||||
|
||||
if (targetFrame > this._buffer[this._buffer.length - 1].frame) {
|
||||
const prev = this._buffer[this._buffer.length - 2];
|
||||
const next = this._buffer[this._buffer.length - 1];
|
||||
const duration = next.frame - prev.frame;
|
||||
let t: Fixed32;
|
||||
if (duration > 0) {
|
||||
const elapsed = targetFrame - prev.frame;
|
||||
t = Fixed32.from(elapsed).div(Fixed32.from(duration));
|
||||
t = Fixed32.min(t, Fixed32.from(2));
|
||||
} else {
|
||||
t = Fixed32.ONE;
|
||||
}
|
||||
return { from: prev, to: next, t };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 根据精确帧时间获取插值快照(支持子帧插值)
|
||||
* @en Get interpolation snapshots by precise frame time (supports sub-frame interpolation)
|
||||
*
|
||||
* @param frameTime - @zh 精确帧时间(定点数)@en Precise frame time (fixed-point)
|
||||
* @returns @zh 插值结果或 null @en Interpolation result or null
|
||||
*/
|
||||
getInterpolationSnapshotsFixed(frameTime: Fixed32): IFixedInterpolationResult<T> | null {
|
||||
if (this._buffer.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFrame = frameTime.sub(Fixed32.from(this._interpolationDelayFrames));
|
||||
|
||||
for (let i = 0; i < this._buffer.length - 1; i++) {
|
||||
const prev = this._buffer[i];
|
||||
const next = this._buffer[i + 1];
|
||||
const prevFrame = Fixed32.from(prev.frame);
|
||||
const nextFrame = Fixed32.from(next.frame);
|
||||
|
||||
if (prevFrame.le(targetFrame) && nextFrame.ge(targetFrame)) {
|
||||
const duration = nextFrame.sub(prevFrame);
|
||||
let t: Fixed32;
|
||||
if (duration.gt(Fixed32.ZERO)) {
|
||||
t = targetFrame.sub(prevFrame).div(duration);
|
||||
t = Fixed32.clamp(t, Fixed32.ZERO, Fixed32.ONE);
|
||||
} else {
|
||||
t = Fixed32.ZERO;
|
||||
}
|
||||
return { from: prev, to: next, t };
|
||||
}
|
||||
}
|
||||
|
||||
const lastFrame = Fixed32.from(this._buffer[this._buffer.length - 1].frame);
|
||||
if (targetFrame.gt(lastFrame)) {
|
||||
const prev = this._buffer[this._buffer.length - 2];
|
||||
const next = this._buffer[this._buffer.length - 1];
|
||||
const prevFrame = Fixed32.from(prev.frame);
|
||||
const nextFrame = Fixed32.from(next.frame);
|
||||
const duration = nextFrame.sub(prevFrame);
|
||||
let t: Fixed32;
|
||||
if (duration.gt(Fixed32.ZERO)) {
|
||||
t = targetFrame.sub(prevFrame).div(duration);
|
||||
t = Fixed32.min(t, Fixed32.from(2));
|
||||
} else {
|
||||
t = Fixed32.ONE;
|
||||
}
|
||||
return { from: prev, to: next, t };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取最新快照
|
||||
* @en Get latest snapshot
|
||||
*/
|
||||
getLatest(): IFixedStateSnapshot<T> | null {
|
||||
return this._buffer.length > 0 ? this._buffer[this._buffer.length - 1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取特定帧号的快照
|
||||
* @en Get snapshot at specific frame
|
||||
*/
|
||||
getAtFrame(frame: number): IFixedStateSnapshot<T> | null {
|
||||
for (const snapshot of this._buffer) {
|
||||
if (snapshot.frame === frame) {
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取特定帧号之后的所有快照
|
||||
* @en Get all snapshots after specific frame
|
||||
*/
|
||||
getSnapshotsAfter(frame: number): IFixedStateSnapshot<T>[] {
|
||||
return this._buffer.filter(s => s.frame > frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除指定帧号之前的所有快照
|
||||
* @en Remove all snapshots before specific frame
|
||||
*/
|
||||
removeSnapshotsBefore(frame: number): void {
|
||||
while (this._buffer.length > 0 && this._buffer[0].frame < frame) {
|
||||
this._buffer.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清空缓冲区
|
||||
* @en Clear buffer
|
||||
*/
|
||||
clear(): void {
|
||||
this._buffer.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建定点数快照缓冲区
|
||||
* @en Create fixed-point snapshot buffer
|
||||
*
|
||||
* @param maxSize - @zh 最大快照数量(默认 30)@en Maximum snapshot count (default 30)
|
||||
* @param interpolationDelayFrames - @zh 插值延迟帧数(默认 2)@en Interpolation delay frames (default 2)
|
||||
*/
|
||||
export function createFixedSnapshotBuffer<T>(
|
||||
maxSize: number = 30,
|
||||
interpolationDelayFrames: number = 2
|
||||
): FixedSnapshotBuffer<T> {
|
||||
return new FixedSnapshotBuffer<T>({ maxSize, interpolationDelayFrames });
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* @zh 定点数变换插值器
|
||||
* @en Fixed-point Transform Interpolator
|
||||
*
|
||||
* @zh 用于帧同步确定性计算的插值器
|
||||
* @en Interpolator for deterministic lockstep calculations
|
||||
*/
|
||||
|
||||
import { Fixed32, FixedVector2, FixedMath } from '@esengine/ecs-framework-math';
|
||||
import {
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
type IFixedTransformStateRaw,
|
||||
type IFixedTransformStateWithVelocityRaw
|
||||
} from './FixedTransformState';
|
||||
|
||||
// =============================================================================
|
||||
// 插值器接口 | Interpolator Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数插值器接口
|
||||
* @en Fixed-point interpolator interface
|
||||
*/
|
||||
export interface IFixedInterpolator<T> {
|
||||
/**
|
||||
* @zh 在两个状态之间插值
|
||||
* @en Interpolate between two states
|
||||
* @param from - @zh 起始状态 @en Start state
|
||||
* @param to - @zh 结束状态 @en End state
|
||||
* @param t - @zh 插值因子 (0-1) @en Interpolation factor (0-1)
|
||||
*/
|
||||
interpolate(from: T, to: T, t: Fixed32): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 定点数外推器接口
|
||||
* @en Fixed-point extrapolator interface
|
||||
*/
|
||||
export interface IFixedExtrapolator<T> {
|
||||
/**
|
||||
* @zh 基于速度外推状态
|
||||
* @en Extrapolate state based on velocity
|
||||
* @param state - @zh 当前状态 @en Current state
|
||||
* @param deltaTime - @zh 时间增量 @en Time delta
|
||||
*/
|
||||
extrapolate(state: T, deltaTime: Fixed32): T;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 定点数变换插值器 | Fixed Transform Interpolator
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数变换状态插值器
|
||||
* @en Fixed-point transform state interpolator
|
||||
*/
|
||||
export class FixedTransformInterpolator
|
||||
implements IFixedInterpolator<FixedTransformState>, IFixedExtrapolator<FixedTransformStateWithVelocity> {
|
||||
|
||||
/**
|
||||
* @zh 在两个变换状态之间插值
|
||||
* @en Interpolate between two transform states
|
||||
*/
|
||||
interpolate(from: FixedTransformState, to: FixedTransformState, t: Fixed32): FixedTransformState {
|
||||
return new FixedTransformState(
|
||||
from.position.lerp(to.position, t),
|
||||
FixedMath.lerpAngle(from.rotation, to.rotation, t)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 基于速度外推变换状态
|
||||
* @en Extrapolate transform state based on velocity
|
||||
*/
|
||||
extrapolate(
|
||||
state: FixedTransformStateWithVelocity,
|
||||
deltaTime: Fixed32
|
||||
): FixedTransformStateWithVelocity {
|
||||
return new FixedTransformStateWithVelocity(
|
||||
state.position.add(state.velocity.mul(deltaTime)),
|
||||
state.rotation.add(state.angularVelocity.mul(deltaTime)),
|
||||
state.velocity,
|
||||
state.angularVelocity
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 使用原始值进行插值
|
||||
* @en Interpolate using raw values
|
||||
*/
|
||||
interpolateRaw(
|
||||
from: IFixedTransformStateRaw,
|
||||
to: IFixedTransformStateRaw,
|
||||
t: number
|
||||
): IFixedTransformStateRaw {
|
||||
const fromState = FixedTransformState.fromRaw(from);
|
||||
const toState = FixedTransformState.fromRaw(to);
|
||||
const tFixed = Fixed32.from(t);
|
||||
return this.interpolate(fromState, toState, tFixed).toRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 使用原始值进行外推
|
||||
* @en Extrapolate using raw values
|
||||
*/
|
||||
extrapolateRaw(
|
||||
state: IFixedTransformStateWithVelocityRaw,
|
||||
deltaTimeMs: number
|
||||
): IFixedTransformStateWithVelocityRaw {
|
||||
const fixedState = FixedTransformStateWithVelocity.fromRaw(state);
|
||||
const deltaTime = Fixed32.from(deltaTimeMs / 1000); // ms to seconds
|
||||
return this.extrapolate(fixedState, deltaTime).toRaw();
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 赫尔米特插值器 | Hermite Interpolator
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数赫尔米特变换插值器(更平滑的曲线)
|
||||
* @en Fixed-point Hermite transform interpolator (smoother curves)
|
||||
*/
|
||||
export class FixedHermiteTransformInterpolator
|
||||
implements IFixedInterpolator<FixedTransformStateWithVelocity> {
|
||||
|
||||
/**
|
||||
* @zh 快照间隔时间(秒)
|
||||
* @en Snapshot interval in seconds
|
||||
*/
|
||||
private readonly snapshotInterval: Fixed32;
|
||||
|
||||
constructor(snapshotIntervalMs: number = 100) {
|
||||
this.snapshotInterval = Fixed32.from(snapshotIntervalMs / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 使用赫尔米特插值
|
||||
* @en Use Hermite interpolation
|
||||
*/
|
||||
interpolate(
|
||||
from: FixedTransformStateWithVelocity,
|
||||
to: FixedTransformStateWithVelocity,
|
||||
t: Fixed32
|
||||
): FixedTransformStateWithVelocity {
|
||||
const t2 = t.mul(t);
|
||||
const t3 = t2.mul(t);
|
||||
|
||||
const two = Fixed32.from(2);
|
||||
const three = Fixed32.from(3);
|
||||
const six = Fixed32.from(6);
|
||||
const four = Fixed32.from(4);
|
||||
|
||||
// Hermite basis functions
|
||||
// h00 = 2t³ - 3t² + 1
|
||||
const h00 = two.mul(t3).sub(three.mul(t2)).add(Fixed32.ONE);
|
||||
// h10 = t³ - 2t² + t
|
||||
const h10 = t3.sub(two.mul(t2)).add(t);
|
||||
// h01 = -2t³ + 3t²
|
||||
const h01 = two.neg().mul(t3).add(three.mul(t2));
|
||||
// h11 = t³ - t²
|
||||
const h11 = t3.sub(t2);
|
||||
|
||||
const dt = this.snapshotInterval;
|
||||
|
||||
// Position interpolation
|
||||
const x = h00.mul(from.position.x)
|
||||
.add(h10.mul(from.velocity.x).mul(dt))
|
||||
.add(h01.mul(to.position.x))
|
||||
.add(h11.mul(to.velocity.x).mul(dt));
|
||||
|
||||
const y = h00.mul(from.position.y)
|
||||
.add(h10.mul(from.velocity.y).mul(dt))
|
||||
.add(h01.mul(to.position.y))
|
||||
.add(h11.mul(to.velocity.y).mul(dt));
|
||||
|
||||
// Velocity derivatives
|
||||
// dh00 = 6t² - 6t
|
||||
const dh00 = six.mul(t2).sub(six.mul(t));
|
||||
// dh10 = 3t² - 4t + 1
|
||||
const dh10 = three.mul(t2).sub(four.mul(t)).add(Fixed32.ONE);
|
||||
// dh01 = -6t² + 6t
|
||||
const dh01 = six.neg().mul(t2).add(six.mul(t));
|
||||
// dh11 = 3t² - 2t
|
||||
const dh11 = three.mul(t2).sub(two.mul(t));
|
||||
|
||||
const velocityX = dh00.mul(from.position.x)
|
||||
.add(dh10.mul(from.velocity.x).mul(dt))
|
||||
.add(dh01.mul(to.position.x))
|
||||
.add(dh11.mul(to.velocity.x).mul(dt))
|
||||
.div(dt);
|
||||
|
||||
const velocityY = dh00.mul(from.position.y)
|
||||
.add(dh10.mul(from.velocity.y).mul(dt))
|
||||
.add(dh01.mul(to.position.y))
|
||||
.add(dh11.mul(to.velocity.y).mul(dt))
|
||||
.div(dt);
|
||||
|
||||
return new FixedTransformStateWithVelocity(
|
||||
new FixedVector2(x, y),
|
||||
FixedMath.lerpAngle(from.rotation, to.rotation, t),
|
||||
new FixedVector2(velocityX, velocityY),
|
||||
Fixed32.lerp(from.angularVelocity, to.angularVelocity, t)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建定点数变换插值器
|
||||
* @en Create fixed-point transform interpolator
|
||||
*/
|
||||
export function createFixedTransformInterpolator(): FixedTransformInterpolator {
|
||||
return new FixedTransformInterpolator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建定点数赫尔米特变换插值器
|
||||
* @en Create fixed-point Hermite transform interpolator
|
||||
*/
|
||||
export function createFixedHermiteTransformInterpolator(
|
||||
snapshotIntervalMs?: number
|
||||
): FixedHermiteTransformInterpolator {
|
||||
return new FixedHermiteTransformInterpolator(snapshotIntervalMs);
|
||||
}
|
||||
265
packages/framework/network/src/sync/fixed/FixedTransformState.ts
Normal file
265
packages/framework/network/src/sync/fixed/FixedTransformState.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @zh 定点数变换状态
|
||||
* @en Fixed-point Transform State
|
||||
*
|
||||
* @zh 用于帧同步确定性计算的变换状态
|
||||
* @en Transform state for deterministic lockstep calculations
|
||||
*/
|
||||
|
||||
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// =============================================================================
|
||||
// 定点数变换状态接口 | Fixed Transform State Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数变换状态(原始值)
|
||||
* @en Fixed-point transform state (raw values)
|
||||
*
|
||||
* @zh 用于网络传输的原始整数格式,确保跨平台一致性
|
||||
* @en Raw integer format for network transmission, ensures cross-platform consistency
|
||||
*/
|
||||
export interface IFixedTransformStateRaw {
|
||||
/**
|
||||
* @zh X 坐标原始值
|
||||
* @en X coordinate raw value
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* @zh Y 坐标原始值
|
||||
* @en Y coordinate raw value
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* @zh 旋转角度原始值(弧度 * 65536)
|
||||
* @en Rotation raw value (radians * 65536)
|
||||
*/
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 带速度的定点数变换状态(原始值)
|
||||
* @en Fixed-point transform state with velocity (raw values)
|
||||
*/
|
||||
export interface IFixedTransformStateWithVelocityRaw extends IFixedTransformStateRaw {
|
||||
/**
|
||||
* @zh X 速度原始值
|
||||
* @en X velocity raw value
|
||||
*/
|
||||
velocityX: number;
|
||||
|
||||
/**
|
||||
* @zh Y 速度原始值
|
||||
* @en Y velocity raw value
|
||||
*/
|
||||
velocityY: number;
|
||||
|
||||
/**
|
||||
* @zh 角速度原始值
|
||||
* @en Angular velocity raw value
|
||||
*/
|
||||
angularVelocity: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 定点数变换状态类 | Fixed Transform State Class
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 定点数变换状态
|
||||
* @en Fixed-point transform state
|
||||
*/
|
||||
export class FixedTransformState {
|
||||
readonly position: FixedVector2;
|
||||
readonly rotation: Fixed32;
|
||||
|
||||
constructor(position: FixedVector2, rotation: Fixed32) {
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从原始值创建
|
||||
* @en Create from raw values
|
||||
*/
|
||||
static fromRaw(raw: IFixedTransformStateRaw): FixedTransformState {
|
||||
return new FixedTransformState(
|
||||
FixedVector2.fromRaw(raw.x, raw.y),
|
||||
Fixed32.fromRaw(raw.rotation)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从浮点数创建
|
||||
* @en Create from floating-point numbers
|
||||
*/
|
||||
static from(x: number, y: number, rotation: number): FixedTransformState {
|
||||
return new FixedTransformState(
|
||||
FixedVector2.from(x, y),
|
||||
Fixed32.from(rotation)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为原始值(用于网络传输)
|
||||
* @en Convert to raw values (for network transmission)
|
||||
*/
|
||||
toRaw(): IFixedTransformStateRaw {
|
||||
return {
|
||||
x: this.position.x.toRaw(),
|
||||
y: this.position.y.toRaw(),
|
||||
rotation: this.rotation.toRaw()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为浮点数对象(用于渲染)
|
||||
* @en Convert to floating-point object (for rendering)
|
||||
*/
|
||||
toFloat(): { x: number; y: number; rotation: number } {
|
||||
return {
|
||||
x: this.position.x.toNumber(),
|
||||
y: this.position.y.toNumber(),
|
||||
rotation: this.rotation.toNumber()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否相等
|
||||
* @en Check equality
|
||||
*/
|
||||
equals(other: FixedTransformState): boolean {
|
||||
return this.position.equals(other.position) && this.rotation.eq(other.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 带速度的定点数变换状态
|
||||
* @en Fixed-point transform state with velocity
|
||||
*/
|
||||
export class FixedTransformStateWithVelocity {
|
||||
readonly position: FixedVector2;
|
||||
readonly rotation: Fixed32;
|
||||
readonly velocity: FixedVector2;
|
||||
readonly angularVelocity: Fixed32;
|
||||
|
||||
constructor(
|
||||
position: FixedVector2,
|
||||
rotation: Fixed32,
|
||||
velocity: FixedVector2,
|
||||
angularVelocity: Fixed32
|
||||
) {
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.velocity = velocity;
|
||||
this.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从原始值创建
|
||||
* @en Create from raw values
|
||||
*/
|
||||
static fromRaw(raw: IFixedTransformStateWithVelocityRaw): FixedTransformStateWithVelocity {
|
||||
return new FixedTransformStateWithVelocity(
|
||||
FixedVector2.fromRaw(raw.x, raw.y),
|
||||
Fixed32.fromRaw(raw.rotation),
|
||||
FixedVector2.fromRaw(raw.velocityX, raw.velocityY),
|
||||
Fixed32.fromRaw(raw.angularVelocity)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从浮点数创建
|
||||
* @en Create from floating-point numbers
|
||||
*/
|
||||
static from(
|
||||
x: number,
|
||||
y: number,
|
||||
rotation: number,
|
||||
velocityX: number,
|
||||
velocityY: number,
|
||||
angularVelocity: number
|
||||
): FixedTransformStateWithVelocity {
|
||||
return new FixedTransformStateWithVelocity(
|
||||
FixedVector2.from(x, y),
|
||||
Fixed32.from(rotation),
|
||||
FixedVector2.from(velocityX, velocityY),
|
||||
Fixed32.from(angularVelocity)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为原始值
|
||||
* @en Convert to raw values
|
||||
*/
|
||||
toRaw(): IFixedTransformStateWithVelocityRaw {
|
||||
return {
|
||||
x: this.position.x.toRaw(),
|
||||
y: this.position.y.toRaw(),
|
||||
rotation: this.rotation.toRaw(),
|
||||
velocityX: this.velocity.x.toRaw(),
|
||||
velocityY: this.velocity.y.toRaw(),
|
||||
angularVelocity: this.angularVelocity.toRaw()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 转换为浮点数对象
|
||||
* @en Convert to floating-point object
|
||||
*/
|
||||
toFloat(): {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
velocityX: number;
|
||||
velocityY: number;
|
||||
angularVelocity: number;
|
||||
} {
|
||||
return {
|
||||
x: this.position.x.toNumber(),
|
||||
y: this.position.y.toNumber(),
|
||||
rotation: this.rotation.toNumber(),
|
||||
velocityX: this.velocity.x.toNumber(),
|
||||
velocityY: this.velocity.y.toNumber(),
|
||||
angularVelocity: this.angularVelocity.toNumber()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否相等
|
||||
* @en Check equality
|
||||
*/
|
||||
equals(other: FixedTransformStateWithVelocity): boolean {
|
||||
return this.position.equals(other.position) &&
|
||||
this.rotation.eq(other.rotation) &&
|
||||
this.velocity.equals(other.velocity) &&
|
||||
this.angularVelocity.eq(other.angularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工具函数 | Utility Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建零状态
|
||||
* @en Create zero state
|
||||
*/
|
||||
export function createZeroFixedTransformState(): FixedTransformState {
|
||||
return new FixedTransformState(FixedVector2.ZERO, Fixed32.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建带速度的零状态
|
||||
* @en Create zero state with velocity
|
||||
*/
|
||||
export function createZeroFixedTransformStateWithVelocity(): FixedTransformStateWithVelocity {
|
||||
return new FixedTransformStateWithVelocity(
|
||||
FixedVector2.ZERO,
|
||||
Fixed32.ZERO,
|
||||
FixedVector2.ZERO,
|
||||
Fixed32.ZERO
|
||||
);
|
||||
}
|
||||
63
packages/framework/network/src/sync/fixed/index.ts
Normal file
63
packages/framework/network/src/sync/fixed/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @zh 定点数网络同步模块
|
||||
* @en Fixed-point network sync module
|
||||
*
|
||||
* @zh 用于帧同步确定性计算的网络同步类型和工具
|
||||
* @en Network sync types and utilities for deterministic lockstep calculations
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 变换状态 | Transform State
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
createZeroFixedTransformState,
|
||||
createZeroFixedTransformStateWithVelocity,
|
||||
type IFixedTransformStateRaw,
|
||||
type IFixedTransformStateWithVelocityRaw,
|
||||
} from './FixedTransformState';
|
||||
|
||||
// =============================================================================
|
||||
// 插值器 | Interpolators
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
FixedTransformInterpolator,
|
||||
FixedHermiteTransformInterpolator,
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator,
|
||||
type IFixedInterpolator,
|
||||
type IFixedExtrapolator,
|
||||
} from './FixedTransformInterpolator';
|
||||
|
||||
// =============================================================================
|
||||
// 快照缓冲区 | Snapshot Buffer
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
FixedSnapshotBuffer,
|
||||
createFixedSnapshotBuffer,
|
||||
type IFixedStateSnapshot,
|
||||
type IFixedSnapshotBufferConfig,
|
||||
type IFixedInterpolationResult,
|
||||
} from './FixedSnapshotBuffer';
|
||||
|
||||
// =============================================================================
|
||||
// 客户端预测 | Client Prediction
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
FixedClientPrediction,
|
||||
createFixedClientPrediction,
|
||||
createFixedMovementPredictor,
|
||||
createFixedMovementPositionExtractor,
|
||||
type IFixedInputSnapshot,
|
||||
type IFixedPredictedState,
|
||||
type IFixedPredictor,
|
||||
type IFixedStatePositionExtractor,
|
||||
type FixedClientPredictionConfig,
|
||||
type IFixedMovementInput,
|
||||
type IFixedMovementState,
|
||||
} from './FixedClientPrediction';
|
||||
@@ -78,3 +78,49 @@ export {
|
||||
ComponentSyncSystem,
|
||||
createComponentSyncSystem
|
||||
} from './ComponentSync';
|
||||
|
||||
// =============================================================================
|
||||
// 定点数同步 | Fixed-point Sync (Deterministic Lockstep)
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
// Transform State
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
createZeroFixedTransformState,
|
||||
createZeroFixedTransformStateWithVelocity,
|
||||
// Interpolators
|
||||
FixedTransformInterpolator,
|
||||
FixedHermiteTransformInterpolator,
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator,
|
||||
// Snapshot Buffer
|
||||
FixedSnapshotBuffer,
|
||||
createFixedSnapshotBuffer,
|
||||
// Client Prediction
|
||||
FixedClientPrediction,
|
||||
createFixedClientPrediction,
|
||||
createFixedMovementPredictor,
|
||||
createFixedMovementPositionExtractor,
|
||||
} from './fixed';
|
||||
|
||||
export type {
|
||||
// Transform State Types
|
||||
IFixedTransformStateRaw,
|
||||
IFixedTransformStateWithVelocityRaw,
|
||||
// Interpolator Types
|
||||
IFixedInterpolator,
|
||||
IFixedExtrapolator,
|
||||
// Snapshot Buffer Types
|
||||
IFixedStateSnapshot,
|
||||
IFixedSnapshotBufferConfig,
|
||||
IFixedInterpolationResult,
|
||||
// Client Prediction Types
|
||||
IFixedInputSnapshot,
|
||||
IFixedPredictedState,
|
||||
IFixedPredictor,
|
||||
IFixedStatePositionExtractor,
|
||||
FixedClientPredictionConfig,
|
||||
IFixedMovementInput,
|
||||
IFixedMovementState,
|
||||
} from './fixed';
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# @esengine/pathfinding
|
||||
|
||||
## 12.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e66bd8`](https://github.com/esengine/esengine/commit/4e66bd8e2be80b366a7723dcc48b99df0457aed4)]:
|
||||
- @esengine/blueprint@4.5.0
|
||||
- @esengine/ecs-framework-math@2.10.1
|
||||
|
||||
## 11.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fa593a3`](https://github.com/esengine/esengine/commit/fa593a3c69292207800750f8106f418465cb7c0f)]:
|
||||
- @esengine/ecs-framework-math@2.10.0
|
||||
|
||||
## 10.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`bffe90b`](https://github.com/esengine/esengine/commit/bffe90b6a17563cc90709faf339b229dc3abd22d)]:
|
||||
- @esengine/ecs-framework-math@2.9.0
|
||||
|
||||
## 9.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`30173f0`](https://github.com/esengine/esengine/commit/30173f076415c9770a429b236b8bab95a2fdc498)]:
|
||||
- @esengine/ecs-framework-math@2.8.0
|
||||
|
||||
## 8.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`0d33cf0`](https://github.com/esengine/esengine/commit/0d33cf00977d16e6282931aba2cf771ec2c84c6b)]:
|
||||
- @esengine/blueprint@4.4.0
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
|
||||
- @esengine/blueprint@4.3.0
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user