Compare commits

...

23 Commits

Author SHA1 Message Date
YHH
b6aef13d93 Merge pull request #449 from esengine/docs/update-cocos-node-editor-v1.2.0
docs: update Cocos Node Editor download link to v1.2.0
2026-01-06 18:37:52 +08:00
yhh
470abb8750 Merge master into docs/update-cocos-node-editor-v1.2.0 2026-01-06 18:36:28 +08:00
yhh
2f95758911 docs: update Cocos Node Editor download link to v1.2.0 2026-01-06 18:35:24 +08:00
YHH
2e9f36b656 Merge pull request #448 from esengine/changeset-release/master
chore: release packages
2026-01-06 18:27:33 +08:00
github-actions[bot]
c188a36f2b chore: release packages 2026-01-06 10:26:20 +00:00
YHH
50681553b5 Merge pull request #447 from esengine/feat/blueprint-schema-system
feat(blueprint): add Schema type system and @BlueprintArray decorator
2026-01-06 18:24:26 +08:00
yhh
4e66bd8e2b feat(blueprint): add Schema type system and @BlueprintArray decorator
- Add Schema fluent API for defining complex data types
- Add @BlueprintArray decorator for array properties with itemSchema
- Support primitive types: float, int, string, boolean, vector2, vector3
- Support composite types: object, array, enum, ref
- Add path-utils for nested property access
- Update documentation with examples and usage guide
2026-01-06 18:19:08 +08:00
YHH
7caa69a22e Merge pull request #445 from esengine/changeset-release/master
chore: release packages
2026-01-06 11:32:03 +08:00
YHH
5a5daf7565 Merge branch 'master' into changeset-release/master 2026-01-06 11:28:27 +08:00
YHH
3415737fcc Merge pull request #446 from esengine/fix/cocos-editor-install-docs
docs(blueprint): fix Cocos extension installation instructions
2026-01-06 11:27:18 +08:00
yhh
876312deb2 docs(blueprint): fix Cocos extension installation instructions
- Change from manual copy to zip import via Extension Manager
- Add first-launch dependency installation flow description
- Plugin auto-detects and prompts to install @esengine/blueprint
2026-01-06 11:25:43 +08:00
github-actions[bot]
a790fc9e92 chore: release packages 2026-01-06 03:11:07 +00:00
YHH
fa593a3c69 docs(blueprint): fix Cocos editor integration guide (#444)
* feat(math): add blueprint nodes for math library

- Add Vector2 blueprint nodes (Make, Break, arithmetic, Length, Normalize, Dot, Cross, Distance, Lerp, Rotate, FromAngle)
- Add Fixed32 blueprint nodes (conversions, arithmetic, math functions, comparison)
- Add FixedVector2 blueprint nodes (Make, Break, arithmetic, vector operations)
- Add Color blueprint nodes (Make, Break, conversions, color manipulation, constants)
- Add documentation with visual examples for all math blueprint nodes
- Update sidebar navigation to include math module

* fix(ci): adjust build order - blueprint before math

math package now depends on blueprint, so blueprint must be built first

* docs(blueprint): fix Cocos editor integration guide

- Remove redundant component/system implementations
- Users should use BlueprintComponent and BlueprintSystem from @esengine/blueprint
- Add BlueprintComponent properties and methods reference table
2026-01-06 11:09:18 +08:00
github-actions[bot]
e7d95dfdaf chore: release packages (#443)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-06 10:34:07 +08:00
YHH
bffe90b6a1 feat(math): add blueprint nodes for math library (#442)
* feat(math): add blueprint nodes for math library

- Add Vector2 blueprint nodes (Make, Break, arithmetic, Length, Normalize, Dot, Cross, Distance, Lerp, Rotate, FromAngle)
- Add Fixed32 blueprint nodes (conversions, arithmetic, math functions, comparison)
- Add FixedVector2 blueprint nodes (Make, Break, arithmetic, vector operations)
- Add Color blueprint nodes (Make, Break, conversions, color manipulation, constants)
- Add documentation with visual examples for all math blueprint nodes
- Update sidebar navigation to include math module

* fix(ci): adjust build order - blueprint before math

math package now depends on blueprint, so blueprint must be built first
2026-01-06 10:32:02 +08:00
github-actions[bot]
e90a42b1c9 chore: release packages (#441)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-05 22:20:23 +08:00
YHH
30173f0764 feat: add fixed-point math and network sync, fix docs links (#440)
- feat(math): add Fixed32, FixedMath, FixedVector2 for deterministic calculations
- feat(network): add FixedSnapshotBuffer and FixedClientPrediction for lockstep sync
- docs: fix relative links in behavior-tree, blueprint, guide docs
- docs: add missing sidebar items (cocos-editor, distributed)
- docs: add scene-manager and persistent-entity Chinese translations
2026-01-05 22:17:30 +08:00
yhh
12da6bd609 chore: update pnpm-lock.yaml for rehype-raw 2026-01-05 19:15:44 +08:00
yhh
6b5b4efa72 fix(docs): enable raw HTML in markdown with rehype-raw
- Add rehype-raw plugin to allow HTML elements in markdown
- Remove inline script tag from nodes.md (loaded globally in Head.astro)
- This fixes blueprint graph examples not rendering in production
2026-01-05 19:12:47 +08:00
yhh
51334dfc50 fix(docs): use is:inline for blueprint-graph.js script 2026-01-05 19:00:10 +08:00
yhh
2035355e22 fix(docs): load blueprint-graph.js on all pages 2026-01-05 18:48:22 +08:00
yhh
9e5f037d5d docs(blueprint): improve graph visualization with auto-layout
- Add blueprint-graph.js for automatic node layout
- Use weighted graph algorithm for better node positioning
- Add drag-to-scroll functionality for large graphs
- Update CSS to support scrollable graph containers
- Sync Chinese and English docs for Example 2 (Health System)
- Add explanation for custom Event OnDamage node
2026-01-05 18:35:42 +08:00
yhh
43be62b4cb docs(blueprint): update editor download links to v1.1.0
- Remove beta/activation code notices (now free)
- Update download links to v1.1.0 release
- Add QQ group and website info
2026-01-05 13:12:43 +08:00
90 changed files with 13665 additions and 811 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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' },
@@ -233,6 +239,7 @@ export default defineConfig({
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' } },
@@ -240,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' },
@@ -271,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' } },

View File

@@ -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"

View 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();
}
})();

View File

@@ -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>

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View 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

View File

@@ -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' };
}
}
```

View File

@@ -3,18 +3,19 @@ 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
> **Beta Testing**: The blueprint editor is currently in beta. An activation code is required.
> Please join QQ Group **481923584** and message the group owner to get your activation code.
Download the latest version from GitHub Release (Free):
Download the latest version from GitHub Release:
**[Download Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
**[Download Cocos Node Editor v1.0.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.0.0)**
> QQ Group: **481923584** | Website: [esengine.cn](https://esengine.cn/)
### Installation Steps
@@ -346,50 +347,244 @@ Blueprints are saved as JSON, compatible with `@esengine/blueprint` runtime:
Move entity every frame:
```
[Event Tick] ─Exec─→ [Get Self] ─Entity─→ [Get Component: Transform]
[Get Delta Time] ▼
│ [Set Property: x]
│ │
[Multiply] ◄──────────────┘
└─ Speed: 100
```
<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:
Check death after taking damage. `Event OnDamage` is a custom event node that can be triggered from code via `vm.triggerCustomEvent('OnDamage', { damage: 50 })`:
```
[On Damage Event] ─→ [Get Component: Health] ─→ [Get Property: current]
[Subtract]
[Set Property: current]
┌─ True ─→ [Destroy Self]
[Branch]─┤
└─ False ─→ (continue)
[Less Or Equal]
current <= 0
```
<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:
```
[Event BeginPlay] ─→ [Do N Times] ─Loop─→ [Delay: 2.0] ─→ [Create Entity: Enemy]
└─ N: 10
```
<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
@@ -409,7 +604,7 @@ Use **Print** nodes to output variable values to the console.
## Next Steps
- [ECS Node Reference](./nodes) - Complete node list
- [Custom Nodes](./custom-nodes) - Create custom nodes
- [Runtime Integration](./vm) - Blueprint VM API
- [Examples](./examples) - More game logic examples
- [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

View File

@@ -7,12 +7,11 @@ description: "Visual scripting system deeply integrated with ECS framework"
## Editor Download
> **Beta Testing**: The blueprint editor is currently in beta. An activation code is required.
> Please join QQ Group **481923584** and message the group owner to get your activation code.
Blueprint Editor Plugin for Cocos Creator (Free):
Blueprint Editor Plugin for Cocos Creator:
**[Download Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
**[Download Cocos Node Editor v1.0.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.0.0)**
> QQ Group: **481923584** | Website: [esengine.cn](https://esengine.cn/)
For detailed usage instructions, see [Editor User Guide](./editor-guide).

View File

@@ -1,192 +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:
### 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 arithmetic |
| `Modulo` | Modulo operation (%) |
| `Negate` | Negate value |
| `Abs` | Absolute value |
| `Sign` | Sign (+1, 0, -1) |
| `Min` / `Max` | Minimum/Maximum |
| `Clamp` | Clamp to range |
| `Wrap` | Wrap value to range |
| `Sin` | Sine |
| `Cos` | Cosine |
| `Tan` | Tangent |
| `Asin` | Arc sine |
| `Acos` | Arc cosine |
| `Atan` | Arc tangent |
| `Atan2` | Two-argument arc tangent |
Power & Roots:
### Random Numbers
| Node | Description |
|------|-------------|
| `Power` | Power (A^B) |
| `Sqrt` | Square root |
| 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 |
Rounding:
### Comparison Nodes
| Node | Description |
|------|-------------|
| `Floor` | Round down |
| `Ceil` | Round up |
| `Round` | Round to nearest |
| 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 |
Trigonometry:
### Extended Math Nodes
| Node | Description |
|------|-------------|
| `Sin` / `Cos` / `Tan` | Sine/Cosine/Tangent |
| `Asin` / `Acos` / `Atan` | Inverse trig functions |
| `Atan2` | Two-argument arctangent |
| `DegToRad` / `RadToDeg` | Degree/Radian conversion |
> **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)
Interpolation:
### Example: Clamp Value
| Node | Description |
|------|-------------|
| `Lerp` | Linear interpolation |
| `InverseLerp` | Inverse linear interpolation |
<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>
Random:
## Variable Nodes
| Node | Description |
|------|-------------|
| `Random Range` | Random float in range |
| `Random Int` | Random integer in range |
Blueprint-defined variables automatically generate Get and Set nodes:
## Logic Nodes
| Node | Description | Type |
|------|-------------|------|
| `Get <varname>` | Read variable value | Pure |
| `Set <varname>` | Set variable value | Exec |
Comparison:
### Example: Counter
| Node | Description |
|------|-------------|
| `Equal` | Equal (==) |
| `Not Equal` | Not equal (!=) |
| `Greater Than` | Greater than (>) |
| `Greater Or Equal` | Greater than or equal (>=) |
| `Less Than` | Less than (<) |
| `Less Or Equal` | Less than or equal (<=) |
| `In Range` | Check if value is in range |
Logical Operations:
| Node | Description |
|------|-------------|
| `AND` | Logical AND |
| `OR` | Logical OR |
| `NOT` | Logical NOT |
| `XOR` | Exclusive OR |
| `NAND` | NOT AND |
Utility:
| Node | Description |
|------|-------------|
| `Is Null` | Check if value is null |
| `Select` | Choose A or B based on condition (ternary) |
<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

View 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

View 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);
```

View 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

View File

@@ -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';
```

View File

@@ -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';
```

View File

@@ -434,6 +434,6 @@ const found = hierarchySystem.findChild(parent, "Child");
## 下一步
- 了解 [实体类](./entity/) 的其他功能
- 了解 [场景管理](./scene/) 如何组织实体和系统
- 了解 [组件系统](./component/) 如何定义和使用组件
- 了解 [实体类](/guide/entity/) 的其他功能
- 了解 [场景管理](/guide/scene/) 如何组织实体和系统
- 了解 [组件系统](/guide/component/) 如何定义和使用组件

View 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/) - 了解场景切换

View File

@@ -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 服务器环境提供支持,适用于游戏服务器和计算服务器。

View 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 的静态方法,你可以轻松管理场景切换。

View File

@@ -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/)学习可视化编辑

View File

@@ -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/)优化你的行为树设计

View File

@@ -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/)深入理解原理

View File

@@ -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/)创建自定义行为

View File

@@ -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/)学习设计模式

View File

@@ -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/)了解更多功能

View File

@@ -117,5 +117,5 @@ BehaviorTreeStarter.start(entity, tree);
## 下一步
- 查看[编辑器工作流](./editor-workflow/)了解完整的开发流程
- 查看[自定义节点执行器](./custom-actions/)学习如何扩展节点
- 查看[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解完整的开发流程
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何扩展节点

View File

@@ -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设计

View File

@@ -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/)学习如何创建。

View File

@@ -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/)

View File

@@ -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

View 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) - 内置节点参考

View File

@@ -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' };
}
}
```

View File

@@ -3,18 +3,19 @@ title: "蓝图编辑器使用指南"
description: "Cocos Creator 蓝图可视化脚本编辑器完整使用教程"
---
<script src="/js/blueprint-graph.js"></script>
本指南介绍如何在 Cocos Creator 中使用蓝图可视化脚本编辑器。
## 下载与安装
### 下载
> **内测中**:蓝图编辑器目前处于内测阶段,需要激活码才能使用。
> 请加入 QQ 群 **481923584** 后私聊群主获取激活码。
从 GitHub Release 下载最新版本(免费):
从 GitHub Release 下载最新版本:
**[下载 Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
**[下载 Cocos Node Editor v1.0.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.0.0)**
> 技术交流 QQ 群:**481923584** | 官网:[esengine.cn](https://esengine.cn/)
### 安装步骤
@@ -94,10 +95,37 @@ your-project/
| **Event EndPlay** | 蓝图停止时 | Exec |
**示例:游戏开始时打印消息**
```
[Event BeginPlay] ──Exec──→ [Print]
└─ Message: "游戏开始!"
```
<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)
@@ -115,10 +143,52 @@ your-project/
| **Set Active** | 设置激活状态 | Exec, Entity, Active | Exec |
**示例:创建新实体**
```
[Event BeginPlay] ──→ [Create Entity] ──→ [Add Component]
└─ Name: "Bullet" └─ Type: Transform
```
<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)
@@ -133,11 +203,50 @@ your-project/
| **Get/Set Property** | 获取/设置组件属性 |
**示例:修改 Transform 组件**
```
[Get Self] ─Entity─→ [Get Component: Transform] ─Component─→ [Set Property]
├─ Property: x
└─ Value: 100
```
<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)
@@ -147,42 +256,161 @@ your-project/
条件判断,类似 if/else。
```
┌─ True ──→ [DoSomething]
[Branch]─┤
└─ False ─→ [DoOtherThing]
```
<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 (序列)
按顺序执行多个分支。
```
┌─ Then 0 ──→ [Step1]
[Sequence]─┼─ Then 1 ──→ [Step2]
└─ Then 2 ──→ [Step3]
```
<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 (循环)
循环执行指定次数。
```
[For Loop] ─Loop Body─→ [每次迭代执行]
└─ Completed ────→ [循环结束后执行]
```
| 输入 | 说明 |
|------|------|
| First Index | 起始索引 |
| Last Index | 结束索引 |
| 输出 | 说明 |
|------|------|
| Loop Body | 每次迭代执行 |
| Index | 当前索引 |
| Completed | 循环结束后执行 |
<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 (遍历)
@@ -213,10 +441,49 @@ your-project/
| **Get Time** | 获取运行总时间 | Number |
**示例:延迟 2 秒后执行**
```
[Event BeginPlay] ──→ [Delay] ──→ [Print]
└─ Duration: 2.0 └─ "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)
@@ -346,50 +613,244 @@ your-project/
实现每帧移动实体:
```
[Event Tick] ─Exec─→ [Get Self] ─Entity─→ [Get Component: Transform]
[Get Delta Time] ▼
│ [Set Property: x]
│ │
[Multiply] ◄──────────────┘
└─ Speed: 100
```
<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 })` 触发
```
[On Damage Event] ─→ [Get Component: Health] ─→ [Get Property: current]
[Subtract]
[Set Property: current]
┌─ True ─→ [Destroy Self]
[Branch]─┤
└─ False ─→ (继续)
[Less Or Equal]
current <= 0
```
<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 秒生成一个敌人:
```
[Event BeginPlay] ─→ [Do N Times] ─Loop─→ [Delay: 2.0] ─→ [Create Entity: Enemy]
└─ N: 10
```
<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>
## 常见问题
@@ -409,7 +870,7 @@ your-project/
## 下一步
- [ECS 节点参考](./nodes) - 完整节点列表
- [自定义节点](./custom-nodes) - 创建自定义节点
- [运行时集成](./vm) - 蓝图虚拟机 API
- [实际示例](./examples) - 更多游戏逻辑示例
- [ECS 节点参考](/modules/blueprint/nodes) - 完整节点列表
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
- [运行时集成](/modules/blueprint/vm) - 蓝图虚拟机 API
- [实际示例](/modules/blueprint/examples) - 更多游戏逻辑示例

View File

@@ -7,12 +7,11 @@ description: "与 ECS 框架深度集成的可视化脚本系统"
## 编辑器下载
> **内测中**:蓝图编辑器目前处于内测阶段,需要激活码才能使用。
> 请加入 QQ 群 **481923584** 后私聊群主获取激活码。
Cocos Creator 蓝图编辑器插件(免费):
Cocos Creator 蓝图编辑器插件:
**[下载 Cocos Node Editor v1.2.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.2.0)**
**[下载 Cocos Node Editor v1.0.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.0.0)**
> 技术交流 QQ 群:**481923584** | 官网:[esengine.cn](https://esengine.cn/)
详细使用教程请参考 [编辑器使用指南](./editor-guide)。

View File

@@ -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,167 +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` | 四则运算 |
| `Modulo` | 取模运算 (%) |
| `Negate` | 取负 |
| `Abs` | 绝对值 |
| `Sign` | 符号 (+1, 0, -1) |
| `Min` / `Max` | 最小/最大值 |
| `Clamp` | 限制在范围内 |
| `Wrap` | 循环限制在范围内 |
| `Print` | 输出消息到控制台 |
幂与根:
## 相关文档
| 节点 | 说明 |
|------|------|
| `Power` | 幂运算 (A^B) |
| `Sqrt` | 平方根 |
取整:
| 节点 | 说明 |
|------|------|
| `Floor` | 向下取整 |
| `Ceil` | 向上取整 |
| `Round` | 四舍五入 |
三角函数:
| 节点 | 说明 |
|------|------|
| `Sin` / `Cos` / `Tan` | 正弦/余弦/正切 |
| `Asin` / `Acos` / `Atan` | 反三角函数 |
| `Atan2` | 两参数反正切 |
| `DegToRad` / `RadToDeg` | 角度与弧度转换 |
插值:
| 节点 | 说明 |
|------|------|
| `Lerp` | 线性插值 |
| `InverseLerp` | 反向线性插值 |
随机数:
| 节点 | 说明 |
|------|------|
| `Random Range` | 范围内随机浮点数 |
| `Random Int` | 范围内随机整数 |
## 逻辑节点 (Logic)
比较运算:
| 节点 | 说明 |
|------|------|
| `Equal` | 等于 (==) |
| `Not Equal` | 不等于 (!=) |
| `Greater Than` | 大于 (>) |
| `Greater Or Equal` | 大于等于 (>=) |
| `Less Than` | 小于 (<) |
| `Less Or Equal` | 小于等于 (<=) |
| `In Range` | 检查值是否在范围内 |
逻辑运算:
| 节点 | 说明 |
|------|------|
| `AND` | 逻辑与 |
| `OR` | 逻辑或 |
| `NOT` | 逻辑非 |
| `XOR` | 异或 |
| `NAND` | 与非 |
工具节点:
| 节点 | 说明 |
|------|------|
| `Is Null` | 检查值是否为空 |
| `Select` | 根据条件选择 A 或 B (三元运算) |
## 调试节点 (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

View 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) - 创建自定义节点

View 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);
```

View 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) - 定点数客户端预测

View File

@@ -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';
```

View File

@@ -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';
```

View File

@@ -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;
}

View File

@@ -1,5 +1,21 @@
# @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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/blueprint",
"version": "4.4.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",

View File

@@ -561,3 +561,325 @@ export class SignExecutor implements INodeExecutor {
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) } };
}
}

View File

@@ -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',

View File

@@ -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

View File

@@ -1,3 +1,5 @@
export * from './pins';
export * from './nodes';
export * from './blueprint';
export * from './schema';
export * from './path-utils';

View File

@@ -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;
}

View 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 '[*]';
}
}

View 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 };
}
};

View File

@@ -1,5 +1,12 @@
# @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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/fsm",
"version": "8.0.0",
"version": "9.0.0",
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
"type": "module",
"main": "./dist/index.js",

View 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

View File

@@ -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",

View 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;
}

View 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;
}
}

View 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));
}
}

View File

@@ -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';

View 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() }
];

View 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() }
];

View 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() }
];

View 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() }
];

View 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
];

View 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);
});
});
});

View 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);
});
});
});

View File

@@ -1,5 +1,43 @@
# @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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/network",
"version": "9.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",

View File

@@ -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'
// ============================================================================

View File

@@ -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;
}
};
}

View 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 });
}

View File

@@ -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);
}

View 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
);
}

View 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';

View File

@@ -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';

View File

@@ -1,5 +1,34 @@
# @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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/pathfinding",
"version": "8.0.0",
"version": "12.0.0",
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,12 @@
# @esengine/procgen
## 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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/procgen",
"version": "8.0.0",
"version": "9.0.0",
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,34 @@
# @esengine/spatial
## 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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/spatial",
"version": "8.0.0",
"version": "12.0.0",
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,12 @@
# @esengine/timer
## 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

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/timer",
"version": "8.0.0",
"version": "9.0.0",
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1,5 +1,40 @@
# @esengine/demos
## 1.0.18
### Patch Changes
- Updated dependencies []:
- @esengine/fsm@9.0.0
- @esengine/pathfinding@12.0.0
- @esengine/procgen@9.0.0
- @esengine/spatial@12.0.0
- @esengine/timer@9.0.0
## 1.0.17
### Patch Changes
- Updated dependencies []:
- @esengine/pathfinding@11.0.0
- @esengine/spatial@11.0.0
## 1.0.16
### Patch Changes
- Updated dependencies []:
- @esengine/pathfinding@10.0.0
- @esengine/spatial@10.0.0
## 1.0.15
### Patch Changes
- Updated dependencies []:
- @esengine/pathfinding@9.0.0
- @esengine/spatial@9.0.0
## 1.0.14
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/demos",
"version": "1.0.14",
"version": "1.0.18",
"private": true,
"description": "Demo tests for ESEngine modules documentation",
"type": "module",

10
pnpm-lock.yaml generated
View File

@@ -168,6 +168,9 @@ importers:
astro:
specifier: ^5.6.1
version: 5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
sharp:
specifier: ^0.34.2
version: 0.34.5
@@ -1572,6 +1575,10 @@ importers:
version: 5.9.3
packages/framework/math:
dependencies:
'@esengine/blueprint':
specifier: workspace:*
version: link:../blueprint
devDependencies:
'@rollup/plugin-commonjs':
specifier: ^28.0.3
@@ -1625,6 +1632,9 @@ importers:
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../math
rimraf:
specifier: ^5.0.5
version: 5.0.10