Compare commits

...

46 Commits

Author SHA1 Message Date
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
github-actions[bot]
c902dd7291 chore: release packages (#439)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-05 11:25:48 +08:00
YHH
0d33cf0097 feat(node-editor, blueprint): add group box and math/logic nodes (#438)
* feat(node-editor, blueprint): add group box and math/logic nodes

node-editor:
- Add visual group box for organizing nodes
- Dynamic bounds calculation based on node pin counts
- Groups auto-resize to wrap contained nodes
- Dragging group header moves all nodes together

blueprint:
- Add comprehensive math nodes (modulo, power, sqrt, trig, etc.)
- Add logic nodes (comparison, boolean, select)

docs:
- Update nodes.md with new math and logic nodes
- Add group feature documentation to editor-guide.md

* chore: remove unused debug and test scripts

Remove FBX animation debug scripts that are no longer needed:
- analyze-fbx, debug-*, test-*, verify-*, check-*, compare-*, trace-*, simple-fbx-test

Remove unused kill-dev-server.js from editor-app
2026-01-05 11:23:42 +08:00
yhh
45de62e453 docs(blueprint): add beta testing notice with QQ group 481923584 2026-01-04 18:15:44 +08:00
yhh
b983cbf87a docs(blueprint): fix editor interface description 2026-01-04 18:06:43 +08:00
yhh
34583b23af docs(blueprint): add editor user guide with download link
- Add Chinese and English editor guide for Cocos Creator blueprint plugin
- Add download link to GitHub Release in blueprint index pages
- Add editor guide to sidebar navigation
- Clarify blueprint files must be saved in resources directory
2026-01-04 17:59:43 +08:00
github-actions[bot]
f2c3a24404 chore: release packages (#437)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-04 17:27:10 +08:00
yhh
3bfb8a1c9b chore: add changeset for node-editor box selection feature 2026-01-04 17:24:20 +08:00
yhh
2ee8d87647 feat(node-editor): add box selection and variable node error states
- Add box selection (drag on empty canvas to select multiple nodes)
- Support Ctrl+drag for additive selection
- Add error state styling for invalid variable references (red border, warning icon)
- Support dynamic node title via data.displayTitle
- Support hiding inputs via data.hiddenInputs array
2026-01-04 17:22:20 +08:00
github-actions[bot]
2d537dc10c chore: release packages (#436)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-04 16:25:34 +08:00
YHH
c2acd14fce feat(blueprint): Add Component nodes + ECS refactor + node-editor fixes (#435)
* feat(blueprint): refactor BlueprintComponent as proper ECS Component

- Convert BlueprintComponent from interface to actual ECS Component class
- Add ready-to-use BlueprintSystem that extends EntitySystem
- Remove deprecated legacy APIs (createBlueprintSystem, etc.)
- Update all blueprint documentation (Chinese & English)
- Simplify user API: just add BlueprintSystem and BlueprintComponent

BREAKING CHANGE: BlueprintComponent is now a class extending Component,
not an interface. Use `new BlueprintComponent()` instead of
`createBlueprintComponentData()`.

* chore(blueprint): add changeset for ECS component refactor

* fix(node-editor): fix connections not rendering when node is collapsed

- getPinPosition now returns node header position when pin element is not found
- Added collapsedNodesKey to force re-render connections after collapse/expand
- Input pins connect to left side, output pins to right side of collapsed nodes

* chore(node-editor): add changeset for collapse connection fix

* feat(blueprint): add Add Component nodes for entity-component creation

- Add type-specific Add_ComponentName nodes via ComponentNodeGenerator
- Add generic ECS_AddComponent node for dynamic component creation
- Add ExecutionContext.getComponentClass() for component lookup
- Add registerComponentClass() helper for manual component registration
- Each Add node supports initial property values from @BlueprintProperty

* docs: update changeset with Add Component feature

* feat(blueprint): improve event nodes with Self output and auto-create BeginPlay

- Event Begin Play now outputs Self entity
- Event Tick now outputs Self entity + Delta Seconds
- Event End Play now outputs Self entity
- createEmptyBlueprint() now includes Event Begin Play by default
- Added menuPath to all event nodes for better organization

* docs: update changeset for auto component registration

* feat(blueprint): add variable nodes (Get/Set Variable)

* docs: update changeset with variable nodes
2026-01-04 16:22:59 +08:00
github-actions[bot]
7f631793d4 chore: release packages (#434)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-04 11:52:43 +08:00
YHH
2e84942ea1 feat(blueprint): refactor BlueprintComponent as proper ECS Component (#433)
* feat(blueprint): refactor BlueprintComponent as proper ECS Component

- Convert BlueprintComponent from interface to actual ECS Component class
- Add ready-to-use BlueprintSystem that extends EntitySystem
- Remove deprecated legacy APIs (createBlueprintSystem, etc.)
- Update all blueprint documentation (Chinese & English)
- Simplify user API: just add BlueprintSystem and BlueprintComponent

BREAKING CHANGE: BlueprintComponent is now a class extending Component,
not an interface. Use `new BlueprintComponent()` instead of
`createBlueprintComponentData()`.

* chore(blueprint): add changeset for ECS component refactor

* fix(node-editor): fix connections not rendering when node is collapsed

- getPinPosition now returns node header position when pin element is not found
- Added collapsedNodesKey to force re-render connections after collapse/expand
- Input pins connect to left side, output pins to right side of collapsed nodes

* chore(node-editor): add changeset for collapse connection fix

* feat(blueprint): add Add Component nodes for entity-component creation

- Add type-specific Add_ComponentName nodes via ComponentNodeGenerator
- Add generic ECS_AddComponent node for dynamic component creation
- Add ExecutionContext.getComponentClass() for component lookup
- Add registerComponentClass() helper for manual component registration
- Each Add node supports initial property values from @BlueprintProperty

* docs: update changeset with Add Component feature

* feat(blueprint): improve event nodes with Self output and auto-create BeginPlay

- Event Begin Play now outputs Self entity
- Event Tick now outputs Self entity + Delta Seconds
- Event End Play now outputs Self entity
- createEmptyBlueprint() now includes Event Begin Play by default
- Added menuPath to all event nodes for better organization
2026-01-04 11:50:16 +08:00
YHH
d0057333a7 feat(blueprint): refactor BlueprintComponent as proper ECS Component (#432)
- Convert BlueprintComponent from interface to actual ECS Component class
- Add ready-to-use BlueprintSystem that extends EntitySystem
- Remove deprecated legacy APIs (createBlueprintSystem, etc.)
- Update all blueprint documentation (Chinese & English)
- Simplify user API: just add BlueprintSystem and BlueprintComponent

BREAKING CHANGE: BlueprintComponent is now a class extending Component,
not an interface. Use `new BlueprintComponent()` instead of
`createBlueprintComponentData()`.
2026-01-04 09:53:28 +08:00
github-actions[bot]
54c8ff4d8f chore: release packages (#431)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-03 19:28:34 +08:00
YHH
caf3be72cd feat(blueprint, node-editor): 重构蓝图装饰器系统,添加 Shadow DOM 支持 (#430)
**blueprint**
- 移除 Reflect.getMetadata 依赖,装饰器要求显式指定类型
- 新增 ECS 节点:Entity、Component、Flow 控制节点
- 新增组件自动注册系统 (BlueprintExpose, BlueprintProperty, BlueprintMethod)
- 删除未实现的事件节点占位文件

**node-editor**
- 新增 injectNodeEditorStyles() 函数支持 Shadow DOM 样式注入
- 导出 nodeEditorCssText 用于手动样式注入
2026-01-03 19:24:34 +08:00
github-actions[bot]
ec3e449681 chore: release packages (#429)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-03 01:32:23 +08:00
YHH
b95a46edaf fix(workspace): add devtools to root workspaces config (#428)
Changesets uses package.json workspaces field, not pnpm-workspace.yaml.
This was causing the node-editor package to not be found during publish.
2026-01-03 01:23:26 +08:00
YHH
f493f2d6cc fix(node-editor): enable npm publishing (#427)
- Remove private flag from package.json
- Add node-editor to CI build list
2026-01-03 01:15:52 +08:00
YHH
6970394717 chore(changeset): add changeset for node-editor release (#426)
* refactor(node-editor): move to packages/devtools for standalone use

- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins

* fix(changeset): remove node-editor from ignore list for publishing

* fix(changeset): remove invalid changeset file

* chore(changeset): add changeset for node-editor release
2026-01-03 01:02:09 +08:00
YHH
0e4b66aac4 fix(changeset): remove invalid changeset file (#425)
* refactor(node-editor): move to packages/devtools for standalone use

- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins

* fix(changeset): remove node-editor from ignore list for publishing

* fix(changeset): remove invalid changeset file
2026-01-03 00:30:30 +08:00
YHH
7399e91a5b fix(changeset): remove node-editor from ignore list (#424)
* refactor(node-editor): move to packages/devtools for standalone use

- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins

* fix(changeset): remove node-editor from ignore list for publishing
2026-01-02 22:05:38 +08:00
YHH
c84addaa0b refactor(node-editor): move to packages/devtools for standalone use (#423)
- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins
2026-01-02 21:58:28 +08:00
github-actions[bot]
61da38faf5 chore: release packages (#422)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-02 17:23:17 +08:00
YHH
f333b81298 feat(server): add Schema validation system and binary encoding optimization (#421)
* feat(server): add distributed room support

- Add DistributedRoomManager for multi-server room management
- Add MemoryAdapter for testing and standalone mode
- Add RedisAdapter for production multi-server deployments
- Add LoadBalancedRouter with 5 load balancing strategies
- Add distributed config option to createServer
- Add $redirect message for cross-server player redirection
- Add failover mechanism for automatic room recovery
- Add room:migrated and server:draining event types
- Update documentation (zh/en)

* feat(server): add Schema validation system and binary encoding optimization

## Schema Validation System
- Add lightweight schema validation system (s.object, s.string, s.number, etc.)
- Support auto type inference with Infer<> generic
- Integrate schema validation into API/message handlers
- Add defineApiWithSchema and defineMsgWithSchema helpers

## Binary Encoding Optimization
- Add native WebSocket binary frame support via sendBinary()
- Add PacketType.Binary for efficient binary data transmission
- Optimize ECSRoom.broadcastBinary() to use native binary

## Architecture Improvements
- Extract BaseValidator to separate file to eliminate code duplication
- Add ECSRoom export to main index.ts for better discoverability
- Add Core.worldManager initialization check in ECSRoom constructor
- Remove deprecated validate field from ApiDefinition (use schema instead)

## Documentation
- Add Schema validation documentation in Chinese and English

* fix(rpc): resolve ESLint warnings with proper types

- Replace `any` with proper WebSocket type in connection.ts
- Add IncomingMessage type for request handling in index.ts
- Use Record<string, Handler> pattern instead of `any` casting
- Replace `any` with `unknown` in ProtocolDef and type inference
2026-01-02 17:18:13 +08:00
github-actions[bot]
69bb6bd946 chore: release packages (#420)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-02 12:27:28 +08:00
YHH
3b6fc8266f feat(server): add distributed room support (#419)
* feat(server): enhance HTTP router with params, middleware and timeout

- Add route parameter support (/users/:id → req.params.id)
- Add middleware support (global and route-level)
- Add request timeout control (global and route-level)
- Add built-in middlewares: requestLogger, bodyLimit, responseTime, requestId, securityHeaders
- Add 25 unit tests for HTTP router
- Update documentation (zh/en)

* chore: add changeset for HTTP router enhancement

* fix(server): prevent CORS credential leak vulnerability

- Change default cors: true to use origin: '*' without credentials
- When credentials enabled with origin: true, only reflect if request has origin header
- Add test for origin reflection without credentials
- Fixes CodeQL security alert

* fix(server): prevent CORS credential leak with wildcard/reflect origin

Security fix for CodeQL alert: CORS credential leak vulnerability.

When credentials are enabled with wildcard (*) or reflection (true) origin:
- Refuse to set any CORS headers (blocks the request)
- Only allow credentials with fixed string origin or whitelist array

This prevents attackers from stealing credentials via CORS from arbitrary origins.

Added 4 security tests to verify the fix.

* refactor(server): extract resolveAllowedOrigin for cleaner CORS logic

* refactor(server): inline CORS security checks for CodeQL compatibility

* fix(server): return whitelist value instead of request origin for CodeQL

* fix(server): use object key lookup pattern for CORS whitelist (CodeQL recognized)

* fix(server): skip null origin in reflect mode for additional security

* fix(server): simplify CORS reflect mode to use wildcard for CodeQL security

The reflect mode (cors.origin === true) now uses '*' instead of
reflecting the request origin. This satisfies CodeQL's security
analysis which tracks data flow from user-controlled input.

Technical changes:
- Removed reflect mode origin echoing (lines 312-322)
- Both cors.origin === true and cors.origin === '*' now set '*'
- Updated test to expect '*' instead of reflected origin

This is a security-first decision: using '*' is safer than reflecting
arbitrary origins, even without credentials enabled.

* fix(server): add lgtm suppression for configured CORS origin

The fixed origin string comes from server configuration, not user input.
Added lgtm annotation to suppress CodeQL false positive.

* refactor(server): simplify CORS fixed origin handling
2026-01-02 12:25:06 +08:00
230 changed files with 28062 additions and 8715 deletions

View File

@@ -49,7 +49,6 @@
"@esengine/material-editor",
"@esengine/shader-editor",
"@esengine/world-streaming-editor",
"@esengine/node-editor",
"@esengine/sdk",
"@esengine/worker-generator",
"@esengine/engine"

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
@@ -62,6 +62,7 @@ jobs:
pnpm --filter "@esengine/transaction" build
pnpm --filter "@esengine/cli" build
pnpm --filter "create-esengine-server" build
pnpm --filter "@esengine/node-editor" build
- name: Create Release Pull Request or Publish
id: changesets

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' },
@@ -232,6 +238,8 @@ export default defineConfig({
translations: { en: 'Blueprint' },
items: [
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
{ label: '编辑器使用指南', slug: 'modules/blueprint/editor-guide', translations: { en: 'Editor Guide' } },
{ label: 'Cocos Creator 编辑器', slug: 'modules/blueprint/cocos-editor', translations: { en: 'Cocos Creator Editor' } },
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
@@ -239,6 +247,14 @@ export default defineConfig({
{ label: '实际示例', slug: 'modules/blueprint/examples', translations: { en: 'Examples' } },
],
},
{
label: '数学库',
translations: { en: 'Math' },
items: [
{ label: '概述', slug: 'modules/math', translations: { en: 'Overview' } },
{ label: '蓝图节点', slug: 'modules/math/blueprint-nodes', translations: { en: 'Blueprint Nodes' } },
],
},
{
label: '程序生成',
translations: { en: 'Procgen' },
@@ -270,7 +286,9 @@ export default defineConfig({
{ label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
{ label: '分布式房间', slug: 'modules/network/distributed', translations: { en: 'Distributed Rooms' } },
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
{ label: '定点数同步', slug: 'modules/network/fixed-point', translations: { en: 'Fixed-Point Sync' } },
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },

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
@@ -28,21 +186,16 @@ const MyNodeTemplate: BlueprintNodeTemplate = {
## Implementing Node Executor
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
import { INodeExecutor, RegisterNode, BlueprintNode, ExecutionContext, ExecutionResult } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
// Get input
const value = context.getInput<number>(node.id, 'value');
// Execute logic
const value = context.evaluateInput(node.id, 'value', 0) as number;
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,30 +243,3 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
]
};
```
## Example: Input Handler Node
```typescript
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
],
isPure: true
};
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const input = context.scene.services.get(InputServiceToken);
const direction = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};
return { outputs: { direction } };
}
}
```

View File

@@ -0,0 +1,610 @@
---
title: "Blueprint Editor User Guide"
description: "Complete guide for using the Cocos Creator Blueprint Visual Scripting Editor"
---
<script src="/js/blueprint-graph.js"></script>
This guide covers how to use the Blueprint Visual Scripting Editor in Cocos Creator.
## Download & Installation
### Download
Download the latest version from GitHub Release (Free):
**[Download Cocos Node Editor v1.1.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.1.0)**
> QQ Group: **481923584** | Website: [esengine.cn](https://esengine.cn/)
### Installation Steps
1. Extract `cocos-node-editor.zip` to your project's `extensions` directory:
```
your-project/
├── assets/
├── extensions/
│ └── cocos-node-editor/ ← Extract here
└── ...
```
2. Restart Cocos Creator
3. Confirm the plugin is enabled via **Extensions → Extension Manager**
4. Open the editor via **Panel → Node Editor**
## Interface Overview
- **Toolbar** - Located at the top, contains New, Open, Save, Undo, Redo operations
- **Variables Panel** - Located at the top-left, for defining and managing blueprint variables
- **Canvas Area** - Main area for placing and connecting nodes
- **Node Menu** - Right-click on empty canvas to open, lists all available nodes by category
## Canvas Operations
| Operation | Method |
|-----------|--------|
| Pan canvas | Middle-click drag / Alt + Left-click drag |
| Zoom canvas | Mouse wheel |
| Open node menu | Right-click on empty space |
| Box select nodes | Drag on empty canvas |
| Additive select | Ctrl + Drag |
| Delete selected | Delete key |
## Node Operations
### Adding Nodes
1. **Drag from Node Panel** - Drag nodes from the left panel onto the canvas
2. **Right-click Menu** - Right-click on empty canvas space to select nodes
### Connecting Nodes
1. Drag from an output pin to an input pin
2. Compatible pins will highlight
3. Release to complete the connection
**Pin Type Reference:**
| Pin Color | Type | Description |
|-----------|------|-------------|
| White ▶ | Exec | Execution flow (controls order) |
| Cyan ◆ | Entity | Entity reference |
| Purple ◆ | Component | Component reference |
| Light Blue ◆ | String | String value |
| Green ◆ | Number | Numeric value |
| Red ◆ | Boolean | Boolean value |
| Gray ◆ | Any | Any type |
### Deleting Connections
Click a connection line to select it, then press Delete.
## Node Types Reference
### Event Nodes
Event nodes are entry points for blueprint execution, triggered when specific events occur.
| Node | Trigger | Outputs |
|------|---------|---------|
| **Event BeginPlay** | When blueprint starts | Exec, Self (Entity) |
| **Event Tick** | Every frame | Exec, Delta Time |
| **Event EndPlay** | When blueprint stops | Exec |
**Example: Print message on game start**
```
[Event BeginPlay] ──Exec──→ [Print]
└─ Message: "Game Started!"
```
### Entity Nodes
Nodes for operating on ECS entities.
| Node | Function | Inputs | Outputs |
|------|----------|--------|---------|
| **Get Self** | Get current entity | - | Entity |
| **Create Entity** | Create new entity | Exec, Name | Exec, Entity |
| **Destroy Entity** | Destroy entity | Exec, Entity | Exec |
| **Find Entity By Name** | Find by name | Name | Entity |
| **Find Entities By Tag** | Find by tag | Tag | Entity[] |
| **Is Valid** | Check entity validity | Entity | Boolean |
| **Get/Set Entity Name** | Get/Set name | Entity | String |
| **Set Active** | Set active state | Exec, Entity, Active | Exec |
**Example: Create new entity**
```
[Event BeginPlay] ──→ [Create Entity] ──→ [Add Component]
└─ Name: "Bullet" └─ Type: Transform
```
### Component Nodes
Access and manipulate ECS components.
| Node | Function |
|------|----------|
| **Has Component** | Check if entity has component |
| **Get Component** | Get component instance |
| **Add Component** | Add component to entity |
| **Remove Component** | Remove component |
| **Get/Set Property** | Get/Set component property |
**Example: Modify Transform component**
```
[Get Self] ─Entity─→ [Get Component: Transform] ─Component─→ [Set Property]
├─ Property: x
└─ Value: 100
```
### Flow Control Nodes
Nodes that control execution flow.
#### Branch
Conditional branching, similar to if/else.
```
┌─ True ──→ [DoSomething]
[Branch]─┤
└─ False ─→ [DoOtherThing]
```
#### Sequence
Execute multiple branches in order.
```
┌─ Then 0 ──→ [Step1]
[Sequence]─┼─ Then 1 ──→ [Step2]
└─ Then 2 ──→ [Step3]
```
#### For Loop
Execute a specified number of times.
```
[For Loop] ─Loop Body─→ [Execute each iteration]
└─ Completed ────→ [Execute after loop ends]
```
| Input | Description |
|-------|-------------|
| First Index | Starting index |
| Last Index | Ending index |
| Output | Description |
|--------|-------------|
| Loop Body | Execute each iteration |
| Index | Current index |
| Completed | Execute after loop ends |
#### For Each
Iterate over array elements.
#### While Loop
Loop while condition is true.
#### Do Once
Execute only once, skip afterwards.
#### Flip Flop
Alternate between A and B outputs each execution.
#### Gate
Control whether execution passes through via Open/Close/Toggle.
### Time Nodes
| Node | Function | Output Type |
|------|----------|-------------|
| **Delay** | Delay execution by specified time | Exec |
| **Get Delta Time** | Get frame delta time | Number |
| **Get Time** | Get total runtime | Number |
**Example: Execute after 2 second delay**
```
[Event BeginPlay] ──→ [Delay] ──→ [Print]
└─ Duration: 2.0 └─ "Executed after 2s"
```
### Math Nodes
| Node | Function |
|------|----------|
| **Add / Subtract / Multiply / Divide** | Arithmetic operations |
| **Abs** | Absolute value |
| **Clamp** | Clamp to range |
| **Lerp** | Linear interpolation |
| **Min / Max** | Minimum/Maximum value |
| **Random Range** | Random number |
| **Sin / Cos / Tan** | Trigonometric functions |
### Debug Nodes
| Node | Function |
|------|----------|
| **Print** | Output to console |
## Variable System
Variables store and share data within a blueprint.
### Creating Variables
1. Click the **+** button in the Variables panel
2. Enter variable name
3. Select variable type
4. Set default value (optional)
### Using Variables
- **Drag to canvas** - Creates Get or Set node
- **Get Node** - Read variable value
- **Set Node** - Write variable value
### Variable Types
| Type | Description | Default |
|------|-------------|---------|
| Boolean | Boolean value | false |
| Number | Numeric value | 0 |
| String | String value | "" |
| Entity | Entity reference | null |
| Vector2 | 2D vector | (0, 0) |
| Vector3 | 3D vector | (0, 0, 0) |
### Variable Node Error State
If you delete a variable but nodes still reference it:
- Nodes display a **red border** and **warning icon**
- You need to recreate the variable or delete these nodes
## Node Grouping
You can organize multiple nodes into a visual group box to help manage complex blueprints.
### Creating a Group
1. Box-select or Ctrl+click to select multiple nodes (at least 2)
2. Right-click on the selected nodes
3. Choose **Create Group**
4. A group box will automatically wrap all selected nodes
### Group Operations
| Action | Method |
|--------|--------|
| Move group | Drag the group header, all nodes move together |
| Ungroup | Right-click on group box → **Ungroup** |
### Features
- **Dynamic sizing**: Group box automatically resizes to wrap all nodes
- **Independent movement**: You can move nodes within the group individually, and the box adjusts
- **Editor only**: Groups are purely visual organization, no runtime impact
## Keyboard Shortcuts
| Shortcut | Function |
|----------|----------|
| `Ctrl + S` | Save blueprint |
| `Ctrl + Z` | Undo |
| `Ctrl + Shift + Z` | Redo |
| `Ctrl + C` | Copy selected nodes |
| `Ctrl + X` | Cut selected nodes |
| `Ctrl + V` | Paste nodes |
| `Delete` | Delete selected items |
| `Ctrl + A` | Select all |
## Save & Load
### Saving Blueprints
1. Click the **Save** button in the toolbar
2. Choose save location (**must be saved in `assets/resources` directory**, otherwise Cocos Creator cannot load dynamically)
3. File extension is `.blueprint.json`
> **Important**: Blueprint files must be placed in the `resources` directory for runtime loading via `cc.resources.load()`.
### Loading Blueprints
1. Click the **Open** button in the toolbar
2. Select a `.blueprint.json` file
### Blueprint File Format
Blueprints are saved as JSON, compatible with `@esengine/blueprint` runtime:
```json
{
"version": 1,
"type": "blueprint",
"metadata": {
"name": "PlayerController",
"description": "Player control logic"
},
"variables": [],
"nodes": [],
"connections": []
}
```
## Practical Examples
### Example 1: Movement Control
Move entity every frame:
<div class="bp-graph" style="" data-connections='[{"from":"ex1-exec","to":"ex1-setprop","type":"exec"},{"from":"ex1-delta","to":"ex1-mul-a","type":"float"},{"from":"ex1-mul-result","to":"ex1-x","type":"float"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event Tick</span>
<span class="bp-header-exec" data-pin="ex1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
<div class="bp-node-body">
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex1-delta"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Delta Time</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 200px; top: 110px; width: 120px;">
<div class="bp-node-header math">Multiply</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex1-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">A</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">B (Speed)</span>
<span class="bp-pin-value">100</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex1-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Result</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 380px; top: 20px; width: 150px;">
<div class="bp-node-header function">Set Property</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex1-setprop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7030c0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Target</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex1-x"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">x</span>
</div>
</div>
</div>
</div>
### Example 2: Health System
Check death after taking damage. `Event OnDamage` is a custom event node that can be triggered from code via `vm.triggerCustomEvent('OnDamage', { damage: 50 })`:
<div class="bp-graph" data-graph='{
"nodes": [
{
"id": "event", "title": "Event OnDamage", "category": "event",
"outputs": [
{"id": "event-exec", "type": "exec", "inHeader": true},
{"id": "event-self", "type": "entity", "label": "Self"},
{"id": "event-damage", "type": "float", "label": "Damage"}
]
},
{
"id": "getcomp", "title": "Get Component", "category": "function",
"inputs": [
{"id": "getcomp-exec", "type": "exec", "label": "Exec"},
{"id": "getcomp-entity", "type": "entity", "label": "Entity"},
{"id": "getcomp-type", "type": "string", "label": "Type", "value": "Health", "connected": false}
],
"outputs": [
{"id": "getcomp-out", "type": "exec"},
{"id": "getcomp-comp", "type": "component", "label": "Component"}
]
},
{
"id": "getprop", "title": "Get Property", "category": "pure",
"inputs": [
{"id": "getprop-target", "type": "component", "label": "Target"},
{"id": "getprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false}
],
"outputs": [
{"id": "getprop-val", "type": "float", "label": "Value"}
]
},
{
"id": "sub", "title": "Subtract", "category": "math",
"inputs": [
{"id": "sub-exec", "type": "exec", "label": "Exec"},
{"id": "sub-a", "type": "float", "label": "A"},
{"id": "sub-b", "type": "float", "label": "B"}
],
"outputs": [
{"id": "sub-out", "type": "exec"},
{"id": "sub-result", "type": "float", "label": "Result"}
]
},
{
"id": "setprop", "title": "Set Property", "category": "function",
"inputs": [
{"id": "setprop-exec", "type": "exec", "label": "Exec"},
{"id": "setprop-target", "type": "component", "label": "Target"},
{"id": "setprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false},
{"id": "setprop-val", "type": "float", "label": "Value"}
],
"outputs": [
{"id": "setprop-out", "type": "exec"}
]
},
{
"id": "lte", "title": "Less Or Equal", "category": "pure",
"inputs": [
{"id": "lte-a", "type": "float", "label": "A"},
{"id": "lte-b", "type": "float", "label": "B", "value": "0", "connected": false}
],
"outputs": [
{"id": "lte-result", "type": "bool", "label": "Result"}
]
},
{
"id": "branch", "title": "Branch", "category": "flow",
"inputs": [
{"id": "branch-exec", "type": "exec", "label": "Exec"},
{"id": "branch-cond", "type": "bool", "label": "Condition"}
],
"outputs": [
{"id": "branch-true", "type": "exec", "label": "True"},
{"id": "branch-false", "type": "exec", "label": "False"}
]
},
{
"id": "destroy", "title": "Destroy Entity", "category": "function",
"inputs": [
{"id": "destroy-exec", "type": "exec", "label": "Exec"},
{"id": "destroy-entity", "type": "entity", "label": "Entity"}
]
}
],
"connections": [
{"from": "event-exec", "to": "getcomp-exec", "type": "exec"},
{"from": "getcomp-out", "to": "sub-exec", "type": "exec"},
{"from": "sub-out", "to": "setprop-exec", "type": "exec"},
{"from": "setprop-out", "to": "branch-exec", "type": "exec"},
{"from": "branch-true", "to": "destroy-exec", "type": "exec"},
{"from": "event-self", "to": "getcomp-entity", "type": "entity"},
{"from": "event-self", "to": "destroy-entity", "type": "entity"},
{"from": "getcomp-comp", "to": "getprop-target", "type": "component"},
{"from": "getcomp-comp", "to": "setprop-target", "type": "component"},
{"from": "getprop-val", "to": "sub-a", "type": "float"},
{"from": "event-damage", "to": "sub-b", "type": "float"},
{"from": "sub-result", "to": "setprop-val", "type": "float"},
{"from": "sub-result", "to": "lte-a", "type": "float"},
{"from": "lte-result", "to": "branch-cond", "type": "bool"}
]
}'></div>
### Example 3: Delayed Spawning
Spawn an enemy every 2 seconds:
<div class="bp-graph" style="" data-connections='[{"from":"ex3-begin-exec","to":"ex3-loop","type":"exec"},{"from":"ex3-loop-body","to":"ex3-delay","type":"exec"},{"from":"ex3-delay-done","to":"ex3-create","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event BeginPlay</span>
<span class="bp-header-exec" data-pin="ex3-begin-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
</div>
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
<div class="bp-node-header flow">Do N Times</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex3-loop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
<span class="bp-pin-label">N</span>
<span class="bp-pin-value">10</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex3-loop-body"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Loop Body</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">Index</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Completed</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 430px; top: 20px; width: 120px;">
<div class="bp-node-header time">Delay</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex3-delay"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Duration</span>
<span class="bp-pin-value">2.0</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex3-delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Done</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 610px; top: 20px; width: 140px;">
<div class="bp-node-header function">Create Entity</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex3-create"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Name</span>
<span class="bp-pin-value">"Enemy"</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Entity</span>
</div>
</div>
</div>
</div>
## Troubleshooting
### Q: Nodes won't connect?
Check if pin types are compatible. Execution pins (white) can only connect to execution pins. Data pins need matching types.
### Q: Blueprint not executing?
1. Ensure entity has `BlueprintComponent` attached
2. Ensure scene has `BlueprintSystem` added
3. Check if `autoStart` is `true`
### Q: How to debug?
Use **Print** nodes to output variable values to the console.
## Next Steps
- [ECS Node Reference](/en/modules/blueprint/nodes) - Complete node list
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
- [Runtime Integration](/en/modules/blueprint/vm) - Blueprint VM API
- [Examples](/en/modules/blueprint/examples) - More game logic examples

View File

@@ -3,85 +3,127 @@ title: "Examples"
description: "ECS integration and best practices"
---
## Player Control Blueprint
## Complete Game Integration Example
```typescript
// Define input handling node
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
],
isPure: true
};
import { Scene, Core, Component, ECSComponent } from '@esengine/ecs-framework';
import {
BlueprintSystem,
BlueprintComponent,
BlueprintExpose,
BlueprintProperty,
BlueprintMethod
} from '@esengine/blueprint';
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const input = context.scene.services.get(InputServiceToken);
const direction = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};
return { outputs: { direction } };
// 1. Define game components
@ECSComponent('Player')
@BlueprintExpose({ displayName: 'Player', category: 'gameplay' })
export class PlayerComponent extends Component {
@BlueprintProperty({ displayName: 'Move Speed', type: 'float' })
moveSpeed: number = 5;
@BlueprintProperty({ displayName: 'Score', type: 'int' })
score: number = 0;
@BlueprintMethod({ displayName: 'Add Score' })
addScore(points: number): void {
this.score += points;
}
}
@ECSComponent('Health')
@BlueprintExpose({ displayName: 'Health', category: 'gameplay' })
export class HealthComponent extends Component {
@BlueprintProperty({ displayName: 'Current Health' })
current: number = 100;
@BlueprintProperty({ displayName: 'Max Health' })
max: number = 100;
@BlueprintMethod({ displayName: 'Heal' })
heal(amount: number): void {
this.current = Math.min(this.current + amount, this.max);
}
@BlueprintMethod({ displayName: 'Take Damage' })
takeDamage(amount: number): boolean {
this.current -= amount;
return this.current <= 0;
}
}
// 2. Initialize game
async function initGame() {
const scene = new Scene();
// Add blueprint system
scene.addSystem(new BlueprintSystem());
Core.setScene(scene);
// 3. Create player
const player = scene.createEntity('Player');
player.addComponent(new PlayerComponent());
player.addComponent(new HealthComponent());
// Add blueprint control
const blueprint = new BlueprintComponent();
blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
player.addComponent(blueprint);
}
```
## State Switching Logic
## Custom Node Example
```typescript
// Implement state machine logic in blueprint
const stateBlueprint = createEmptyBlueprint('PlayerState');
import type { Entity } from '@esengine/ecs-framework';
import {
BlueprintNodeTemplate,
BlueprintNode,
ExecutionContext,
ExecutionResult,
INodeExecutor,
RegisterNode
} from '@esengine/blueprint';
// Add state variable
stateBlueprint.variables.push({
name: 'currentState',
type: 'string',
defaultValue: 'idle',
scope: 'instance'
});
// Check state transitions in Tick event
// ... implemented via node connections
```
## Damage Handling System
```typescript
// Custom damage node
const ApplyDamageTemplate: BlueprintNodeTemplate = {
type: 'ApplyDamage',
title: 'Apply Damage',
category: 'combat',
color: '#aa2222',
description: 'Apply damage to entity with Health component',
keywords: ['damage', 'hurt', 'attack'],
menuPath: ['Combat', 'Apply Damage'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'target', type: 'entity', direction: 'input' },
{ name: 'amount', type: 'number', direction: 'input', defaultValue: 10 }
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'target', type: 'entity', displayName: 'Target' },
{ name: 'amount', type: 'float', displayName: 'Damage', defaultValue: 10 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'killed', type: 'boolean', direction: 'output' }
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'killed', type: 'bool', displayName: 'Killed' }
]
};
@RegisterNode(ApplyDamageTemplate)
class ApplyDamageExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const target = context.getInput<Entity>(node.id, 'target');
const amount = context.getInput<number>(node.id, 'amount');
const target = context.evaluateInput(node.id, 'target', context.entity) as Entity;
const amount = context.evaluateInput(node.id, 'amount', 10) as number;
if (!target || target.isDestroyed) {
return { outputs: { killed: false }, nextExec: 'exec' };
}
const health = target.components.find(c =>
(c.constructor as any).__componentName__ === 'Health'
) as any;
const health = target.getComponent(HealthComponent);
if (health) {
health.current -= amount;
const killed = health.current <= 0;
return {
outputs: { killed },
nextExec: 'exec'
};
return { outputs: { killed }, nextExec: 'exec' };
}
return { outputs: { killed: false }, nextExec: 'exec' };
@@ -132,7 +174,8 @@ vm.maxStepsPerFrame = 1000;
```typescript
// Enable debug mode for execution logs
vm.debug = true;
const blueprint = entity.getComponent(BlueprintComponent);
blueprint.debug = true;
// Use Print nodes for intermediate values
// Set breakpoints in editor

View File

@@ -1,414 +1,162 @@
---
title: "Blueprint Visual Scripting"
description: "Visual scripting system deeply integrated with ECS framework"
---
`@esengine/blueprint` provides a full-featured visual scripting system supporting node-based programming, event-driven execution, and blueprint composition.
`@esengine/blueprint` provides a visual scripting system deeply integrated with the ECS framework, supporting node-based programming to control entity behavior.
## Installation
## Editor Download
Blueprint Editor Plugin for Cocos Creator (Free):
**[Download Cocos Node Editor v1.1.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.1.0)**
> QQ Group: **481923584** | Website: [esengine.cn](https://esengine.cn/)
For detailed usage instructions, see [Editor User Guide](./editor-guide).
## Runtime Installation
```bash
npm install @esengine/blueprint
```
## Core Features
- **Deep ECS Integration** - Built-in Entity and Component operation nodes
- **Auto-generated Component Nodes** - Use decorators to mark components, auto-generate Get/Set/Call nodes
- **Runtime Blueprint Execution** - Efficient virtual machine executes blueprint logic
## Quick Start
### 1. Add Blueprint System
```typescript
import { Scene, Core } from '@esengine/ecs-framework';
import { BlueprintSystem } from '@esengine/blueprint';
// Create scene and add blueprint system
const scene = new Scene();
scene.addSystem(new BlueprintSystem());
// Set scene
Core.setScene(scene);
```
### 2. Add Blueprint to Entity
```typescript
import { BlueprintComponent } from '@esengine/blueprint';
// Create entity
const player = scene.createEntity('Player');
// Add blueprint component
const blueprint = new BlueprintComponent();
blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
blueprint.autoStart = true;
player.addComponent(blueprint);
```
### 3. Mark Components (Auto-generate Blueprint Nodes)
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
BlueprintExpose,
BlueprintProperty,
BlueprintMethod
} from '@esengine/blueprint';
import { Component, ECSComponent } from '@esengine/ecs-framework';
// Create blueprint system
const blueprintSystem = createBlueprintSystem(scene);
@ECSComponent('Health')
@BlueprintExpose({ displayName: 'Health', category: 'gameplay' })
export class HealthComponent extends Component {
@BlueprintProperty({ displayName: 'Current Health', type: 'float' })
current: number = 100;
// Load blueprint asset
const blueprint = await loadBlueprintAsset('player.bp');
@BlueprintProperty({ displayName: 'Max Health', type: 'float' })
max: number = 100;
// Create blueprint component data
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
@BlueprintMethod({
displayName: 'Heal',
params: [{ name: 'amount', type: 'float' }]
})
heal(amount: number): void {
this.current = Math.min(this.current + amount, this.max);
}
// Update in game loop
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
@BlueprintMethod({ displayName: 'Take Damage' })
takeDamage(amount: number): boolean {
this.current -= amount;
return this.current <= 0;
}
}
```
## Core Concepts
After marking, the following nodes will appear in the blueprint editor:
- **Get Health** - Get Health component
- **Get Current Health** - Get current property
- **Set Current Health** - Set current property
- **Heal** - Call heal method
- **Take Damage** - Call takeDamage method
### Blueprint Asset Structure
## ECS Integration Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Core.update() │
│ ↓ │
│ Scene.updateSystems() │
│ ↓ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ BlueprintSystem │ │
│ │ │ │
│ │ Matcher.all(BlueprintComponent) │ │
│ │ ↓ │ │
│ │ process(entities) → blueprint.tick() for each entity │ │
│ │ ↓ │ │
│ │ BlueprintVM.tick(dt) │ │
│ │ ↓ │ │
│ │ Execute Event/ECS/Flow Nodes │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Node Types
| Category | Description | Color |
|----------|-------------|-------|
| `event` | Event nodes (BeginPlay, Tick, EndPlay) | Red |
| `entity` | ECS entity operations | Blue |
| `component` | ECS component access | Cyan |
| `flow` | Flow control (Branch, Sequence, Loop) | Gray |
| `math` | Math operations | Green |
| `time` | Time utilities (Delay, GetDeltaTime) | Cyan |
| `debug` | Debug utilities (Print) | Gray |
## Blueprint Asset Structure
Blueprints are saved as `.bp` files:
```typescript
interface BlueprintAsset {
version: number; // Format version
type: 'blueprint'; // Asset type
metadata: BlueprintMetadata; // Metadata
variables: BlueprintVariable[]; // Variable definitions
nodes: BlueprintNode[]; // Node instances
connections: BlueprintConnection[]; // Connections
version: number;
type: 'blueprint';
metadata: {
name: string;
description?: string;
};
variables: BlueprintVariable[];
nodes: BlueprintNode[];
connections: BlueprintConnection[];
}
```
### Node Categories
| Category | Description | Color |
|----------|-------------|-------|
| `event` | Event nodes (entry points) | 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 |
### Pin Types
Nodes connect through pins:
```typescript
interface BlueprintPinDefinition {
name: string; // Pin name
type: PinDataType; // Data type
direction: 'input' | 'output';
isExec?: boolean; // Execution pin
defaultValue?: unknown;
}
type PinDataType =
| 'exec' // Execution flow
| 'boolean' // Boolean
| 'number' // Number
| 'string' // String
| 'vector2' // 2D vector
| 'vector3' // 3D vector
| 'entity' // Entity reference
| 'component' // Component reference
| 'any'; // Any type
```
### Variable Scopes
```typescript
type VariableScope =
| 'local' // Per execution
| 'instance' // Per entity
| 'global'; // Shared globally
```
## Virtual Machine API
### BlueprintVM
The virtual machine executes blueprint graphs:
```typescript
import { BlueprintVM } from '@esengine/blueprint';
const vm = new BlueprintVM(blueprintAsset, entity, scene);
vm.start(); // Start (triggers BeginPlay)
vm.tick(deltaTime); // Update (triggers Tick)
vm.stop(); // Stop (triggers EndPlay)
vm.pause();
vm.resume();
// Trigger events
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });
// Debug mode
vm.debug = true;
```
### Execution Context
```typescript
interface ExecutionContext {
blueprint: BlueprintAsset;
entity: Entity;
scene: IScene;
deltaTime: number;
time: number;
getInput<T>(nodeId: string, pinName: string): T;
setOutput(nodeId: string, pinName: string, value: unknown): void;
getVariable<T>(name: string): T;
setVariable(name: string, value: unknown): void;
}
```
### Execution Result
```typescript
interface ExecutionResult {
outputs?: Record<string, unknown>; // Output values
nextExec?: string | null; // Next exec pin
delay?: number; // Delay execution (ms)
yield?: boolean; // Pause until next frame
error?: string; // Error message
}
```
## Custom Nodes
### Define Node Template
```typescript
import { BlueprintNodeTemplate } from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'A custom node example',
keywords: ['custom', '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' }
]
};
```
### Implement Node Executor
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const value = context.getInput<number>(node.id, 'value');
const result = value * 2;
return {
outputs: { result },
nextExec: 'exec'
};
}
}
```
### Registration Methods
```typescript
// Method 1: Decorator
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }
// Method 2: Manual registration
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
```
## Node Registry
```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')) { ... }
```
## Built-in Nodes
### Event Nodes
| Node | Description |
|------|-------------|
| `EventBeginPlay` | Triggered on blueprint start |
| `EventTick` | Triggered every frame |
| `EventEndPlay` | Triggered on blueprint stop |
| `EventCollision` | Triggered on collision |
| `EventInput` | Triggered on input |
| `EventTimer` | Triggered by timer |
### Time Nodes
| Node | Description |
|------|-------------|
| `Delay` | Delay execution |
| `GetDeltaTime` | Get frame delta |
| `GetTime` | Get total runtime |
### Math Nodes
| Node | Description |
|------|-------------|
| `Add`, `Subtract`, `Multiply`, `Divide` | Basic operations |
| `Abs`, `Clamp`, `Lerp`, `Min`, `Max` | Utility functions |
### Debug Nodes
| Node | Description |
|------|-------------|
| `Print` | Print to console |
## Blueprint Composition
### Blueprint Fragments
Encapsulate reusable logic as fragments:
```typescript
import { createFragment } from '@esengine/blueprint';
const healthFragment = createFragment('HealthSystem', {
inputs: [
{ name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
],
outputs: [
{ name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
],
graph: { nodes: [...], connections: [...], variables: [...] }
});
```
### Compose Blueprints
```typescript
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
// Register fragments
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);
// Create composer
const composer = createComposer('PlayerBlueprint');
// Add fragments to slots
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
// Connect slots
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
// Validate
const validation = composer.validate();
if (!validation.isValid) {
console.error(validation.errors);
}
// Compile to blueprint
const blueprint = composer.compile();
```
## Trigger System
### Define Trigger Conditions
```typescript
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
const lowHealthCondition: TriggerCondition = {
type: 'comparison',
left: { type: 'variable', name: 'health' },
operator: '<',
right: { type: 'constant', value: 20 }
};
```
### Use Trigger Dispatcher
```typescript
const dispatcher = new TriggerDispatcher();
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
context.triggerEvent('OnLowHealth');
});
dispatcher.evaluate(context);
```
## ECS Integration
### Using Blueprint System
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
this.blueprintSystem.process(this.entities, dt);
}
}
```
### Triggering Blueprint Events
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
```
## Serialization
### Save Blueprint
```typescript
import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void {
if (!validateBlueprintAsset(blueprint)) {
throw new Error('Invalid blueprint structure');
}
const json = JSON.stringify(blueprint, null, 2);
fs.writeFileSync(path, json);
}
```
### Load Blueprint
```typescript
async function loadBlueprint(path: string): Promise<BlueprintAsset> {
const json = await fs.readFile(path, 'utf-8');
const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) {
throw new Error('Invalid blueprint file');
}
return asset;
}
```
## Best Practices
1. **Use fragments for reusable logic**
2. **Choose appropriate variable scopes**
- `local`: Temporary calculations
- `instance`: Entity state (e.g., health)
- `global`: Game-wide state
3. **Avoid infinite loops** - VM has max steps per frame (default 1000)
4. **Debug techniques**
- Enable `vm.debug = true` for execution logs
- Use Print nodes for intermediate values
5. **Performance optimization**
- Pure nodes (`isPure: true`) cache outputs
- Avoid heavy computation in Tick
## Documentation
- [Virtual Machine API](./vm) - BlueprintVM execution and context
- [Custom Nodes](./custom-nodes) - Creating custom nodes
- [Built-in Nodes](./nodes) - Built-in node reference
- [Blueprint Composition](./composition) - Fragments and composer
- [Examples](./examples) - ECS integration and best practices
## Documentation Navigation
- [Editor User Guide](./editor-guide) - Cocos Creator Blueprint Editor tutorial
- [Virtual Machine API](./vm) - BlueprintVM and ECS integration
- [ECS Node Reference](./nodes) - Built-in ECS operation nodes
- [Custom Nodes](./custom-nodes) - Create custom ECS nodes
- [Blueprint Composition](./composition) - Fragment reuse
- [Examples](./examples) - ECS game logic examples

View File

@@ -1,107 +1,616 @@
---
title: "Built-in Nodes"
description: "Blueprint built-in node reference"
title: "ECS Node Reference"
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
| Node | Description |
|------|-------------|
| `EventBeginPlay` | Triggered when blueprint starts |
| `EventTick` | Triggered each frame |
| `EventEndPlay` | Triggered when blueprint stops |
| `EventCollision` | Triggered on collision |
| `EventInput` | Triggered on input event |
| `EventTimer` | Triggered by timer |
| `EventMessage` | Triggered by custom message |
Lifecycle events as blueprint entry points:
## Flow Control Nodes
| Node | Description | Outputs |
|------|-------------|---------|
| `EventBeginPlay` | Triggered when blueprint starts | Exec, Self (Entity) |
| `EventTick` | Triggered each frame | Exec, Delta Time |
| `EventEndPlay` | Triggered when blueprint stops | Exec |
| Node | Description |
|------|-------------|
| `Branch` | Conditional branch (if/else) |
| `Sequence` | Execute multiple outputs in sequence |
| `ForLoop` | Loop execution |
| `WhileLoop` | Conditional loop |
| `DoOnce` | Execute only once |
| `FlipFlop` | Alternate between two branches |
| `Gate` | Toggleable execution gate |
### Example: Game Initialization
## Time Nodes
<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>
| Node | Description |
|------|-------------|
| `Delay` | Delay execution |
| `GetDeltaTime` | Get frame delta time |
| `GetTime` | Get runtime |
| `SetTimer` | Set timer |
| `ClearTimer` | Clear timer |
### Example: Per-Frame Movement
## Math Nodes
| Node | Description |
|------|-------------|
| `Add` | Addition |
| `Subtract` | Subtraction |
| `Multiply` | Multiplication |
| `Divide` | Division |
| `Abs` | Absolute value |
| `Clamp` | Clamp to range |
| `Lerp` | Linear interpolation |
| `Min` / `Max` | Minimum/Maximum |
| `Sin` / `Cos` | Trigonometric functions |
| `Sqrt` | Square root |
| `Power` | Power |
## Logic Nodes
| Node | Description |
|------|-------------|
| `And` | Logical AND |
| `Or` | Logical OR |
| `Not` | Logical NOT |
| `Equal` | Equality comparison |
| `NotEqual` | Inequality comparison |
| `Greater` | Greater than comparison |
| `Less` | Less than comparison |
## Vector Nodes
| Node | Description |
|------|-------------|
| `MakeVector2` | Create 2D vector |
| `BreakVector2` | Break 2D vector |
| `VectorAdd` | Vector addition |
| `VectorSubtract` | Vector subtraction |
| `VectorMultiply` | Vector multiplication |
| `VectorLength` | Vector length |
| `VectorNormalize` | Vector normalization |
| `VectorDistance` | Vector distance |
<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
Manipulate ECS entities:
| Node | Description | Type |
|------|-------------|------|
| `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 | Exec |
| `Find Entity By Name` | Find entity by name | 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
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 |
| `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 blueprint execution flow:
| Node | Description |
|------|-------------|
| `GetSelf` | Get current entity |
| `GetComponent` | Get component |
| `HasComponent` | Check component |
| `AddComponent` | Add component |
| `RemoveComponent` | Remove component |
| `SpawnEntity` | Create entity |
| `DestroyEntity` | Destroy entity |
| `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 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 | Output |
|------|-------------|--------|
| `Delay` | Delay execution by specified seconds | Exec |
| `Get Delta Time` | Get frame delta time | Float |
| `Get Time` | Get total runtime | Float |
### Example: Delayed Execution
<div class="bp-graph" style="" data-connections='[{"from":"en-delay-bp-exec","to":"en-delay-exec","type":"exec"},{"from":"en-delay-done","to":"en-delay-print-exec","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event BeginPlay</span>
<span class="bp-header-exec" data-pin="en-delay-bp-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
</div>
<div class="bp-node" style="left: 280px; top: 20px; width: 120px;">
<div class="bp-node-header time">Delay</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="en-delay-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Duration</span>
<span class="bp-pin-value">2.0</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="en-delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Done</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 490px; top: 20px; width: 130px;">
<div class="bp-node-header debug">Print</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="en-delay-print-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Msg</span>
<span class="bp-pin-value">"After 2s"</span>
</div>
</div>
</div>
</div>
## Math Nodes
### Basic Operations
| Node | Description | Inputs | Output |
|------|-------------|--------|--------|
| `Add` | Addition | A, B | A + B |
| `Subtract` | Subtraction | A, B | A - B |
| `Multiply` | Multiplication | A, B | A × B |
| `Divide` | Division | A, B | A / B |
| `Modulo` | Modulo | A, B | A % B |
### Math Functions
| Node | Description | Inputs | Output |
|------|-------------|--------|--------|
| `Abs` | Absolute value | Value | \|Value\| |
| `Sqrt` | Square root | Value | √Value |
| `Pow` | Power | Base, Exp | Base^Exp |
| `Floor` | Floor | Value | ⌊Value⌋ |
| `Ceil` | Ceiling | Value | ⌈Value⌉ |
| `Round` | Round | Value | round(Value) |
| `Clamp` | Clamp to range | Value, Min, Max | min(max(V, Min), Max) |
| `Lerp` | Linear interpolation | A, B, Alpha | A + (B-A) × Alpha |
| `Min` | Minimum | A, B | min(A, B) |
| `Max` | Maximum | A, B | max(A, B) |
### Trigonometric Functions
| Node | Description |
|------|-------------|
| `Sin` | Sine |
| `Cos` | Cosine |
| `Tan` | Tangent |
| `Asin` | Arc sine |
| `Acos` | Arc cosine |
| `Atan` | Arc tangent |
| `Atan2` | Two-argument arc tangent |
### Random Numbers
| Node | Description | Inputs | Output |
|------|-------------|--------|--------|
| `Random` | Random float [0, 1) | - | Float |
| `Random Range` | Random in range | Min, Max | Float |
| `Random Int` | Random integer | Min, Max | Int |
### Comparison Nodes
| Node | Description | Output |
|------|-------------|--------|
| `Equal` | A == B | Boolean |
| `Not Equal` | A != B | Boolean |
| `Greater` | A > B | Boolean |
| `Greater Or Equal` | A >= B | Boolean |
| `Less` | A < B | Boolean |
| `Less Or Equal` | A <= B | Boolean |
### Extended Math Nodes
> **Vector2, Fixed32, FixedVector2, Color** and other advanced math nodes are provided by the `@esengine/ecs-framework-math` module.
>
> See: [Math Blueprint Nodes](/en/modules/math/blueprint-nodes)
### Example: Clamp Value
<div class="bp-graph" style="" data-connections='[{"from":"en-rand-result","to":"en-clamp-value","type":"float"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 130px;">
<div class="bp-node-header math">Random Range</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Min</span>
<span class="bp-pin-value">0</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Max</span>
<span class="bp-pin-value">100</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="en-rand-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Result</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
<div class="bp-node-header math">Clamp</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="en-clamp-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Value</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Min</span>
<span class="bp-pin-value">20</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Max</span>
<span class="bp-pin-value">80</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Result</span>
</div>
</div>
</div>
</div>
## Variable Nodes
| Node | Description |
|------|-------------|
| `GetVariable` | Get variable value |
| `SetVariable` | Set variable value |
Blueprint-defined variables automatically generate Get and Set nodes:
| Node | Description | Type |
|------|-------------|------|
| `Get <varname>` | Read variable value | Pure |
| `Set <varname>` | Set variable value | Exec |
### Example: Counter
<div class="bp-graph" style="" data-connections='[{"from":"en-cnt-tick-exec","to":"en-cnt-add-exec","type":"exec"},{"from":"en-cnt-get-value","to":"en-cnt-add-a","type":"int"},{"from":"en-cnt-add-result","to":"en-cnt-set-value","type":"int"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event Tick</span>
<span class="bp-header-exec" data-pin="en-cnt-tick-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
<div class="bp-node-body">
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Delta</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 20px; top: 120px; width: 110px;">
<div class="bp-node-header variable">Get Count</div>
<div class="bp-node-body">
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="en-cnt-get-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">Value</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 240px; top: 20px; width: 110px;">
<div class="bp-node-header math">Add</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="en-cnt-add-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="en-cnt-add-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">A</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
<span class="bp-pin-label">B</span>
<span class="bp-pin-value">1</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="en-cnt-add-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">Result</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 430px; top: 20px; width: 110px;">
<div class="bp-node-header variable">Set Count</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="en-cnt-set-value"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">Value</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
</div>
## Debug Nodes
| Node | Description |
|------|-------------|
| `Print` | Print to console |
| `DrawDebugLine` | Draw debug line |
| `DrawDebugPoint` | Draw debug point |
| `Breakpoint` | Debug breakpoint |
| `Print` | Output message to console |
## Related Documentation
- [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

@@ -45,7 +45,7 @@ interface ExecutionContext {
time: number; // Total runtime
// Get input value
getInput<T>(nodeId: string, pinName: string): T;
evaluateInput(nodeId: string, pinName: string, defaultValue: unknown): unknown;
// Set output value
setOutput(nodeId: string, pinName: string, value: unknown): void;
@@ -70,35 +70,33 @@ interface ExecutionResult {
## ECS Integration
### Using Blueprint System
### Using Built-in Blueprint System
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
import { Scene, Core } from '@esengine/ecs-framework';
import { BlueprintSystem, BlueprintComponent } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
// Add blueprint system to scene
const scene = new Scene();
scene.addSystem(new BlueprintSystem());
Core.setScene(scene);
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
// Process all entities with blueprint components
this.blueprintSystem.process(this.entities, dt);
}
}
// Add blueprint to entity
const entity = scene.createEntity('Player');
const blueprint = new BlueprintComponent();
blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
entity.addComponent(blueprint);
```
### Triggering Blueprint Events
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
// Trigger built-in event
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
// Trigger custom event
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
// Get blueprint component from entity and trigger events
const blueprint = entity.getComponent(BlueprintComponent);
if (blueprint?.vm) {
blueprint.vm.triggerEvent('EventCollision', { other: otherEntity });
blueprint.vm.triggerCustomEvent('OnPickup', { item: itemEntity });
}
```
## Serialization

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,441 @@
---
title: "Distributed Rooms"
description: "Multi-server room management with DistributedRoomManager"
---
## Overview
Distributed room support allows multiple server instances to share a room registry, enabling cross-server player routing and failover.
```
┌─────────────────────────────────────────────────────────┐
│ Server A Server B Server C │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Room 1 │ │ Room 3 │ │ Room 5 │ │
│ │ Room 2 │ │ Room 4 │ │ Room 6 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IDistributedAdapter │ │
│ │ (Redis / Memory) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Quick Start
### Single Server Mode (Testing)
```typescript
import {
DistributedRoomManager,
MemoryAdapter,
Room
} from '@esengine/server';
// Define room type
class GameRoom extends Room {
maxPlayers = 4;
}
// Create adapter and manager
const adapter = new MemoryAdapter();
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000
}, (conn, type, data) => conn.send(JSON.stringify({ type, data })));
// Register room type
manager.define('game', GameRoom);
// Start manager
await manager.start();
// Distributed join/create room
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// Player should connect to another server
console.log(`Redirect to: ${result.redirect}`);
} else {
// Player joined local room
const { room, player } = result;
}
// Graceful shutdown
await manager.stop(true);
```
### Multi-Server Mode (Production)
```typescript
import Redis from 'ioredis';
import { DistributedRoomManager, RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis({
host: 'redis.example.com',
port: 6379
}),
prefix: 'game:',
serverTtl: 30,
snapshotTtl: 86400
});
const manager = new DistributedRoomManager(adapter, {
serverId: process.env.SERVER_ID,
serverAddress: process.env.PUBLIC_IP,
serverPort: 3000,
heartbeatInterval: 5000,
snapshotInterval: 30000,
enableFailover: true,
capacity: 100
}, sendFn);
```
## DistributedRoomManager
### Configuration Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `serverId` | `string` | required | Unique server identifier |
| `serverAddress` | `string` | required | Public address for client connections |
| `serverPort` | `number` | required | Server port |
| `heartbeatInterval` | `number` | `5000` | Heartbeat interval (ms) |
| `snapshotInterval` | `number` | `30000` | State snapshot interval, 0 to disable |
| `migrationTimeout` | `number` | `10000` | Room migration timeout |
| `enableFailover` | `boolean` | `true` | Enable automatic failover |
| `capacity` | `number` | `100` | Max rooms on this server |
### Lifecycle Methods
#### start()
Start the distributed room manager. Connects to adapter, registers server, starts heartbeat.
```typescript
await manager.start();
```
#### stop(graceful?)
Stop the manager. If `graceful=true`, marks server as draining and saves all room snapshots.
```typescript
await manager.stop(true);
```
### Routing Methods
#### joinOrCreateDistributed()
Join or create a room with distributed awareness. Returns `{ room, player }` for local rooms or `{ redirect: string }` for remote rooms.
```typescript
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// Client should redirect to another server
res.json({ redirect: result.redirect });
} else {
// Player joined local room
const { room, player } = result;
}
```
#### route()
Route a player to the appropriate room/server.
```typescript
const result = await manager.route({
roomType: 'game',
playerId: 'p1'
});
switch (result.type) {
case 'local': // Room is on this server
break;
case 'redirect': // Room is on another server
// result.serverAddress contains target server
break;
case 'create': // No room exists, need to create
break;
case 'unavailable': // Cannot find or create room
// result.reason contains error message
break;
}
```
### State Management
#### saveSnapshot()
Manually save a room's state snapshot.
```typescript
await manager.saveSnapshot(roomId);
```
#### restoreFromSnapshot()
Restore a room from its saved snapshot.
```typescript
const success = await manager.restoreFromSnapshot(roomId);
```
### Query Methods
#### getServers()
Get all online servers.
```typescript
const servers = await manager.getServers();
```
#### queryDistributedRooms()
Query rooms across all servers.
```typescript
const rooms = await manager.queryDistributedRooms({
roomType: 'game',
hasSpace: true,
notLocked: true
});
```
## IDistributedAdapter
Interface for distributed backends. Implement this to add support for Redis, message queues, etc.
### Built-in Adapters
#### MemoryAdapter
In-memory implementation for testing and single-server mode.
```typescript
const adapter = new MemoryAdapter({
serverTtl: 15000, // Server offline after no heartbeat (ms)
enableTtlCheck: true, // Enable automatic TTL checking
ttlCheckInterval: 5000 // TTL check interval (ms)
});
```
#### RedisAdapter
Redis-based implementation for production multi-server deployments.
```typescript
import Redis from 'ioredis';
import { RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis('redis://localhost:6379'),
prefix: 'game:', // Key prefix (default: 'dist:')
serverTtl: 30, // Server TTL in seconds (default: 30)
roomTtl: 0, // Room TTL, 0 = never expire (default: 0)
snapshotTtl: 86400, // Snapshot TTL in seconds (default: 24h)
channel: 'game:events' // Pub/Sub channel (default: 'distributed:events')
});
```
**RedisAdapter Configuration:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `factory` | `() => RedisClient` | required | Redis client factory (lazy connection) |
| `prefix` | `string` | `'dist:'` | Key prefix for all Redis keys |
| `serverTtl` | `number` | `30` | Server TTL in seconds |
| `roomTtl` | `number` | `0` | Room TTL in seconds, 0 = no expiry |
| `snapshotTtl` | `number` | `86400` | Snapshot TTL in seconds |
| `channel` | `string` | `'distributed:events'` | Pub/Sub channel name |
**Features:**
- Server registry with automatic heartbeat TTL
- Room registry with cross-server lookup
- State snapshots with configurable TTL
- Pub/Sub for cross-server events
- Distributed locks using Redis SET NX
### Custom Adapters
```typescript
import type { IDistributedAdapter } from '@esengine/server';
class MyAdapter implements IDistributedAdapter {
// Lifecycle
async connect(): Promise<void> { }
async disconnect(): Promise<void> { }
isConnected(): boolean { return true; }
// Server Registry
async registerServer(server: ServerRegistration): Promise<void> { }
async unregisterServer(serverId: string): Promise<void> { }
async heartbeat(serverId: string): Promise<void> { }
async getServers(): Promise<ServerRegistration[]> { return []; }
// Room Registry
async registerRoom(room: RoomRegistration): Promise<void> { }
async unregisterRoom(roomId: string): Promise<void> { }
async queryRooms(query: RoomQuery): Promise<RoomRegistration[]> { return []; }
async findAvailableRoom(roomType: string): Promise<RoomRegistration | null> { return null; }
// State Snapshots
async saveSnapshot(snapshot: RoomSnapshot): Promise<void> { }
async loadSnapshot(roomId: string): Promise<RoomSnapshot | null> { return null; }
// Pub/Sub
async publish(event: DistributedEvent): Promise<void> { }
async subscribe(pattern: string, handler: Function): Promise<() => void> { return () => {}; }
// Distributed Locks
async acquireLock(key: string, ttlMs: number): Promise<boolean> { return true; }
async releaseLock(key: string): Promise<void> { }
}
```
## Player Routing Flow
```
Client Server A Server B
│ │ │
│─── joinOrCreate ────────►│ │
│ │ │
│ │── findAvailableRoom() ───►│
│ │◄──── room on Server B ────│
│ │ │
│◄─── redirect: B:3001 ────│ │
│ │ │
│───────────────── connect to Server B ───────────────►│
│ │ │
│◄─────────────────────────────── joined ─────────────│
```
## Event Types
The distributed system publishes these events:
| Event | Description |
|-------|-------------|
| `server:online` | Server came online |
| `server:offline` | Server went offline |
| `server:draining` | Server is draining |
| `room:created` | Room was created |
| `room:disposed` | Room was disposed |
| `room:updated` | Room info updated |
| `room:message` | Cross-server room message |
| `room:migrated` | Room migrated to another server |
| `player:joined` | Player joined room |
| `player:left` | Player left room |
## Best Practices
1. **Use Unique Server IDs** - Use hostname, container ID, or UUID
2. **Configure Proper Heartbeat** - Balance between freshness and network overhead
3. **Enable Snapshots for Stateful Rooms** - Ensure room state survives server restarts
4. **Handle Redirects Gracefully** - Client should reconnect to target server
```typescript
// Client handling redirect
if (response.redirect) {
await client.disconnect();
await client.connect(response.redirect);
await client.joinRoom(roomId);
}
```
5. **Use Distributed Locks** - Prevent race conditions in joinOrCreate
## Using createServer Integration
The simplest way to use distributed rooms is through `createServer`'s `distributed` config:
```typescript
import { createServer } from '@esengine/server';
import { RedisAdapter, Room } from '@esengine/server';
import Redis from 'ioredis';
class GameRoom extends Room {
maxPlayers = 4;
}
const server = await createServer({
port: 3000,
distributed: {
enabled: true,
adapter: new RedisAdapter({ factory: () => new Redis() }),
serverId: 'server-1',
serverAddress: 'ws://192.168.1.100',
serverPort: 3000,
enableFailover: true,
capacity: 100
}
});
server.define('game', GameRoom);
await server.start();
```
When clients call the `JoinRoom` API, the server will automatically:
1. Find available rooms (local or remote)
2. If room is on another server, send `$redirect` message to client
3. Client receives redirect and connects to target server
## Load Balancing
Use `LoadBalancedRouter` for server selection:
```typescript
import { LoadBalancedRouter, createLoadBalancedRouter } from '@esengine/server';
// Using factory function
const router = createLoadBalancedRouter('least-players');
// Or create directly
const router = new LoadBalancedRouter({
strategy: 'least-rooms', // Select server with fewest rooms
preferLocal: true // Prefer local server
});
// Available strategies
// - 'round-robin': Round robin selection
// - 'least-rooms': Fewest rooms
// - 'least-players': Fewest players
// - 'random': Random selection
// - 'weighted': Weighted by capacity usage
```
## Failover
When a server goes offline with `enableFailover` enabled, the system will automatically:
1. Detect server offline (via heartbeat timeout)
2. Query all rooms on that server
3. Use distributed lock to prevent multiple servers recovering same room
4. Restore room state from snapshot
5. Publish `room:migrated` event to notify other servers
```typescript
// Ensure periodic snapshots
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000,
snapshotInterval: 30000, // Save snapshot every 30 seconds
enableFailover: true // Enable failover
}, sendFn);
```
## Future Releases
- Redis Cluster support
- More load balancing strategies (geo-location, latency-aware)

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

@@ -147,6 +147,7 @@ service.on('chat', (data) => {
- [Client Usage](/en/modules/network/client/) - NetworkPlugin, components and systems
- [Server Side](/en/modules/network/server/) - GameServer and Room management
- [Distributed Rooms](/en/modules/network/distributed/) - Multi-server room management and player routing
- [State Sync](/en/modules/network/sync/) - Interpolation and snapshot buffering
- [Client Prediction](/en/modules/network/prediction/) - Input prediction and server reconciliation
- [Area of Interest (AOI)](/en/modules/network/aoi/) - View filtering and bandwidth optimization

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

@@ -266,6 +266,122 @@ class GameRoom extends Room {
}
```
## Schema Validation
Use the built-in Schema validation system for runtime type validation:
### Basic Usage
```typescript
import { s, defineApiWithSchema } from '@esengine/server'
// Define schema
const MoveSchema = s.object({
x: s.number(),
y: s.number(),
speed: s.number().optional()
})
// Auto type inference
type Move = s.infer<typeof MoveSchema> // { x: number; y: number; speed?: number }
// Use schema to define API (auto validation)
export default defineApiWithSchema(MoveSchema, {
handler(req, ctx) {
// req is validated, type-safe
console.log(req.x, req.y)
}
})
```
### Validator Types
| Type | Example | Description |
|------|---------|-------------|
| `s.string()` | `s.string().min(1).max(50)` | String with length constraints |
| `s.number()` | `s.number().min(0).int()` | Number with range and integer constraints |
| `s.boolean()` | `s.boolean()` | Boolean |
| `s.literal()` | `s.literal('admin')` | Literal type |
| `s.object()` | `s.object({ name: s.string() })` | Object |
| `s.array()` | `s.array(s.number())` | Array |
| `s.enum()` | `s.enum(['a', 'b'] as const)` | Enum |
| `s.union()` | `s.union([s.string(), s.number()])` | Union type |
| `s.record()` | `s.record(s.any())` | Record type |
### Modifiers
```typescript
// Optional field
s.string().optional()
// Default value
s.number().default(0)
// Nullable
s.string().nullable()
// String validation
s.string().min(1).max(100).email().url().regex(/^[a-z]+$/)
// Number validation
s.number().min(0).max(100).int().positive()
// Array validation
s.array(s.string()).min(1).max(10).nonempty()
// Object validation
s.object({ ... }).strict() // No extra fields allowed
s.object({ ... }).partial() // All fields optional
s.object({ ... }).pick('name', 'age') // Pick fields
s.object({ ... }).omit('password') // Omit fields
```
### Message Validation
```typescript
import { s, defineMsgWithSchema } from '@esengine/server'
const InputSchema = s.object({
keys: s.array(s.string()),
timestamp: s.number()
})
export default defineMsgWithSchema(InputSchema, {
handler(msg, ctx) {
// msg is validated
console.log(msg.keys, msg.timestamp)
}
})
```
### Manual Validation
```typescript
import { s, parse, safeParse, createGuard } from '@esengine/server'
const UserSchema = s.object({
name: s.string(),
age: s.number().int().min(0)
})
// Throws on error
const user = parse(UserSchema, data)
// Returns result object
const result = safeParse(UserSchema, data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error)
}
// Type guard
const isUser = createGuard(UserSchema)
if (isUser(data)) {
// data is User type
}
```
## Protocol Definition
Define shared types in `src/shared/protocol.ts`:

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
@@ -28,21 +186,16 @@ const MyNodeTemplate: BlueprintNodeTemplate = {
## 实现节点执行器
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
import { INodeExecutor, RegisterNode, BlueprintNode, ExecutionContext, ExecutionResult } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
// 获取输入
const value = context.getInput<number>(node.id, 'value');
// 执行逻辑
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,30 +243,3 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
]
};
```
## 实际示例:输入处理节点
```typescript
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
],
isPure: true
};
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const input = context.scene.services.get(InputServiceToken);
const direction = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};
return { outputs: { direction } };
}
}
```

View File

@@ -0,0 +1,876 @@
---
title: "蓝图编辑器使用指南"
description: "Cocos Creator 蓝图可视化脚本编辑器完整使用教程"
---
<script src="/js/blueprint-graph.js"></script>
本指南介绍如何在 Cocos Creator 中使用蓝图可视化脚本编辑器。
## 下载与安装
### 下载
从 GitHub Release 下载最新版本(免费):
**[下载 Cocos Node Editor v1.1.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.1.0)**
> 技术交流 QQ 群:**481923584** | 官网:[esengine.cn](https://esengine.cn/)
### 安装步骤
1. 解压 `cocos-node-editor.zip` 到项目的 `extensions` 目录:
```
your-project/
├── assets/
├── extensions/
│ └── cocos-node-editor/ ← 解压到这里
└── ...
```
2. 重启 Cocos Creator
3. 通过菜单 **扩展 → 扩展管理器** 确认插件已启用
4. 通过菜单 **面板 → Node Editor** 打开编辑器
## 界面介绍
- **工具栏** - 位于顶部,包含新建、打开、保存、撤销、重做等操作
- **变量面板** - 位于左上角,用于定义和管理蓝图变量
- **画布区域** - 主区域,用于放置和连接节点
- **节点菜单** - 右键点击画布空白处打开,按分类列出所有可用节点
## 画布操作
| 操作 | 方式 |
|------|------|
| 平移画布 | 鼠标中键拖拽 / Alt + 左键拖拽 |
| 缩放画布 | 鼠标滚轮 |
| 打开节点菜单 | 右键点击空白处 |
| 框选多个节点 | 在空白处拖拽 |
| 追加框选 | Ctrl + 拖拽 |
| 删除选中 | Delete 键 |
## 节点操作
### 添加节点
1. **从节点面板拖拽** - 将节点从左侧面板拖到画布
2. **右键菜单** - 右键点击画布空白处,选择节点
### 连接节点
1. 从输出引脚拖拽到输入引脚
2. 兼容类型的引脚会高亮显示
3. 松开鼠标完成连接
**引脚类型说明:**
| 引脚颜色 | 类型 | 说明 |
|---------|------|------|
| 白色 ▶ | Exec | 执行流程(控制执行顺序) |
| 青色 ◆ | Entity | 实体引用 |
| 紫色 ◆ | Component | 组件引用 |
| 浅蓝 ◆ | String | 字符串 |
| 绿色 ◆ | Number | 数值 |
| 红色 ◆ | Boolean | 布尔值 |
| 灰色 ◆ | Any | 任意类型 |
### 删除连接
点击连接线选中,按 Delete 键删除。
## 节点类型详解
### 事件节点 (Event)
事件节点是蓝图的入口点,当特定事件发生时触发执行。
| 节点 | 触发时机 | 输出 |
|------|---------|------|
| **Event BeginPlay** | 蓝图开始运行时 | Exec, Self (实体) |
| **Event Tick** | 每帧执行 | Exec, Delta Time |
| **Event EndPlay** | 蓝图停止时 | Exec |
**示例:游戏开始时打印消息**
<div class="bp-graph" style="" data-connections='[{"from":"eg1-exec","to":"eg1-print","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event BeginPlay</span>
<span class="bp-header-exec" data-pin="eg1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
<div class="bp-node-body">
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Self</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
<div class="bp-node-header debug">Print</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg1-print"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Message</span>
<span class="bp-pin-value">"游戏开始!"</span>
</div>
</div>
</div>
</div>
### 实体节点 (Entity)
操作 ECS 实体的节点。
| 节点 | 功能 | 输入 | 输出 |
|------|------|------|------|
| **Get Self** | 获取当前实体 | - | Entity |
| **Create Entity** | 创建新实体 | Exec, Name | Exec, Entity |
| **Destroy Entity** | 销毁实体 | Exec, Entity | Exec |
| **Find Entity By Name** | 按名称查找 | Name | Entity |
| **Find Entities By Tag** | 按标签查找 | Tag | Entity[] |
| **Is Valid** | 检查实体有效性 | Entity | Boolean |
| **Get/Set Entity Name** | 获取/设置名称 | Entity | String |
| **Set Active** | 设置激活状态 | Exec, Entity, Active | Exec |
**示例:创建新实体**
<div class="bp-graph" style="" data-connections='[{"from":"eg2-exec","to":"eg2-create","type":"exec"},{"from":"eg2-create-out","to":"eg2-add","type":"exec"},{"from":"eg2-entity","to":"eg2-add-entity","type":"entity"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event BeginPlay</span>
<span class="bp-header-exec" data-pin="eg2-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
</div>
<div class="bp-node" style="left: 280px; top: 20px; width: 150px;">
<div class="bp-node-header function">Create Entity</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg2-create"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Name</span>
<span class="bp-pin-value">"Bullet"</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg2-create-out"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg2-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Entity</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 520px; top: 20px; width: 150px;">
<div class="bp-node-header function">Add Transform</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg2-add"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg2-add-entity"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Entity</span>
</div>
</div>
</div>
</div>
### 组件节点 (Component)
访问和操作 ECS 组件。
| 节点 | 功能 |
|------|------|
| **Has Component** | 检查实体是否有指定组件 |
| **Get Component** | 获取组件实例 |
| **Add Component** | 添加组件到实体 |
| **Remove Component** | 移除组件 |
| **Get/Set Property** | 获取/设置组件属性 |
**示例:修改 Transform 组件**
<div class="bp-graph" style="" data-connections='[{"from":"eg3-self","to":"eg3-getcomp","type":"entity"},{"from":"eg3-comp","to":"eg3-setprop","type":"component"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 100px;">
<div class="bp-node-header pure">Get Self</div>
<div class="bp-node-body">
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg3-self"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Entity</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 200px; top: 20px; width: 150px;">
<div class="bp-node-header pure">Get Component</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg3-getcomp"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Entity</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg3-comp"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
<span class="bp-pin-label">Transform</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 430px; top: 20px; width: 130px;">
<div class="bp-node-header function">Set Property</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg3-setprop"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7030c0"/></svg></span>
<span class="bp-pin-label">Target</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">x</span>
<span class="bp-pin-value">100</span>
</div>
</div>
</div>
</div>
### 流程控制节点 (Flow)
控制执行流程的节点。
#### Branch (分支)
条件判断,类似 if/else。
<div class="bp-graph" style="" data-connections='[{"from":"eg4-true","to":"eg4-do1","type":"exec"},{"from":"eg4-false","to":"eg4-do2","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 50px; width: 110px;">
<div class="bp-node-header flow">Branch</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#8c0000" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Condition</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg4-true"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">True</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg4-false"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">False</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 220px; top: 20px; width: 130px;">
<div class="bp-node-header function">DoSomething</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg4-do1"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 220px; top: 110px; width: 130px;">
<div class="bp-node-header function">DoOtherThing</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg4-do2"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
</div>
#### Sequence (序列)
按顺序执行多个分支。
<div class="bp-graph" style="" data-connections='[{"from":"eg5-then0","to":"eg5-step1","type":"exec"},{"from":"eg5-then1","to":"eg5-step2","type":"exec"},{"from":"eg5-then2","to":"eg5-step3","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 110px;">
<div class="bp-node-header flow">Sequence</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg5-then0"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Then 0</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg5-then1"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Then 1</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg5-then2"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Then 2</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 220px; top: 20px; width: 100px;">
<div class="bp-node-header function">Step 1</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg5-step1"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 220px; top: 100px; width: 100px;">
<div class="bp-node-header function">Step 2</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg5-step2"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 220px; top: 180px; width: 100px;">
<div class="bp-node-header function">Step 3</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg5-step3"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
</div>
#### For Loop (循环)
循环执行指定次数。
<div class="bp-graph" style="" data-connections='[{"from":"eg6-body","to":"eg6-iter","type":"exec"},{"from":"eg6-done","to":"eg6-finish","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
<div class="bp-node-header flow">For Loop</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
<span class="bp-pin-label">First Index</span>
<span class="bp-pin-value">0</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Last Index</span>
<span class="bp-pin-value">10</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg6-body"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Loop Body</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">Index</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg6-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Completed</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 250px; top: 80px; width: 130px;">
<div class="bp-node-header function">每次迭代执行</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg6-iter"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 250px; top: 160px; width: 140px;">
<div class="bp-node-header function">循环结束后执行</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg6-finish"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
</div>
</div>
</div>
#### For Each (遍历)
遍历数组元素。
#### While Loop (条件循环)
当条件为真时持续循环。
#### Do Once (单次执行)
只执行一次,之后跳过。
#### Flip Flop (交替执行)
每次执行时交替触发 A 和 B 输出。
#### Gate (门)
可通过 Open/Close/Toggle 控制是否允许执行通过。
### 时间节点 (Time)
| 节点 | 功能 | 输出类型 |
|------|------|---------|
| **Delay** | 延迟指定时间后继续执行 | Exec |
| **Get Delta Time** | 获取帧间隔时间 | Number |
| **Get Time** | 获取运行总时间 | Number |
**示例:延迟 2 秒后执行**
<div class="bp-graph" style="" data-connections='[{"from":"eg7-exec","to":"eg7-delay","type":"exec"},{"from":"eg7-done","to":"eg7-print","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event BeginPlay</span>
<span class="bp-header-exec" data-pin="eg7-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
</div>
<div class="bp-node" style="left: 280px; top: 20px; width: 120px;">
<div class="bp-node-header time">Delay</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg7-delay"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Duration</span>
<span class="bp-pin-value">2.0</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="eg7-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Done</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 490px; top: 20px; width: 130px;">
<div class="bp-node-header debug">Print</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="eg7-print"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Msg</span>
<span class="bp-pin-value">"2秒后执行"</span>
</div>
</div>
</div>
</div>
### 数学节点 (Math)
| 节点 | 功能 |
|------|------|
| **Add / Subtract / Multiply / Divide** | 四则运算 |
| **Abs** | 绝对值 |
| **Clamp** | 限制在范围内 |
| **Lerp** | 线性插值 |
| **Min / Max** | 最小/最大值 |
| **Random Range** | 随机数 |
| **Sin / Cos / Tan** | 三角函数 |
### 调试节点 (Debug)
| 节点 | 功能 |
|------|------|
| **Print** | 输出到控制台 |
## 变量系统
变量用于在蓝图中存储和共享数据。
### 创建变量
1. 在变量面板点击 **+** 按钮
2. 输入变量名称
3. 选择变量类型
4. 设置默认值(可选)
### 使用变量
- **拖拽到画布** - 创建 Get 或 Set 节点
- **Get 节点** - 读取变量值
- **Set 节点** - 写入变量值
### 变量类型
| 类型 | 说明 | 默认值 |
|------|------|--------|
| Boolean | 布尔值 | false |
| Number | 数值 | 0 |
| String | 字符串 | "" |
| Entity | 实体引用 | null |
| Vector2 | 二维向量 | (0, 0) |
| Vector3 | 三维向量 | (0, 0, 0) |
### 变量节点错误状态
如果删除了一个变量,但画布上还有引用该变量的节点:
- 节点会显示 **红色边框****警告图标**
- 需要重新创建变量或删除这些节点
## 节点分组
可以将多个节点组织到一个可视化组框中,便于整理复杂蓝图。
### 创建组
1. 框选或 Ctrl+点击 选中多个节点(至少 2 个)
2. 右键点击选中的节点
3. 选择 **创建分组**
4. 组框会自动包裹所有选中的节点
### 组操作
| 操作 | 方式 |
|------|------|
| 移动组 | 拖拽组框头部,所有节点一起移动 |
| 取消分组 | 右键点击组框 → **取消分组** |
### 特性
- **动态大小**:组框会自动调整大小以包裹所有节点
- **独立移动**:可以单独移动组内的节点,组框会自动调整
- **仅编辑器**:组是纯视觉组织,不影响运行时逻辑
## 快捷键
| 快捷键 | 功能 |
|--------|------|
| `Ctrl + S` | 保存蓝图 |
| `Ctrl + Z` | 撤销 |
| `Ctrl + Shift + Z` | 重做 |
| `Ctrl + C` | 复制选中节点 |
| `Ctrl + X` | 剪切选中节点 |
| `Ctrl + V` | 粘贴节点 |
| `Delete` | 删除选中项 |
| `Ctrl + A` | 全选 |
## 保存与加载
### 保存蓝图
1. 点击工具栏 **保存** 按钮
2. 选择保存位置(**必须保存在 `assets/resources` 目录下**,否则 Cocos Creator 无法动态加载)
3. 文件扩展名为 `.blueprint.json`
> **重要提示**:蓝图文件必须放在 `resources` 目录下,游戏运行时才能通过 `cc.resources.load()` 加载。
### 加载蓝图
1. 点击工具栏 **打开** 按钮
2. 选择 `.blueprint.json` 文件
### 蓝图文件格式
蓝图保存为 JSON 格式,可与 `@esengine/blueprint` 运行时兼容:
```json
{
"version": 1,
"type": "blueprint",
"metadata": {
"name": "PlayerController",
"description": "玩家控制逻辑"
},
"variables": [],
"nodes": [],
"connections": []
}
```
## 实战示例
### 示例 1移动控制
实现每帧移动实体:
<div class="bp-graph" style="" data-connections='[{"from":"ex1-exec","to":"ex1-setprop","type":"exec"},{"from":"ex1-delta","to":"ex1-mul-a","type":"float"},{"from":"ex1-mul-result","to":"ex1-x","type":"float"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 140px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event Tick</span>
<span class="bp-header-exec" data-pin="ex1-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
<div class="bp-node-body">
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex1-delta"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Delta Time</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 200px; top: 110px; width: 120px;">
<div class="bp-node-header math">Multiply</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex1-mul-a"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">A</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">B (Speed)</span>
<span class="bp-pin-value">100</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex1-mul-result"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">Result</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 380px; top: 20px; width: 150px;">
<div class="bp-node-header function">Set Property</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex1-setprop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7030c0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Target</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex1-x"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#7ecd32"/></svg></span>
<span class="bp-pin-label">x</span>
</div>
</div>
</div>
</div>
### 示例 2生命值系统
受伤后检查死亡逻辑。`Event OnDamage` 是一个自定义事件节点,可以通过代码 `vm.triggerCustomEvent('OnDamage', { damage: 50 })` 触发:
<div class="bp-graph" data-graph='{
"nodes": [
{
"id": "event", "title": "Event OnDamage", "category": "event",
"outputs": [
{"id": "event-exec", "type": "exec", "inHeader": true},
{"id": "event-self", "type": "entity", "label": "Self"},
{"id": "event-damage", "type": "float", "label": "Damage"}
]
},
{
"id": "getcomp", "title": "Get Component", "category": "function",
"inputs": [
{"id": "getcomp-exec", "type": "exec", "label": "Exec"},
{"id": "getcomp-entity", "type": "entity", "label": "Entity"},
{"id": "getcomp-type", "type": "string", "label": "Type", "value": "Health", "connected": false}
],
"outputs": [
{"id": "getcomp-out", "type": "exec"},
{"id": "getcomp-comp", "type": "component", "label": "Component"}
]
},
{
"id": "getprop", "title": "Get Property", "category": "pure",
"inputs": [
{"id": "getprop-target", "type": "component", "label": "Target"},
{"id": "getprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false}
],
"outputs": [
{"id": "getprop-val", "type": "float", "label": "Value"}
]
},
{
"id": "sub", "title": "Subtract", "category": "math",
"inputs": [
{"id": "sub-exec", "type": "exec", "label": "Exec"},
{"id": "sub-a", "type": "float", "label": "A"},
{"id": "sub-b", "type": "float", "label": "B"}
],
"outputs": [
{"id": "sub-out", "type": "exec"},
{"id": "sub-result", "type": "float", "label": "Result"}
]
},
{
"id": "setprop", "title": "Set Property", "category": "function",
"inputs": [
{"id": "setprop-exec", "type": "exec", "label": "Exec"},
{"id": "setprop-target", "type": "component", "label": "Target"},
{"id": "setprop-prop", "type": "string", "label": "Property", "value": "current", "connected": false},
{"id": "setprop-val", "type": "float", "label": "Value"}
],
"outputs": [
{"id": "setprop-out", "type": "exec"}
]
},
{
"id": "lte", "title": "Less Or Equal", "category": "pure",
"inputs": [
{"id": "lte-a", "type": "float", "label": "A"},
{"id": "lte-b", "type": "float", "label": "B", "value": "0", "connected": false}
],
"outputs": [
{"id": "lte-result", "type": "bool", "label": "Result"}
]
},
{
"id": "branch", "title": "Branch", "category": "flow",
"inputs": [
{"id": "branch-exec", "type": "exec", "label": "Exec"},
{"id": "branch-cond", "type": "bool", "label": "Condition"}
],
"outputs": [
{"id": "branch-true", "type": "exec", "label": "True"},
{"id": "branch-false", "type": "exec", "label": "False"}
]
},
{
"id": "destroy", "title": "Destroy Entity", "category": "function",
"inputs": [
{"id": "destroy-exec", "type": "exec", "label": "Exec"},
{"id": "destroy-entity", "type": "entity", "label": "Entity"}
]
}
],
"connections": [
{"from": "event-exec", "to": "getcomp-exec", "type": "exec"},
{"from": "getcomp-out", "to": "sub-exec", "type": "exec"},
{"from": "sub-out", "to": "setprop-exec", "type": "exec"},
{"from": "setprop-out", "to": "branch-exec", "type": "exec"},
{"from": "branch-true", "to": "destroy-exec", "type": "exec"},
{"from": "event-self", "to": "getcomp-entity", "type": "entity"},
{"from": "event-self", "to": "destroy-entity", "type": "entity"},
{"from": "getcomp-comp", "to": "getprop-target", "type": "component"},
{"from": "getcomp-comp", "to": "setprop-target", "type": "component"},
{"from": "getprop-val", "to": "sub-a", "type": "float"},
{"from": "event-damage", "to": "sub-b", "type": "float"},
{"from": "sub-result", "to": "setprop-val", "type": "float"},
{"from": "sub-result", "to": "lte-a", "type": "float"},
{"from": "lte-result", "to": "branch-cond", "type": "bool"}
]
}'></div>
### 示例 3延迟生成
每 2 秒生成一个敌人:
<div class="bp-graph" style="" data-connections='[{"from":"ex3-begin-exec","to":"ex3-loop","type":"exec"},{"from":"ex3-loop-body","to":"ex3-delay","type":"exec"},{"from":"ex3-delay-done","to":"ex3-create","type":"exec"}]'>
<svg class="bp-connections"></svg>
<div class="bp-node" style="left: 20px; top: 20px; width: 170px;">
<div class="bp-node-header event">
<span class="bp-node-header-icon"></span>
<span class="bp-node-header-title">Event BeginPlay</span>
<span class="bp-header-exec" data-pin="ex3-begin-exec"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
</div>
<div class="bp-node" style="left: 240px; top: 20px; width: 130px;">
<div class="bp-node-header flow">Do N Times</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex3-loop"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#1cc4c4" stroke-width="2"/></svg></span>
<span class="bp-pin-label">N</span>
<span class="bp-pin-value">10</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex3-loop-body"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Loop Body</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#1cc4c4"/></svg></span>
<span class="bp-pin-label">Index</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Completed</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 430px; top: 20px; width: 120px;">
<div class="bp-node-header time">Delay</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex3-delay"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#7ecd32" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Duration</span>
<span class="bp-pin-value">2.0</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin" data-pin="ex3-delay-done"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Done</span>
</div>
</div>
</div>
<div class="bp-node" style="left: 610px; top: 20px; width: 140px;">
<div class="bp-node-header function">Create Entity</div>
<div class="bp-node-body">
<div class="bp-pin-row input">
<span class="bp-pin" data-pin="ex3-create"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
<span class="bp-pin-label">Exec</span>
</div>
<div class="bp-pin-row input">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="none" stroke="#e060e0" stroke-width="2"/></svg></span>
<span class="bp-pin-label">Name</span>
<span class="bp-pin-value">"Enemy"</span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><polygon points="1,1 11,6 1,11" fill="#fff"/></svg></span>
</div>
<div class="bp-pin-row output">
<span class="bp-pin"><svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#00a0e0"/></svg></span>
<span class="bp-pin-label">Entity</span>
</div>
</div>
</div>
</div>
## 常见问题
### Q: 节点无法连接?
检查引脚类型是否匹配。执行引脚(白色)只能连接执行引脚,数据引脚需要类型兼容。
### Q: 蓝图不执行?
1. 确保实体添加了 `BlueprintComponent`
2. 确保场景添加了 `BlueprintSystem`
3. 检查 `autoStart` 是否为 `true`
### Q: 如何调试?
使用 **Print** 节点输出变量值到控制台。
## 下一步
- [ECS 节点参考](/modules/blueprint/nodes) - 完整节点列表
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
- [运行时集成](/modules/blueprint/vm) - 蓝图虚拟机 API
- [实际示例](/modules/blueprint/examples) - 更多游戏逻辑示例

View File

@@ -3,85 +3,127 @@ title: "实际示例"
description: "ECS 集成和最佳实践"
---
## 玩家控制蓝图
## 完整游戏集成示例
```typescript
// 定义输入处理节点
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
],
isPure: true
};
import { Scene, Core, Component, ECSComponent } from '@esengine/ecs-framework';
import {
BlueprintSystem,
BlueprintComponent,
BlueprintExpose,
BlueprintProperty,
BlueprintMethod
} from '@esengine/blueprint';
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const input = context.scene.services.get(InputServiceToken);
const direction = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};
return { outputs: { direction } };
// 1. 定义游戏组件
@ECSComponent('Player')
@BlueprintExpose({ displayName: '玩家', category: 'gameplay' })
export class PlayerComponent extends Component {
@BlueprintProperty({ displayName: '移动速度', type: 'float' })
moveSpeed: number = 5;
@BlueprintProperty({ displayName: '分数', type: 'int' })
score: number = 0;
@BlueprintMethod({ displayName: '增加分数' })
addScore(points: number): void {
this.score += points;
}
}
@ECSComponent('Health')
@BlueprintExpose({ displayName: '生命值', category: 'gameplay' })
export class HealthComponent extends Component {
@BlueprintProperty({ displayName: '当前生命值' })
current: number = 100;
@BlueprintProperty({ displayName: '最大生命值' })
max: number = 100;
@BlueprintMethod({ displayName: '治疗' })
heal(amount: number): void {
this.current = Math.min(this.current + amount, this.max);
}
@BlueprintMethod({ displayName: '受伤' })
takeDamage(amount: number): boolean {
this.current -= amount;
return this.current <= 0;
}
}
// 2. 初始化游戏
async function initGame() {
const scene = new Scene();
// 添加蓝图系统
scene.addSystem(new BlueprintSystem());
Core.setScene(scene);
// 3. 创建玩家
const player = scene.createEntity('Player');
player.addComponent(new PlayerComponent());
player.addComponent(new HealthComponent());
// 添加蓝图控制
const blueprint = new BlueprintComponent();
blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
player.addComponent(blueprint);
}
```
## 状态切换逻辑
## 自定义节点示例
```typescript
// 在蓝图中实现状态机逻辑
const stateBlueprint = createEmptyBlueprint('PlayerState');
import type { Entity } from '@esengine/ecs-framework';
import {
BlueprintNodeTemplate,
BlueprintNode,
ExecutionContext,
ExecutionResult,
INodeExecutor,
RegisterNode
} from '@esengine/blueprint';
// 添加状态变量
stateBlueprint.variables.push({
name: 'currentState',
type: 'string',
defaultValue: 'idle',
scope: 'instance'
});
// 在 Tick 事件中检查状态转换
// ... 通过节点连接实现
```
## 伤害处理系统
```typescript
// 自定义伤害节点
const ApplyDamageTemplate: BlueprintNodeTemplate = {
type: 'ApplyDamage',
title: 'Apply Damage',
category: 'combat',
color: '#aa2222',
description: '对带有 Health 组件的实体造成伤害',
keywords: ['damage', 'hurt', 'attack'],
menuPath: ['Combat', 'Apply Damage'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'target', type: 'entity', direction: 'input' },
{ name: 'amount', type: 'number', direction: 'input', defaultValue: 10 }
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'target', type: 'entity', displayName: '目标' },
{ name: 'amount', type: 'float', displayName: '伤害量', defaultValue: 10 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'killed', type: 'boolean', direction: 'output' }
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'killed', type: 'bool', displayName: '已击杀' }
]
};
@RegisterNode(ApplyDamageTemplate)
class ApplyDamageExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const target = context.getInput<Entity>(node.id, 'target');
const amount = context.getInput<number>(node.id, 'amount');
const target = context.evaluateInput(node.id, 'target', context.entity) as Entity;
const amount = context.evaluateInput(node.id, 'amount', 10) as number;
if (!target || target.isDestroyed) {
return { outputs: { killed: false }, nextExec: 'exec' };
}
const health = target.components.find(c =>
(c.constructor as any).__componentName__ === 'Health'
) as any;
const health = target.getComponent(HealthComponent);
if (health) {
health.current -= amount;
const killed = health.current <= 0;
return {
outputs: { killed },
nextExec: 'exec'
};
return { outputs: { killed }, nextExec: 'exec' };
}
return { outputs: { killed: false }, nextExec: 'exec' };
@@ -89,25 +131,6 @@ class ApplyDamageExecutor implements INodeExecutor {
}
```
## 技能冷却系统
```typescript
// 冷却检查节点
const CheckCooldownTemplate: BlueprintNodeTemplate = {
type: 'CheckCooldown',
title: 'Check Cooldown',
category: 'ability',
inputs: [
{ name: 'skillId', type: 'string', direction: 'input' }
],
outputs: [
{ name: 'ready', type: 'boolean', direction: 'output' },
{ name: 'remaining', type: 'number', direction: 'output' }
],
isPure: true
};
```
## 最佳实践
### 1. 使用片段复用逻辑
@@ -151,7 +174,8 @@ vm.maxStepsPerFrame = 1000;
```typescript
// 启用调试模式查看执行日志
vm.debug = true;
const blueprint = entity.getComponent(BlueprintComponent);
blueprint.debug = true;
// 使用 Print 节点输出中间值
// 在编辑器中设置断点

View File

@@ -1,114 +1,162 @@
---
title: "蓝图可视化脚本 (Blueprint)"
description: "完整的可视化脚本系统"
description: "与 ECS 框架深度集成的可视化脚本系统"
---
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合
`@esengine/blueprint` 提供与 ECS 框架深度集成的可视化脚本系统,支持通过节点式编程控制实体行为
## 安装
## 编辑器下载
Cocos Creator 蓝图编辑器插件(免费):
**[下载 Cocos Node Editor v1.1.0](https://github.com/esengine/esengine/releases/tag/cocos-node-editor-v1.1.0)**
> 技术交流 QQ 群:**481923584** | 官网:[esengine.cn](https://esengine.cn/)
详细使用教程请参考 [编辑器使用指南](./editor-guide)。
## 安装运行时
```bash
npm install @esengine/blueprint
```
## 核心特性
- **ECS 深度集成** - 内置 Entity、Component 操作节点
- **组件自动节点生成** - 使用装饰器标记组件,自动生成 Get/Set/Call 节点
- **运行时蓝图执行** - 高效的虚拟机执行蓝图逻辑
## 快速开始
### 1. 添加蓝图系统
```typescript
import { Scene, Core } from '@esengine/ecs-framework';
import { BlueprintSystem } from '@esengine/blueprint';
// 创建场景并添加蓝图系统
const scene = new Scene();
scene.addSystem(new BlueprintSystem());
// 设置场景
Core.setScene(scene);
```
### 2. 为实体添加蓝图
```typescript
import { BlueprintComponent } from '@esengine/blueprint';
// 创建实体
const player = scene.createEntity('Player');
// 添加蓝图组件
const blueprint = new BlueprintComponent();
blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
blueprint.autoStart = true;
player.addComponent(blueprint);
```
### 3. 标记组件(自动生成蓝图节点)
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
BlueprintExpose,
BlueprintProperty,
BlueprintMethod
} from '@esengine/blueprint';
import { Component, ECSComponent } from '@esengine/ecs-framework';
// 创建蓝图系统
const blueprintSystem = createBlueprintSystem(scene);
@ECSComponent('Health')
@BlueprintExpose({ displayName: '生命值', category: 'gameplay' })
export class HealthComponent extends Component {
@BlueprintProperty({ displayName: '当前生命值', type: 'float' })
current: number = 100;
// 加载蓝图资产
const blueprint = await loadBlueprintAsset('player.bp');
@BlueprintProperty({ displayName: '最大生命值', type: 'float' })
max: number = 100;
// 创建蓝图组件数据
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
@BlueprintMethod({
displayName: '治疗',
params: [{ name: 'amount', type: 'float' }]
})
heal(amount: number): void {
this.current = Math.min(this.current + amount, this.max);
}
// 在游戏循环中更新
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
@BlueprintMethod({ displayName: '受伤' })
takeDamage(amount: number): boolean {
this.current -= amount;
return this.current <= 0;
}
}
```
## 核心概念
标记后,蓝图编辑器中会自动出现以下节点:
- **Get Health** - 获取 Health 组件
- **Get 当前生命值** - 获取 current 属性
- **Set 当前生命值** - 设置 current 属性
- **治疗** - 调用 heal 方法
- **受伤** - 调用 takeDamage 方法
### 蓝图资产结
## ECS 集成架
蓝图保存为 `.bp` 文件,包含以下结构:
```typescript
interface BlueprintAsset {
version: number; // 格式版本
type: 'blueprint'; // 资产类型
metadata: BlueprintMetadata; // 元数据
variables: BlueprintVariable[]; // 变量定义
nodes: BlueprintNode[]; // 节点实例
connections: BlueprintConnection[]; // 连接
}
```
┌─────────────────────────────────────────────────────────────┐
│ Core.update() │
│ ↓ │
Scene.updateSystems() │
↓ │
│ ┌───────────────────────────────────────────────────────┐ │
BlueprintSystem │ │
│ │
Matcher.all(BlueprintComponent) │ │
│ │ ↓ │ │
│ │ process(entities) → blueprint.tick() for each entity │ │
│ │ ↓ │ │
│ │ BlueprintVM.tick(dt) │ │
│ │ ↓ │ │
│ │ Execute Event/ECS/Flow Nodes │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 节点类型
节点按功能分为以下类别:
## 节点类型
| 类别 | 说明 | 颜色 |
|------|------|------|
| `event` | 事件节点(入口点 | 红色 |
| `flow` | 流程控制 | 色 |
| `entity` | 实体操作 | 色 |
| `component` | 组件访问 | 色 |
| `event` | 事件节点(BeginPlay, Tick, EndPlay | 红色 |
| `entity` | ECS 实体操作 | 色 |
| `component` | ECS 组件访问 | 色 |
| `flow` | 流程控制Branch, Sequence, Loop | 色 |
| `math` | 数学运算 | 绿色 |
| `logic` | 逻辑运算 | 色 |
| `variable` | 变量访问 | 色 |
| `time` | 时间工具 | 青色 |
| `debug` | 调试工具 | 灰色 |
| `time` | 时间工具Delay, GetDeltaTime | 色 |
| `debug` | 调试工具Print | 色 |
### 引脚类型
## 蓝图资产结构
节点通过引脚连接
蓝图保存为 `.bp` 文件
```typescript
interface BlueprintPinDefinition {
name: string; // 引脚名称
type: PinDataType; // 数据类型
direction: 'input' | 'output';
isExec?: boolean; // 是否是执行引脚
defaultValue?: unknown;
interface BlueprintAsset {
version: number;
type: 'blueprint';
metadata: {
name: string;
description?: string;
};
variables: BlueprintVariable[];
nodes: BlueprintNode[];
connections: BlueprintConnection[];
}
// 支持的数据类型
type PinDataType =
| 'exec' // 执行流
| 'boolean' // 布尔值
| 'number' // 数字
| 'string' // 字符串
| 'vector2' // 2D 向量
| 'vector3' // 3D 向量
| 'entity' // 实体引用
| 'component' // 组件引用
| 'any'; // 任意类型
```
### 变量作用域
```typescript
type VariableScope =
| 'local' // 每次执行独立
| 'instance' // 每个实体独立
| 'global'; // 全局共享
```
## 文档导航
- [虚拟机 API](./vm) - BlueprintVM 执行和上下文
- [自定义节点](./custom-nodes) - 创建自定义节点
- [内置节点](./nodes) - 内置节点参考
- [蓝图组合](./composition) - 片段和组合器
- [实际示例](./examples) - ECS 集成和最佳实践
- [编辑器使用指南](./editor-guide) - Cocos Creator 蓝图编辑器教程
- [虚拟机 API](./vm) - BlueprintVM 与 ECS 集成
- [ECS 节点参考](./nodes) - 内置 ECS 操作节点
- [自定义节点](./custom-nodes) - 创建自定义 ECS 节点
- [蓝图组合](./composition) - 片段复用
- [实际示例](./examples) - ECS 游戏逻辑示例

View File

@@ -1,107 +1,547 @@
---
title: "内置节点"
description: "蓝图内置节点参考"
title: "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` | 每帧触发 |
| `EventEndPlay` | 蓝图停止时触发 |
| `EventCollision` | 碰撞时触发 |
| `EventInput` | 输入事件触发 |
| `EventTimer` | 定时器触发 |
| `EventMessage` | 自定义消息触发 |
生命周期事件,作为蓝图执行的入口点:
| 节点 | 说明 | 输出 |
|------|------|------|
| `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)
操作 ECS 实体:
| 节点 | 说明 | 类型 |
|------|------|------|
| `Get Self` | 获取拥有此蓝图的实体 | 纯节点 |
| `Create Entity` | 在场景中创建新实体 | 执行节点 |
| `Destroy Entity` | 销毁指定实体 | 执行节点 |
| `Destroy Self` | 销毁自身实体 | 执行节点 |
| `Is Valid` | 检查实体是否有效 | 纯节点 |
| `Get Entity Name` | 获取实体名称 | 纯节点 |
| `Set Entity Name` | 设置实体名称 | 执行节点 |
| `Find Entity By Name` | 按名称查找实体 | 纯节点 |
| `Find Entities By Tag` | 按标签查找所有实体 | 纯节点 |
### 示例:创建子弹
<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)
读写组件属性:
| 节点 | 说明 | 类型 |
|------|------|------|
| `Get Component` | 从实体获取指定类型组件 | 纯节点 |
| `Has Component` | 检查实体是否拥有指定组件 | 纯节点 |
| `Add Component` | 为实体添加组件 | 执行节点 |
| `Remove Component` | 从实体移除组件 | 执行节点 |
| `Get Property` | 获取组件属性值 | 纯节点 |
| `Set Property` | 设置组件属性值 | 执行节点 |
### 示例:修改位置
<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` | 顺序执行多个输出 |
| `ForLoop` | 循环执行 |
| `WhileLoop` | 条件循环 |
| `DoOnce` | 只执行一次 |
| `FlipFlop` | 交替执行两个分支 |
| `Gate` | 可开关的执行门 |
| `Branch` | 条件分支if/else |
| `Sequence` | 顺序执行多个分支 |
| `For Loop` | 指定次数循环 |
| `For Each` | 遍历数组元素 |
| `While Loop` | 条件循环 |
| `Do Once` | 仅执行一次 |
| `Flip Flop` | 交替执行 A/B |
| `Gate` | 门控开关 |
### 示例:条件分支
<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` | 延迟执行 |
| `GetDeltaTime` | 获取帧间隔 |
| `GetTime` | 获取运行时间 |
| `SetTimer` | 设置定时器 |
| `ClearTimer` | 清除定时器 |
| 节点 | 说明 | 输出 |
|------|------|------|
| `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` | 加法 |
| `Subtract` | 减法 |
| `Multiply` | 乘法 |
| `Divide` | 除法 |
| `Abs` | 绝对值 |
| `Clamp` | 限制范围 |
| `Lerp` | 线性插值 |
| `Min` / `Max` | 最小/最大值 |
| `Sin` / `Cos` | 三角函数 |
| `Sqrt` | 平方根 |
| `Power` | 幂运算 |
### 基础运算
## 逻辑节点
| 节点 | 说明 | 输入 | 输出 |
|------|------|------|------|
| `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) |
### 三角函数
| 节点 | 说明 |
|------|------|
| `And` | 逻辑与 |
| `Or` | 逻辑或 |
| `Not` | 逻辑非 |
| `Equal` | 相等比较 |
| `NotEqual` | 不等比较 |
| `Greater` | 大于比较 |
| `Less` | 小于比较 |
| `Sin` | 正弦 |
| `Cos` | 余弦 |
| `Tan` | 正切 |
| `Asin` | 反正弦 |
| `Acos` | 反余弦 |
| `Atan` | 反正切 |
| `Atan2` | 二参数反正切 |
## 向量节点
### 随机数
| 节点 | 说明 |
|------|------|
| `MakeVector2` | 创建 2D 向量 |
| `BreakVector2` | 分解 2D 向量 |
| `VectorAdd` | 向量加法 |
| `VectorSubtract` | 向量减法 |
| `VectorMultiply` | 向量乘法 |
| `VectorLength` | 向量长度 |
| `VectorNormalize` | 向量归一化 |
| `VectorDistance` | 向量距离 |
| 节点 | 说明 | 输入 | 输出 |
|------|------|------|------|
| `Random` | 随机浮点数 [0, 1) | - | Float |
| `Random Range` | 范围内随机数 | Min, Max | Float |
| `Random Int` | 随机整数 | Min, Max | Int |
## 实体节点
### 比较节点
| 节点 | 说明 |
|------|------|
| `GetSelf` | 获取当前实体 |
| `GetComponent` | 获取组件 |
| `HasComponent` | 检查组件 |
| `AddComponent` | 添加组件 |
| `RemoveComponent` | 移除组件 |
| `SpawnEntity` | 创建实体 |
| `DestroyEntity` | 销毁实体 |
| 节点 | 说明 | 输出 |
|------|------|------|
| `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>
## 变量节点
| 节点 | 说明 |
|------|------|
| `GetVariable` | 获取变量值 |
| `SetVariable` | 设置变量值 |
蓝图定义的变量会自动生成 Get 和 Set 节点:
| 节点 | 说明 | 类型 |
|------|------|------|
| `Get <变量名>` | 读取变量值 | 纯节点 |
| `Set <变量名>` | 设置变量值 | 执行节点 |
## 调试节点
| 节点 | 说明 |
|------|------|
| `Print` | 打印到控制台 |
| `DrawDebugLine` | 绘制调试线 |
| `DrawDebugPoint` | 绘制调试点 |
| `Breakpoint` | 调试断点 |
| `Print` | 输出消息到控制台 |
## 相关文档
- [数学库蓝图节点](/modules/math/blueprint-nodes) - Vector2、Fixed32、Color 等数学节点
- [蓝图编辑器指南](/modules/blueprint/editor-guide) - 学习如何使用编辑器
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
- [蓝图虚拟机](/modules/blueprint/vm) - 运行时 API

View File

@@ -45,7 +45,7 @@ interface ExecutionContext {
time: number; // 总运行时间
// 获取输入值
getInput<T>(nodeId: string, pinName: string): T;
evaluateInput(nodeId: string, pinName: string, defaultValue: unknown): unknown;
// 设置输出值
setOutput(nodeId: string, pinName: string, value: unknown): void;
@@ -70,35 +70,33 @@ interface ExecutionResult {
## 与 ECS 集成
### 使用蓝图系统
### 使用内置蓝图系统
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
import { Scene, Core } from '@esengine/ecs-framework';
import { BlueprintSystem, BlueprintComponent } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
// 添加蓝图系统到场景
const scene = new Scene();
scene.addSystem(new BlueprintSystem());
Core.setScene(scene);
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
// 处理所有带蓝图组件的实体
this.blueprintSystem.process(this.entities, dt);
}
}
// 为实体添加蓝图
const entity = scene.createEntity('Player');
const blueprint = new BlueprintComponent();
blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
entity.addComponent(blueprint);
```
### 触发蓝图事件
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
// 触发内置事件
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
// 触发自定义事件
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
// 从实体获取蓝图组件并触发事件
const blueprint = entity.getComponent(BlueprintComponent);
if (blueprint?.vm) {
blueprint.vm.triggerEvent('EventCollision', { other: otherEntity });
blueprint.vm.triggerCustomEvent('OnPickup', { item: itemEntity });
}
```
## 序列化

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,441 @@
---
title: "分布式房间"
description: "使用 DistributedRoomManager 实现多服务器房间管理"
---
## 概述
分布式房间支持允许多个服务器实例共享房间注册表,实现跨服务器玩家路由和故障转移。
```
┌─────────────────────────────────────────────────────────┐
│ Server A Server B Server C │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Room 1 │ │ Room 3 │ │ Room 5 │ │
│ │ Room 2 │ │ Room 4 │ │ Room 6 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IDistributedAdapter │ │
│ │ (Redis / Memory) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## 快速开始
### 单机模式(测试用)
```typescript
import {
DistributedRoomManager,
MemoryAdapter,
Room
} from '@esengine/server';
// 定义房间类型
class GameRoom extends Room {
maxPlayers = 4;
}
// 创建适配器和管理器
const adapter = new MemoryAdapter();
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000
}, (conn, type, data) => conn.send(JSON.stringify({ type, data })));
// 注册房间类型
manager.define('game', GameRoom);
// 启动管理器
await manager.start();
// 分布式加入/创建房间
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// 玩家应连接到其他服务器
console.log(`重定向到: ${result.redirect}`);
} else {
// 玩家加入本地房间
const { room, player } = result;
}
// 优雅关闭
await manager.stop(true);
```
### 多服务器模式(生产用)
```typescript
import Redis from 'ioredis';
import { DistributedRoomManager, RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis({
host: 'redis.example.com',
port: 6379
}),
prefix: 'game:',
serverTtl: 30,
snapshotTtl: 86400
});
const manager = new DistributedRoomManager(adapter, {
serverId: process.env.SERVER_ID,
serverAddress: process.env.PUBLIC_IP,
serverPort: 3000,
heartbeatInterval: 5000,
snapshotInterval: 30000,
enableFailover: true,
capacity: 100
}, sendFn);
```
## DistributedRoomManager
### 配置选项
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `serverId` | `string` | 必填 | 服务器唯一标识 |
| `serverAddress` | `string` | 必填 | 客户端连接的公开地址 |
| `serverPort` | `number` | 必填 | 服务器端口 |
| `heartbeatInterval` | `number` | `5000` | 心跳间隔(毫秒) |
| `snapshotInterval` | `number` | `30000` | 状态快照间隔0 禁用 |
| `migrationTimeout` | `number` | `10000` | 房间迁移超时 |
| `enableFailover` | `boolean` | `true` | 启用自动故障转移 |
| `capacity` | `number` | `100` | 本服务器最大房间数 |
### 生命周期方法
#### start()
启动分布式房间管理器。连接适配器、注册服务器、启动心跳。
```typescript
await manager.start();
```
#### stop(graceful?)
停止管理器。如果 `graceful=true`,将服务器标记为 draining 并保存所有房间快照。
```typescript
await manager.stop(true);
```
### 路由方法
#### joinOrCreateDistributed()
分布式感知的加入或创建房间。返回本地房间的 `{ room, player }` 或远程房间的 `{ redirect: string }`
```typescript
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// 客户端应重定向到其他服务器
res.json({ redirect: result.redirect });
} else {
// 玩家加入了本地房间
const { room, player } = result;
}
```
#### route()
将玩家路由到合适的房间/服务器。
```typescript
const result = await manager.route({
roomType: 'game',
playerId: 'p1'
});
switch (result.type) {
case 'local': // 房间在本服务器
break;
case 'redirect': // 房间在其他服务器
// result.serverAddress 包含目标服务器地址
break;
case 'create': // 没有可用房间,需要创建
break;
case 'unavailable': // 无法找到或创建房间
// result.reason 包含错误信息
break;
}
```
### 状态管理
#### saveSnapshot()
手动保存房间状态快照。
```typescript
await manager.saveSnapshot(roomId);
```
#### restoreFromSnapshot()
从保存的快照恢复房间。
```typescript
const success = await manager.restoreFromSnapshot(roomId);
```
### 查询方法
#### getServers()
获取所有在线服务器。
```typescript
const servers = await manager.getServers();
```
#### queryDistributedRooms()
查询所有服务器上的房间。
```typescript
const rooms = await manager.queryDistributedRooms({
roomType: 'game',
hasSpace: true,
notLocked: true
});
```
## IDistributedAdapter
分布式后端的接口。实现此接口以支持 Redis、消息队列等。
### 内置适配器
#### MemoryAdapter
用于测试和单机模式的内存实现。
```typescript
const adapter = new MemoryAdapter({
serverTtl: 15000, // 无心跳后服务器离线时间(毫秒)
enableTtlCheck: true, // 启用自动 TTL 检查
ttlCheckInterval: 5000 // TTL 检查间隔(毫秒)
});
```
#### RedisAdapter
用于生产环境多服务器部署的 Redis 实现。
```typescript
import Redis from 'ioredis';
import { RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis('redis://localhost:6379'),
prefix: 'game:', // 键前缀(默认: 'dist:'
serverTtl: 30, // 服务器 TTL默认: 30
roomTtl: 0, // 房间 TTL0 = 永不过期(默认: 0
snapshotTtl: 86400, // 快照 TTL默认: 24 小时)
channel: 'game:events' // Pub/Sub 频道(默认: 'distributed:events'
});
```
**RedisAdapter 配置:**
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `factory` | `() => RedisClient` | 必填 | Redis 客户端工厂(惰性连接) |
| `prefix` | `string` | `'dist:'` | 所有 Redis 键的前缀 |
| `serverTtl` | `number` | `30` | 服务器 TTL |
| `roomTtl` | `number` | `0` | 房间 TTL0 = 不过期 |
| `snapshotTtl` | `number` | `86400` | 快照 TTL |
| `channel` | `string` | `'distributed:events'` | Pub/Sub 频道名 |
**功能特性:**
- 带自动心跳 TTL 的服务器注册
- 跨服务器查找的房间注册
- 可配置 TTL 的状态快照
- 跨服务器事件的 Pub/Sub
- 使用 Redis SET NX 的分布式锁
### 自定义适配器
```typescript
import type { IDistributedAdapter } from '@esengine/server';
class MyAdapter implements IDistributedAdapter {
// 生命周期
async connect(): Promise<void> { }
async disconnect(): Promise<void> { }
isConnected(): boolean { return true; }
// 服务器注册
async registerServer(server: ServerRegistration): Promise<void> { }
async unregisterServer(serverId: string): Promise<void> { }
async heartbeat(serverId: string): Promise<void> { }
async getServers(): Promise<ServerRegistration[]> { return []; }
// 房间注册
async registerRoom(room: RoomRegistration): Promise<void> { }
async unregisterRoom(roomId: string): Promise<void> { }
async queryRooms(query: RoomQuery): Promise<RoomRegistration[]> { return []; }
async findAvailableRoom(roomType: string): Promise<RoomRegistration | null> { return null; }
// 状态快照
async saveSnapshot(snapshot: RoomSnapshot): Promise<void> { }
async loadSnapshot(roomId: string): Promise<RoomSnapshot | null> { return null; }
// 发布/订阅
async publish(event: DistributedEvent): Promise<void> { }
async subscribe(pattern: string, handler: Function): Promise<() => void> { return () => {}; }
// 分布式锁
async acquireLock(key: string, ttlMs: number): Promise<boolean> { return true; }
async releaseLock(key: string): Promise<void> { }
}
```
## 玩家路由流程
```
客户端 服务器 A 服务器 B
│ │ │
│─── joinOrCreate ────────►│ │
│ │ │
│ │── findAvailableRoom() ───►│
│ │◄──── 服务器 B 上有房间 ────│
│ │ │
│◄─── redirect: B:3001 ────│ │
│ │ │
│───────────────── 连接到服务器 B ────────────────────►│
│ │ │
│◄─────────────────────────────── 已加入 ─────────────│
```
## 事件类型
分布式系统发布以下事件:
| 事件 | 描述 |
|------|------|
| `server:online` | 服务器上线 |
| `server:offline` | 服务器离线 |
| `server:draining` | 服务器正在排空 |
| `room:created` | 房间已创建 |
| `room:disposed` | 房间已销毁 |
| `room:updated` | 房间信息已更新 |
| `room:message` | 跨服务器房间消息 |
| `room:migrated` | 房间已迁移到其他服务器 |
| `player:joined` | 玩家加入房间 |
| `player:left` | 玩家离开房间 |
## 最佳实践
1. **使用唯一服务器 ID** - 使用主机名、容器 ID 或 UUID
2. **配置合适的心跳** - 在新鲜度和网络开销之间平衡
3. **为有状态房间启用快照** - 确保房间状态在服务器重启后存活
4. **优雅处理重定向** - 客户端应重新连接到目标服务器
```typescript
// 客户端处理重定向
if (response.redirect) {
await client.disconnect();
await client.connect(response.redirect);
await client.joinRoom(roomId);
}
```
5. **使用分布式锁** - 防止 joinOrCreate 中的竞态条件
## 使用 createServer 集成
最简单的使用方式是通过 `createServer` 的 `distributed` 配置:
```typescript
import { createServer } from '@esengine/server';
import { RedisAdapter, Room } from '@esengine/server';
import Redis from 'ioredis';
class GameRoom extends Room {
maxPlayers = 4;
}
const server = await createServer({
port: 3000,
distributed: {
enabled: true,
adapter: new RedisAdapter({ factory: () => new Redis() }),
serverId: 'server-1',
serverAddress: 'ws://192.168.1.100',
serverPort: 3000,
enableFailover: true,
capacity: 100
}
});
server.define('game', GameRoom);
await server.start();
```
当客户端调用 `JoinRoom` API 时,服务器会自动:
1. 查找可用房间(本地或远程)
2. 如果房间在其他服务器,发送 `$redirect` 消息给客户端
3. 客户端收到重定向消息后连接到目标服务器
## 负载均衡
使用 `LoadBalancedRouter` 进行服务器选择:
```typescript
import { LoadBalancedRouter, createLoadBalancedRouter } from '@esengine/server';
// 使用工厂函数
const router = createLoadBalancedRouter('least-players');
// 或直接创建
const router = new LoadBalancedRouter({
strategy: 'least-rooms', // 选择房间数最少的服务器
preferLocal: true // 优先选择本地服务器
});
// 可用策略
// - 'round-robin': 轮询
// - 'least-rooms': 最少房间数
// - 'least-players': 最少玩家数
// - 'random': 随机选择
// - 'weighted': 权重(基于容量使用率)
```
## 故障转移
当服务器离线时,启用 `enableFailover` 后系统会自动:
1. 检测到服务器离线(通过心跳超时)
2. 查询该服务器上的所有房间
3. 使用分布式锁防止多服务器同时恢复
4. 从快照恢复房间状态
5. 发布 `room:migrated` 事件通知其他服务器
```typescript
// 确保定期保存快照
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000,
snapshotInterval: 30000, // 每 30 秒保存快照
enableFailover: true // 启用故障转移
}, sendFn);
```
## 后续版本
- Redis Cluster 支持
- 更多负载均衡策略(地理位置、延迟感知)

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

@@ -147,6 +147,7 @@ service.on('chat', (data) => {
- [客户端使用](/modules/network/client/) - NetworkPlugin、组件和系统
- [服务器端](/modules/network/server/) - GameServer 和 Room 管理
- [分布式房间](/modules/network/distributed/) - 多服务器房间管理和玩家路由
- [状态同步](/modules/network/sync/) - 插值和快照缓冲
- [客户端预测](/modules/network/prediction/) - 输入预测和服务器校正
- [兴趣区域 (AOI)](/modules/network/aoi/) - 视野过滤和带宽优化

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

@@ -280,6 +280,122 @@ class GameRoom extends Room {
}
```
## Schema 验证
使用内置的 Schema 验证系统进行运行时类型验证:
### 基础用法
```typescript
import { s, defineApiWithSchema } from '@esengine/server'
// 定义 Schema
const MoveSchema = s.object({
x: s.number(),
y: s.number(),
speed: s.number().optional()
})
// 类型自动推断
type Move = s.infer<typeof MoveSchema> // { x: number; y: number; speed?: number }
// 使用 Schema 定义 API自动验证
export default defineApiWithSchema(MoveSchema, {
handler(req, ctx) {
// req 已验证,类型安全
console.log(req.x, req.y)
}
})
```
### 验证器类型
| 类型 | 示例 | 描述 |
|------|------|------|
| `s.string()` | `s.string().min(1).max(50)` | 字符串,支持长度限制 |
| `s.number()` | `s.number().min(0).int()` | 数字,支持范围和整数限制 |
| `s.boolean()` | `s.boolean()` | 布尔值 |
| `s.literal()` | `s.literal('admin')` | 字面量类型 |
| `s.object()` | `s.object({ name: s.string() })` | 对象 |
| `s.array()` | `s.array(s.number())` | 数组 |
| `s.enum()` | `s.enum(['a', 'b'] as const)` | 枚举 |
| `s.union()` | `s.union([s.string(), s.number()])` | 联合类型 |
| `s.record()` | `s.record(s.any())` | 记录类型 |
### 修饰符
```typescript
// 可选字段
s.string().optional()
// 默认值
s.number().default(0)
// 可为 null
s.string().nullable()
// 字符串验证
s.string().min(1).max(100).email().url().regex(/^[a-z]+$/)
// 数字验证
s.number().min(0).max(100).int().positive()
// 数组验证
s.array(s.string()).min(1).max(10).nonempty()
// 对象验证
s.object({ ... }).strict() // 不允许额外字段
s.object({ ... }).partial() // 所有字段可选
s.object({ ... }).pick('name', 'age') // 选择字段
s.object({ ... }).omit('password') // 排除字段
```
### 消息验证
```typescript
import { s, defineMsgWithSchema } from '@esengine/server'
const InputSchema = s.object({
keys: s.array(s.string()),
timestamp: s.number()
})
export default defineMsgWithSchema(InputSchema, {
handler(msg, ctx) {
// msg 已验证
console.log(msg.keys, msg.timestamp)
}
})
```
### 手动验证
```typescript
import { s, parse, safeParse, createGuard } from '@esengine/server'
const UserSchema = s.object({
name: s.string(),
age: s.number().int().min(0)
})
// 抛出错误
const user = parse(UserSchema, data)
// 返回结果对象
const result = safeParse(UserSchema, data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error)
}
// 类型守卫
const isUser = createGuard(UserSchema)
if (isUser(data)) {
// data 是 User 类型
}
```
## 协议定义
`src/shared/protocol.ts` 中定义客户端和服务端共享的类型:

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

@@ -13,6 +13,7 @@
"packages/network-ext/*",
"packages/editor/*",
"packages/editor/plugins/*",
"packages/devtools/*",
"packages/rust/*",
"packages/tools/*"
],

View File

@@ -0,0 +1,75 @@
# @esengine/node-editor
## 1.4.0
### Minor Changes
- [#438](https://github.com/esengine/esengine/pull/438) [`0d33cf0`](https://github.com/esengine/esengine/commit/0d33cf00977d16e6282931aba2cf771ec2c84c6b) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): add visual group box for organizing nodes
- Add NodeGroup model with dynamic bounds calculation based on node pin counts
- Add GroupNodeComponent for rendering group boxes behind nodes
- Groups automatically resize to wrap contained nodes
- Dragging group header moves all nodes inside together
- Support group serialization/deserialization
- Export `estimateNodeHeight` and `NodeBounds` for accurate size calculation
feat(blueprint): add comprehensive math and logic nodes
Math nodes:
- Modulo, Abs, Min, Max, Power, Sqrt
- Floor, Ceil, Round, Sign, Negate
- Sin, Cos, Tan, Asin, Acos, Atan, Atan2
- DegToRad, RadToDeg, Lerp, InverseLerp
- Clamp, Wrap, RandomRange, RandomInt
Logic nodes:
- Equal, NotEqual, GreaterThan, GreaterThanOrEqual
- LessThan, LessThanOrEqual, InRange
- AND, OR, NOT, XOR, NAND
- IsNull, Select (ternary)
## 1.3.0
### Minor Changes
- [`3bfb8a1`](https://github.com/esengine/esengine/commit/3bfb8a1c9baba18373717910d29f266a71c1f63e) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): add box selection and variable node error states
- Add box selection: drag on empty canvas to select multiple nodes
- Support Ctrl+drag for additive selection (add to existing selection)
- Add error state styling for invalid variable references (red border, warning icon)
- Support dynamic node title via `data.displayTitle`
- Support hiding inputs via `data.hiddenInputs` array
## 1.2.2
### Patch Changes
- [#435](https://github.com/esengine/esengine/pull/435) [`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e) Thanks [@esengine](https://github.com/esengine)! - fix(node-editor): 修复节点收缩后连线不显示的问题
- 节点收缩时,连线会连接到节点头部(输入引脚在左侧,输出引脚在右侧)
- 展开后连线会自动恢复到正确位置
## 1.2.1
### Patch Changes
- [#433](https://github.com/esengine/esengine/pull/433) [`2e84942`](https://github.com/esengine/esengine/commit/2e84942ea14c5326620398add05840fa8bea16f8) Thanks [@esengine](https://github.com/esengine)! - fix(node-editor): 修复节点收缩后连线不显示的问题
- 节点收缩时,连线会连接到节点头部(输入引脚在左侧,输出引脚在右侧)
- 展开后连线会自动恢复到正确位置
## 1.2.0
### Minor Changes
- [#430](https://github.com/esengine/esengine/pull/430) [`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): 添加 Shadow DOM 样式注入支持 | Add Shadow DOM style injection support
**@esengine/node-editor**
- 新增 `nodeEditorCssText` 导出,包含所有编辑器样式的 CSS 文本 | Added `nodeEditorCssText` export containing all editor styles as CSS text
- 新增 `injectNodeEditorStyles(root)` 函数,支持将样式注入到 Shadow DOM | Added `injectNodeEditorStyles(root)` function for injecting styles into Shadow DOM
- 支持在 Cocos Creator 等使用 Shadow DOM 的环境中使用 | Support usage in Shadow DOM environments like Cocos Creator
## 1.1.0
### Minor Changes
- [#426](https://github.com/esengine/esengine/pull/426) [`6970394`](https://github.com/esengine/esengine/commit/6970394717ab8f743b0a41e248e3404a3b6fc7dc) Thanks [@esengine](https://github.com/esengine)! - feat: 独立发布节点编辑器 | Standalone node editor release
- 移动到 packages/devtools 目录 | Move to packages/devtools directory
- 清理依赖,使包可独立使用 | Clean dependencies for standalone use
- 可用于 Cocos Creator / LayaAir 插件开发 | Available for Cocos/Laya plugin development

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/node-editor",
"version": "1.0.0",
"version": "1.4.0",
"description": "Universal node-based visual editor for blueprint, shader graph, and state machine",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -9,7 +9,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./styles": {
"import": "./dist/styles/index.css"
@@ -30,17 +31,18 @@
"blueprint",
"shader-graph",
"state-machine",
"ecs",
"game-engine"
"react"
],
"author": "yhh",
"author": "ESEngine Team",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"react": "^18.3.1",
"zustand": "^5.0.8",
"@types/node": "^20.19.17",
"@types/react": "^18.3.12",
"@vitejs/plugin-react": "^4.7.0",
"react": "^18.3.1",
"rimraf": "^5.0.0",
"typescript": "^5.8.3",
"vite": "^6.0.7",
@@ -56,7 +58,6 @@
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"directory": "packages/node-editor"
},
"private": true
"directory": "packages/devtools/node-editor"
}
}

View File

@@ -50,6 +50,15 @@ export interface GraphCanvasProps {
/** Canvas context menu callback (画布右键菜单回调) */
onContextMenu?: (position: Position, e: React.MouseEvent) => void;
/** Canvas mouse down callback for box selection (画布鼠标按下回调,用于框选) */
onMouseDown?: (position: Position, e: React.MouseEvent) => void;
/** Canvas mouse move callback (画布鼠标移动回调) */
onCanvasMouseMove?: (position: Position, e: React.MouseEvent) => void;
/** Canvas mouse up callback (画布鼠标释放回调) */
onCanvasMouseUp?: (position: Position, e: React.MouseEvent) => void;
/** Children to render (要渲染的子元素) */
children?: React.ReactNode;
}
@@ -75,6 +84,9 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
onZoomChange,
onClick,
onContextMenu,
onMouseDown: onMouseDownProp,
onCanvasMouseMove,
onCanvasMouseUp,
children
}) => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -132,22 +144,30 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
}, [zoom, pan, minZoom, maxZoom, updateZoom, updatePan]);
/**
* Handles mouse down for panning
*
* Handles mouse down for panning or box selection
*
*/
const handleMouseDown = useCallback((e: React.MouseEvent) => {
// Middle mouse button or space + left click for panning
// 中键或空格+左键平移
// Middle mouse button or Alt + left click for panning
// 中键或 Alt+左键平移
if (e.button === 1 || (e.button === 0 && e.altKey)) {
e.preventDefault();
setIsPanning(true);
lastMousePos.current = new Position(e.clientX, e.clientY);
} else if (e.button === 0) {
// Left click on canvas background - start box selection
// 左键点击画布背景 - 开始框选
const target = e.target as HTMLElement;
if (target === containerRef.current || target.classList.contains('ne-canvas-content')) {
const canvasPos = screenToCanvas(e.clientX, e.clientY);
onMouseDownProp?.(canvasPos, e);
}
}
}, []);
}, [screenToCanvas, onMouseDownProp]);
/**
* Handles mouse move for panning
*
* Handles mouse move for panning or box selection
*
*/
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (isPanning) {
@@ -157,13 +177,27 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
const newPan = new Position(pan.x + dx, pan.y + dy);
updatePan(newPan);
}
}, [isPanning, pan, updatePan]);
// Always call canvas mouse move for box selection
// 始终调用画布鼠标移动回调用于框选
const canvasPos = screenToCanvas(e.clientX, e.clientY);
onCanvasMouseMove?.(canvasPos, e);
}, [isPanning, pan, updatePan, screenToCanvas, onCanvasMouseMove]);
/**
* Handles mouse up to stop panning
*
* Handles mouse up to stop panning or box selection
*
*/
const handleMouseUp = useCallback(() => {
const handleMouseUp = useCallback((e: React.MouseEvent) => {
setIsPanning(false);
const canvasPos = screenToCanvas(e.clientX, e.clientY);
onCanvasMouseUp?.(canvasPos, e);
}, [screenToCanvas, onCanvasMouseUp]);
/**
* Handles mouse leave to stop panning
*
*/
const handleMouseLeave = useCallback(() => {
setIsPanning(false);
}, []);
@@ -276,7 +310,7 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
onContextMenu={handleContextMenu}
>

View File

@@ -1,11 +1,13 @@
import React, { useRef, useCallback, useState, useMemo } from 'react';
import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react';
import { Graph } from '../../domain/models/Graph';
import { GraphNode, NodeTemplate } from '../../domain/models/GraphNode';
import { Connection } from '../../domain/models/Connection';
import { Pin } from '../../domain/models/Pin';
import { Position } from '../../domain/value-objects/Position';
import { NodeGroup, computeGroupBounds, estimateNodeHeight } from '../../domain/models/NodeGroup';
import { GraphCanvas } from '../canvas/GraphCanvas';
import { MemoizedGraphNodeComponent, NodeExecutionState } from '../nodes/GraphNodeComponent';
import { MemoizedGroupNodeComponent } from '../nodes/GroupNodeComponent';
import { ConnectionLayer } from '../connections/ConnectionLine';
/**
@@ -56,6 +58,12 @@ export interface NodeEditorProps {
/** Connection context menu callback (连接右键菜单回调) */
onConnectionContextMenu?: (connection: Connection, e: React.MouseEvent) => void;
/** Group context menu callback (组右键菜单回调) */
onGroupContextMenu?: (group: NodeGroup, e: React.MouseEvent) => void;
/** Group double click callback - typically used to expand group (组双击回调 - 通常用于展开组) */
onGroupDoubleClick?: (group: NodeGroup) => void;
}
/**
@@ -80,6 +88,16 @@ interface ConnectionDragState {
isValid?: boolean;
}
/**
* Box selection state
*
*/
interface BoxSelectState {
startPos: Position;
currentPos: Position;
additive: boolean;
}
/**
* NodeEditor - Complete node graph editor component
* NodeEditor -
@@ -102,7 +120,9 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onNodeDoubleClick: _onNodeDoubleClick,
onCanvasContextMenu,
onNodeContextMenu,
onConnectionContextMenu
onConnectionContextMenu,
onGroupContextMenu,
onGroupDoubleClick
}) => {
// Silence unused variable warnings (消除未使用变量警告)
void _templates;
@@ -126,6 +146,88 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
const [dragState, setDragState] = useState<DragState | null>(null);
const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null);
const [hoveredPin, setHoveredPin] = useState<Pin | null>(null);
const [boxSelectState, setBoxSelectState] = useState<BoxSelectState | null>(null);
// Track if box selection just ended to prevent click from clearing selection
// 跟踪框选是否刚刚结束,以防止 click 清除选择
const boxSelectJustEndedRef = useRef(false);
// Force re-render after mount to ensure connections are drawn correctly
// 挂载后强制重渲染以确保连接线正确绘制
const [, forceUpdate] = useState(0);
// Track collapsed state to force connection re-render
// 跟踪折叠状态以强制连接线重渲染
const collapsedNodesKey = useMemo(() => {
return graph.nodes.map(n => `${n.id}:${n.isCollapsed}`).join(',');
}, [graph.nodes]);
// Groups are now simple visual boxes - no node hiding
// 组现在是简单的可视化框 - 不隐藏节点
// Track selected group IDs (local state, managed similarly to nodes)
// 跟踪选中的组ID本地状态类似节点管理方式
const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>(new Set());
// Group drag state - includes initial positions of nodes in the group
// 组拖拽状态 - 包含组内节点的初始位置
const [groupDragState, setGroupDragState] = useState<{
groupId: string;
startGroupPosition: Position;
startMouse: { x: number; y: number };
nodeStartPositions: Map<string, Position>;
} | null>(null);
// Key for tracking group changes
const groupsKey = useMemo(() => {
return graph.groups.map(g => `${g.id}:${g.position.x}:${g.position.y}`).join(',');
}, [graph.groups]);
// Compute dynamic group bounds based on current node positions and sizes
// 根据当前节点位置和尺寸动态计算组边界
const groupsWithDynamicBounds = useMemo(() => {
const defaultNodeWidth = 200;
return graph.groups.map(group => {
// Get current bounds of all nodes in this group
const nodeBounds = group.nodeIds
.map(nodeId => graph.getNode(nodeId))
.filter((node): node is GraphNode => node !== undefined)
.map(node => ({
x: node.position.x,
y: node.position.y,
width: defaultNodeWidth,
height: estimateNodeHeight(
node.inputPins.length,
node.outputPins.length,
node.isCollapsed
)
}));
if (nodeBounds.length === 0) {
// No nodes found, use stored position/size as fallback
return group;
}
// Calculate dynamic bounds based on actual node sizes
const { position, size } = computeGroupBounds(nodeBounds);
return {
...group,
position,
size
};
});
}, [graph.groups, graph.nodes]);
useEffect(() => {
// Use requestAnimationFrame to wait for DOM to be fully rendered
// 使用 requestAnimationFrame 等待 DOM 完全渲染
const rafId = requestAnimationFrame(() => {
forceUpdate(n => n + 1);
});
return () => cancelAnimationFrame(rafId);
}, [graph.id, collapsedNodesKey, groupsKey]);
/**
* Converts screen coordinates to canvas coordinates
@@ -146,21 +248,52 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
*
*
* DOM
*
*
*/
const getPinPosition = useCallback((pinId: string): Position | undefined => {
// First, find which node this pin belongs to
// 首先查找该引脚属于哪个节点
let ownerNode: GraphNode | undefined;
for (const node of graph.nodes) {
if (node.allPins.some(p => p.id === pinId)) {
ownerNode = node;
break;
}
}
if (!ownerNode) return undefined;
// Find the pin element and its parent node
const pinElement = containerRef.current?.querySelector(`[data-pin-id="${pinId}"]`) as HTMLElement;
if (!pinElement) return undefined;
// If pin element not found (e.g., node is collapsed), use node header position
// 如果找不到引脚元素(例如节点已收缩),使用节点头部位置
if (!pinElement) {
const nodeElement = containerRef.current?.querySelector(`[data-node-id="${ownerNode.id}"]`) as HTMLElement;
if (!nodeElement) return undefined;
const nodeRect = nodeElement.getBoundingClientRect();
const { zoom } = transformRef.current;
// Find the pin to determine if it's input or output
const pin = ownerNode.allPins.find(p => p.id === pinId);
const isOutput = pin?.isOutput ?? false;
// For collapsed nodes, position at the right side for outputs, left side for inputs
// 对于收缩的节点,输出引脚在右侧,输入引脚在左侧
const headerHeight = 28; // Approximate header height
const relativeX = isOutput ? nodeRect.width / zoom : 0;
const relativeY = headerHeight / 2;
return new Position(
ownerNode.position.x + relativeX,
ownerNode.position.y + relativeY
);
}
const nodeElement = pinElement.closest('[data-node-id]') as HTMLElement;
if (!nodeElement) return undefined;
const nodeId = nodeElement.getAttribute('data-node-id');
if (!nodeId) return undefined;
const node = graph.getNode(nodeId);
if (!node) return undefined;
// Get pin position relative to node element (in unscaled pixels)
const nodeRect = nodeElement.getBoundingClientRect();
const pinRect = pinElement.getBoundingClientRect();
@@ -172,8 +305,8 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
// Final position = node position + relative position
return new Position(
node.position.x + relativeX,
node.position.y + relativeY
ownerNode.position.x + relativeX,
ownerNode.position.y + relativeY
);
}, [graph]);
@@ -255,6 +388,22 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onGraphChange?.(newGraph);
}
// Group dragging - moves all nodes inside (group bounds are dynamic)
// 组拖拽 - 移动组内所有节点(组边界是动态计算的)
if (groupDragState) {
const dx = mousePos.x - groupDragState.startMouse.x;
const dy = mousePos.y - groupDragState.startMouse.y;
// Only move nodes - group bounds will auto-recalculate
let newGraph = graph;
for (const [nodeId, startPos] of groupDragState.nodeStartPositions) {
const newNodePos = new Position(startPos.x + dx, startPos.y + dy);
newGraph = newGraph.moveNode(nodeId, newNodePos);
}
onGraphChange?.(newGraph);
}
// Connection dragging (连接拖拽)
if (connectionDrag) {
const isValid = hoveredPin ? connectionDrag.fromPin.canConnectTo(hoveredPin) : undefined;
@@ -266,7 +415,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
isValid
} : null);
}
}, [graph, dragState, connectionDrag, hoveredPin, screenToCanvas, onGraphChange]);
}, [graph, dragState, groupDragState, connectionDrag, hoveredPin, screenToCanvas, onGraphChange]);
/**
* Handles mouse up to end dragging
@@ -278,6 +427,11 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
setDragState(null);
}
// End group dragging (结束组拖拽)
if (groupDragState) {
setGroupDragState(null);
}
// End connection dragging (结束连接拖拽)
if (connectionDrag) {
// Use hoveredPin directly instead of relying on async state update
@@ -312,7 +466,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
setConnectionDrag(null);
}
}, [graph, dragState, connectionDrag, hoveredPin, onGraphChange]);
}, [graph, dragState, groupDragState, connectionDrag, hoveredPin, onGraphChange]);
/**
* Handles pin mouse down
@@ -423,13 +577,95 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
}
}, [graph, onConnectionContextMenu]);
/**
* Handles group selection
*
*/
const handleGroupSelect = useCallback((groupId: string, additive: boolean) => {
if (readOnly) return;
const newSelection = new Set(selectedGroupIds);
if (additive) {
if (newSelection.has(groupId)) {
newSelection.delete(groupId);
} else {
newSelection.add(groupId);
}
} else {
newSelection.clear();
newSelection.add(groupId);
}
setSelectedGroupIds(newSelection);
// Clear node and connection selection when selecting groups
onSelectionChange?.(new Set(), new Set());
}, [selectedGroupIds, readOnly, onSelectionChange]);
/**
* Handles group drag start
*
*
* Captures initial positions of both the group and all nodes inside it
*
*/
const handleGroupDragStart = useCallback((groupId: string, startMouse: { x: number; y: number }) => {
if (readOnly) return;
const group = graph.getGroup(groupId);
if (!group) return;
// Convert screen coordinates to canvas coordinates (same as node dragging)
// 将屏幕坐标转换为画布坐标(与节点拖拽相同)
const canvasPos = screenToCanvas(startMouse.x, startMouse.y);
// Capture initial positions of all nodes in the group
const nodeStartPositions = new Map<string, Position>();
for (const nodeId of group.nodeIds) {
const node = graph.getNode(nodeId);
if (node) {
nodeStartPositions.set(nodeId, node.position);
}
}
setGroupDragState({
groupId,
startGroupPosition: group.position,
startMouse: { x: canvasPos.x, y: canvasPos.y },
nodeStartPositions
});
}, [graph, readOnly, screenToCanvas]);
/**
* Handles group context menu
*
*/
const handleGroupContextMenu = useCallback((group: NodeGroup, e: React.MouseEvent) => {
onGroupContextMenu?.(group, e);
}, [onGroupContextMenu]);
/**
* Handles group double click
*
*/
const handleGroupDoubleClick = useCallback((group: NodeGroup) => {
onGroupDoubleClick?.(group);
}, [onGroupDoubleClick]);
/**
* Handles canvas click to deselect
*
*/
const handleCanvasClick = useCallback((_position: Position, _e: React.MouseEvent) => {
// Skip if box selection just ended (click fires after mouseup)
// 如果框选刚刚结束则跳过click 在 mouseup 之后触发)
if (boxSelectJustEndedRef.current) {
boxSelectJustEndedRef.current = false;
return;
}
if (!readOnly) {
onSelectionChange?.(new Set(), new Set());
setSelectedGroupIds(new Set());
}
}, [readOnly, onSelectionChange]);
@@ -441,6 +677,79 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onCanvasContextMenu?.(position, e);
}, [onCanvasContextMenu]);
/**
* Handles box selection start
*
*/
const handleBoxSelectStart = useCallback((position: Position, e: React.MouseEvent) => {
if (readOnly) return;
setBoxSelectState({
startPos: position,
currentPos: position,
additive: e.ctrlKey || e.metaKey
});
}, [readOnly]);
/**
* Handles box selection move
*
*/
const handleBoxSelectMove = useCallback((position: Position) => {
if (boxSelectState) {
setBoxSelectState(prev => prev ? { ...prev, currentPos: position } : null);
}
}, [boxSelectState]);
/**
* Handles box selection end
*
*/
const handleBoxSelectEnd = useCallback(() => {
if (!boxSelectState) return;
const { startPos, currentPos, additive } = boxSelectState;
// Calculate selection box bounds
const minX = Math.min(startPos.x, currentPos.x);
const maxX = Math.max(startPos.x, currentPos.x);
const minY = Math.min(startPos.y, currentPos.y);
const maxY = Math.max(startPos.y, currentPos.y);
// Find nodes within the selection box
const nodesInBox: string[] = [];
const nodeWidth = 200; // Approximate node width
const nodeHeight = 100; // Approximate node height
for (const node of graph.nodes) {
const nodeLeft = node.position.x;
const nodeTop = node.position.y;
const nodeRight = nodeLeft + nodeWidth;
const nodeBottom = nodeTop + nodeHeight;
// Check if node intersects with selection box
const intersects = !(nodeRight < minX || nodeLeft > maxX || nodeBottom < minY || nodeTop > maxY);
if (intersects) {
nodesInBox.push(node.id);
}
}
// Update selection
if (additive) {
// Add to existing selection
const newSelection = new Set(selectedNodeIds);
nodesInBox.forEach(id => newSelection.add(id));
onSelectionChange?.(newSelection, new Set());
} else {
// Replace selection
onSelectionChange?.(new Set(nodesInBox), new Set());
}
// Mark that box selection just ended to prevent click from clearing selection
// 标记框选刚刚结束,以防止 click 清除选择
boxSelectJustEndedRef.current = true;
setBoxSelectState(null);
}, [boxSelectState, graph.nodes, selectedNodeIds, onSelectionChange]);
/**
* Handles pin value change
*
@@ -497,7 +806,25 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onContextMenu={handleCanvasContextMenu}
onPanChange={handlePanChange}
onZoomChange={handleZoomChange}
onMouseDown={handleBoxSelectStart}
onCanvasMouseMove={handleBoxSelectMove}
onCanvasMouseUp={handleBoxSelectEnd}
>
{/* Group boxes - rendered first so they appear behind nodes (组框 - 先渲染,这样显示在节点后面) */}
{/* Use dynamically calculated bounds so groups auto-resize to fit nodes */}
{groupsWithDynamicBounds.map(group => (
<MemoizedGroupNodeComponent
key={group.id}
group={group}
isSelected={selectedGroupIds.has(group.id)}
isDragging={groupDragState?.groupId === group.id}
onSelect={handleGroupSelect}
onDragStart={handleGroupDragStart}
onContextMenu={handleGroupContextMenu}
onDoubleClick={handleGroupDoubleClick}
/>
))}
{/* Connection layer (连接层) */}
<ConnectionLayer
connections={graph.connections}
@@ -509,7 +836,7 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onConnectionContextMenu={handleConnectionContextMenu}
/>
{/* Nodes (节点) */}
{/* All Nodes (所有节点) */}
{graph.nodes.map(node => (
<MemoizedGraphNodeComponent
key={node.id}
@@ -531,6 +858,19 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onToggleCollapse={handleToggleCollapse}
/>
))}
{/* Box selection overlay (框选覆盖层) */}
{boxSelectState && (
<div
className="ne-selection-box"
style={{
left: Math.min(boxSelectState.startPos.x, boxSelectState.currentPos.x),
top: Math.min(boxSelectState.startPos.y, boxSelectState.currentPos.y),
width: Math.abs(boxSelectState.currentPos.x - boxSelectState.startPos.x),
height: Math.abs(boxSelectState.currentPos.y - boxSelectState.startPos.y)
}}
/>
)}
</GraphCanvas>
</div>
);

View File

@@ -64,6 +64,8 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
return draggingFromPin.canConnectTo(pin);
}, [draggingFromPin]);
const hasError = Boolean(node.data.invalidVariable);
const classNames = useMemo(() => {
const classes = ['ne-node'];
if (isSelected) classes.push('selected');
@@ -71,8 +73,9 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
if (node.isCollapsed) classes.push('collapsed');
if (node.category === 'comment') classes.push('comment');
if (executionState !== 'idle') classes.push(executionState);
if (hasError) classes.push('has-error');
return classes.join(' ');
}, [isSelected, isDragging, node.isCollapsed, node.category, executionState]);
}, [isSelected, isDragging, node.isCollapsed, node.category, executionState, hasError]);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
@@ -102,8 +105,10 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
: undefined;
// Separate exec pins from data pins
// Also filter out pins listed in data.hiddenInputs
const hiddenInputs = (node.data.hiddenInputs as string[]) || [];
const inputExecPins = node.inputPins.filter(p => p.isExec && !p.hidden);
const inputDataPins = node.inputPins.filter(p => !p.isExec && !p.hidden);
const inputDataPins = node.inputPins.filter(p => !p.isExec && !p.hidden && !hiddenInputs.includes(p.name));
const outputExecPins = node.outputPins.filter(p => p.isExec && !p.hidden);
const outputDataPins = node.outputPins.filter(p => !p.isExec && !p.hidden);
@@ -129,13 +134,17 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
className={`ne-node-header ${node.category}`}
style={headerStyle}
>
{/* Diamond icon for event nodes, or custom icon */}
{/* Warning icon for invalid nodes, or diamond/custom icon */}
<span className="ne-node-header-icon">
{node.icon && renderIcon ? renderIcon(node.icon) : null}
{hasError ? (
<span className="ne-node-warning-icon" title={`Variable '${node.data.variableName}' not found`}></span>
) : (
node.icon && renderIcon ? renderIcon(node.icon) : null
)}
</span>
<span className="ne-node-header-title">
{node.title}
{(node.data.displayTitle as string) || node.title}
{node.subtitle && (
<span className="ne-node-header-subtitle">
{node.subtitle}

View File

@@ -0,0 +1,124 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { NodeGroup } from '../../domain/models/NodeGroup';
import '../../styles/GroupNode.css';
export interface GroupNodeComponentProps {
/** The group to render */
group: NodeGroup;
/** Whether the group is selected */
isSelected?: boolean;
/** Whether the group is being dragged */
isDragging?: boolean;
/** Selection handler */
onSelect?: (groupId: string, additive: boolean) => void;
/** Drag start handler */
onDragStart?: (groupId: string, startPosition: { x: number; y: number }) => void;
/** Context menu handler */
onContextMenu?: (group: NodeGroup, e: React.MouseEvent) => void;
/** Double click handler for editing name */
onDoubleClick?: (group: NodeGroup) => void;
}
/**
* GroupNodeComponent - Renders a visual group box around nodes
* GroupNodeComponent - 渲染节点周围的可视化组框
*
* This is a simple background box that provides visual organization.
* 这是一个简单的背景框,提供视觉组织功能。
*/
export const GroupNodeComponent: React.FC<GroupNodeComponentProps> = ({
group,
isSelected = false,
isDragging = false,
onSelect,
onDragStart,
onContextMenu,
onDoubleClick
}) => {
const groupRef = useRef<HTMLDivElement>(null);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
// Only handle clicks on the header or border, not on the content area
const target = e.target as HTMLElement;
if (!target.closest('.ne-group-box-header') && !target.classList.contains('ne-group-box')) {
return;
}
e.stopPropagation();
const additive = e.ctrlKey || e.metaKey;
onSelect?.(group.id, additive);
onDragStart?.(group.id, { x: e.clientX, y: e.clientY });
}, [group.id, onSelect, onDragStart]);
const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onContextMenu?.(group, e);
}, [group, onContextMenu]);
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
// Only handle double-click on the header
const target = e.target as HTMLElement;
if (!target.closest('.ne-group-box-header')) {
return;
}
e.stopPropagation();
onDoubleClick?.(group);
}, [group, onDoubleClick]);
const classNames = useMemo(() => {
const classes = ['ne-group-box'];
if (isSelected) classes.push('selected');
if (isDragging) classes.push('dragging');
return classes.join(' ');
}, [isSelected, isDragging]);
const style: React.CSSProperties = useMemo(() => ({
left: group.position.x,
top: group.position.y,
width: group.size.width,
height: group.size.height,
'--group-color': group.color || 'rgba(100, 149, 237, 0.15)'
} as React.CSSProperties), [group.position.x, group.position.y, group.size.width, group.size.height, group.color]);
return (
<div
ref={groupRef}
className={classNames}
style={style}
data-group-id={group.id}
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
onDoubleClick={handleDoubleClick}
>
<div className="ne-group-box-header">
<span className="ne-group-box-title">{group.name}</span>
</div>
</div>
);
};
/**
* Memoized version for performance
*/
export const MemoizedGroupNodeComponent = React.memo(GroupNodeComponent, (prev, next) => {
if (prev.group.id !== next.group.id) return false;
if (prev.isSelected !== next.isSelected) return false;
if (prev.isDragging !== next.isDragging) return false;
if (prev.group.position.x !== next.group.position.x ||
prev.group.position.y !== next.group.position.y) return false;
if (prev.group.size.width !== next.group.size.width ||
prev.group.size.height !== next.group.size.height) return false;
if (prev.group.name !== next.group.name) return false;
if (prev.group.color !== next.group.color) return false;
return true;
});
export default GroupNodeComponent;

View File

@@ -4,3 +4,9 @@ export {
type GraphNodeComponentProps,
type NodeExecutionState
} from './GraphNodeComponent';
export {
GroupNodeComponent,
MemoizedGroupNodeComponent,
type GroupNodeComponentProps
} from './GroupNodeComponent';

View File

@@ -2,6 +2,7 @@ import { GraphNode } from './GraphNode';
import { Connection } from './Connection';
import { Pin } from './Pin';
import { Position } from '../value-objects/Position';
import { NodeGroup, serializeNodeGroup } from './NodeGroup';
/**
* Graph - Aggregate root for the node graph
@@ -15,6 +16,7 @@ export class Graph {
private readonly _name: string;
private readonly _nodes: Map<string, GraphNode>;
private readonly _connections: Connection[];
private readonly _groups: NodeGroup[];
private readonly _metadata: Record<string, unknown>;
constructor(
@@ -22,12 +24,14 @@ export class Graph {
name: string,
nodes: GraphNode[] = [],
connections: Connection[] = [],
metadata: Record<string, unknown> = {}
metadata: Record<string, unknown> = {},
groups: NodeGroup[] = []
) {
this._id = id;
this._name = name;
this._nodes = new Map(nodes.map(n => [n.id, n]));
this._connections = [...connections];
this._groups = [...groups];
this._metadata = { ...metadata };
}
@@ -59,6 +63,29 @@ export class Graph {
return this._connections.length;
}
/**
* Gets all groups ()
*/
get groups(): NodeGroup[] {
return [...this._groups];
}
/**
* Gets a group by ID
* ID获取组
*/
getGroup(groupId: string): NodeGroup | undefined {
return this._groups.find(g => g.id === groupId);
}
/**
* Gets the group containing a specific node
*
*/
getNodeGroup(nodeId: string): NodeGroup | undefined {
return this._groups.find(g => g.nodeIds.includes(nodeId));
}
/**
* Gets a node by ID
* ID获取节点
@@ -112,7 +139,7 @@ export class Graph {
throw new Error(`Node with ID "${node.id}" already exists`);
}
const newNodes = [...this.nodes, node];
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata);
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata, this._groups);
}
/**
@@ -125,7 +152,14 @@ export class Graph {
}
const newNodes = this.nodes.filter(n => n.id !== nodeId);
const newConnections = this._connections.filter(c => !c.involvesNode(nodeId));
return new Graph(this._id, this._name, newNodes, newConnections, this._metadata);
// Also remove the node from any groups it belongs to
const newGroups = this._groups.map(g => {
if (g.nodeIds.includes(nodeId)) {
return { ...g, nodeIds: g.nodeIds.filter(id => id !== nodeId) };
}
return g;
}).filter(g => g.nodeIds.length > 0); // Remove empty groups
return new Graph(this._id, this._name, newNodes, newConnections, this._metadata, newGroups);
}
/**
@@ -138,7 +172,7 @@ export class Graph {
const updatedNode = updater(node);
const newNodes = this.nodes.map(n => n.id === nodeId ? updatedNode : n);
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata);
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata, this._groups);
}
/**
@@ -184,7 +218,7 @@ export class Graph {
}
newConnections.push(connection);
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata, this._groups);
}
/**
@@ -196,7 +230,7 @@ export class Graph {
if (newConnections.length === this._connections.length) {
return this;
}
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata, this._groups);
}
/**
@@ -208,7 +242,47 @@ export class Graph {
if (newConnections.length === this._connections.length) {
return this;
}
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata, this._groups);
}
// ========== Group Operations (组操作) ==========
/**
* Adds a new group (immutable)
*
*/
addGroup(group: NodeGroup): Graph {
if (this._groups.some(g => g.id === group.id)) {
throw new Error(`Group with ID "${group.id}" already exists`);
}
const newGroups = [...this._groups, group];
return new Graph(this._id, this._name, this.nodes, this._connections, this._metadata, newGroups);
}
/**
* Removes a group (immutable)
*
*/
removeGroup(groupId: string): Graph {
const newGroups = this._groups.filter(g => g.id !== groupId);
if (newGroups.length === this._groups.length) {
return this;
}
return new Graph(this._id, this._name, this.nodes, this._connections, this._metadata, newGroups);
}
/**
* Updates a group (immutable)
*
*/
updateGroup(groupId: string, updater: (group: NodeGroup) => NodeGroup): Graph {
const groupIndex = this._groups.findIndex(g => g.id === groupId);
if (groupIndex === -1) return this;
const updatedGroup = updater(this._groups[groupIndex]);
const newGroups = [...this._groups];
newGroups[groupIndex] = updatedGroup;
return new Graph(this._id, this._name, this.nodes, this._connections, this._metadata, newGroups);
}
/**
@@ -219,7 +293,7 @@ export class Graph {
return new Graph(this._id, this._name, this.nodes, this._connections, {
...this._metadata,
...metadata
});
}, this._groups);
}
/**
@@ -227,7 +301,7 @@ export class Graph {
*
*/
rename(newName: string): Graph {
return new Graph(this._id, newName, this.nodes, this._connections, this._metadata);
return new Graph(this._id, newName, this.nodes, this._connections, this._metadata, this._groups);
}
/**
@@ -257,6 +331,7 @@ export class Graph {
name: this._name,
nodes: this.nodes.map(n => n.toJSON()),
connections: this._connections.map(c => c.toJSON()),
groups: this._groups.map(g => serializeNodeGroup(g)),
metadata: this._metadata
};
}

View File

@@ -0,0 +1,146 @@
import { Position } from '../value-objects/Position';
/**
* NodeGroup - Represents a visual group box around nodes
* NodeGroup - 表示节点周围的可视化组框
*
* Groups are purely visual organization - they don't affect runtime execution.
* 组是纯视觉组织 - 不影响运行时执行。
*/
export interface NodeGroup {
/** Unique identifier for the group */
id: string;
/** Display name of the group */
name: string;
/** IDs of nodes contained in this group */
nodeIds: string[];
/** Position of the group box (top-left corner) */
position: Position;
/** Size of the group box */
size: { width: number; height: number };
/** Optional color for the group box */
color?: string;
}
/**
* Creates a new NodeGroup with the given properties
*/
export function createNodeGroup(
id: string,
name: string,
nodeIds: string[],
position: Position,
size: { width: number; height: number },
color?: string
): NodeGroup {
return {
id,
name,
nodeIds: [...nodeIds],
position,
size,
color
};
}
/**
* Node bounds info for group calculation
* 用于组计算的节点边界信息
*/
export interface NodeBounds {
x: number;
y: number;
width: number;
height: number;
}
/**
* Estimates node height based on pin count
* 根据引脚数量估算节点高度
*/
export function estimateNodeHeight(inputPinCount: number, outputPinCount: number, isCollapsed: boolean = false): number {
if (isCollapsed) {
return 32; // Just header
}
const headerHeight = 32;
const pinHeight = 26;
const bottomPadding = 12;
const maxPins = Math.max(inputPinCount, outputPinCount);
return headerHeight + maxPins * pinHeight + bottomPadding;
}
/**
* Computes the bounding box for a group based on its nodes
* Returns position (top-left) and size with padding
*
* @param nodeBounds - Array of node bounds (position + size)
* @param padding - Padding around the group box
*/
export function computeGroupBounds(
nodeBounds: NodeBounds[],
padding: number = 30
): { position: Position; size: { width: number; height: number } } {
if (nodeBounds.length === 0) {
return {
position: new Position(0, 0),
size: { width: 250, height: 150 }
};
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const node of nodeBounds) {
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + node.width);
maxY = Math.max(maxY, node.y + node.height);
}
// Add padding and header space for group title
const groupHeaderHeight = 28;
return {
position: new Position(minX - padding, minY - padding - groupHeaderHeight),
size: {
width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2 + groupHeaderHeight
}
};
}
/**
* Serializes a NodeGroup for JSON storage
*/
export function serializeNodeGroup(group: NodeGroup): Record<string, unknown> {
return {
id: group.id,
name: group.name,
nodeIds: [...group.nodeIds],
position: { x: group.position.x, y: group.position.y },
size: { width: group.size.width, height: group.size.height },
color: group.color
};
}
/**
* Deserializes a NodeGroup from JSON
*/
export function deserializeNodeGroup(data: Record<string, unknown>): NodeGroup {
const pos = data.position as { x: number; y: number } | undefined;
const size = data.size as { width: number; height: number } | undefined;
return {
id: data.id as string,
name: data.name as string,
nodeIds: (data.nodeIds as string[]) || [],
position: new Position(pos?.x || 0, pos?.y || 0),
size: size || { width: 250, height: 150 },
color: data.color as string | undefined
};
}

View File

@@ -2,3 +2,12 @@ export { Pin, type PinDefinition } from './Pin';
export { GraphNode, type NodeTemplate, type NodeCategory } from './GraphNode';
export { Connection } from './Connection';
export { Graph } from './Graph';
export {
type NodeGroup,
type NodeBounds,
createNodeGroup,
computeGroupBounds,
estimateNodeHeight,
serializeNodeGroup,
deserializeNodeGroup
} from './NodeGroup';

View File

@@ -10,6 +10,9 @@
// Import styles (导入样式)
import './styles/index.css';
// CSS utilities for Shadow DOM (Shadow DOM 的 CSS 工具)
export { nodeEditorCssText, injectNodeEditorStyles } from './styles/cssText';
// Domain models (领域模型)
export {
// Models
@@ -20,7 +23,15 @@ export {
// Types
type NodeTemplate,
type NodeCategory,
type PinDefinition
type PinDefinition,
// NodeGroup
type NodeGroup,
type NodeBounds,
createNodeGroup,
computeGroupBounds,
estimateNodeHeight,
serializeNodeGroup,
deserializeNodeGroup
} from './domain/models';
// Value objects (值对象)

View File

@@ -38,6 +38,24 @@
z-index: var(--ne-z-dragging);
}
/* Error state for invalid variable references */
.ne-node.has-error {
border-color: #e74c3c;
box-shadow: 0 0 0 1px #e74c3c,
0 0 12px rgba(231, 76, 60, 0.4),
0 4px 8px rgba(0, 0, 0, 0.4);
}
.ne-node.has-error .ne-node-header {
background: linear-gradient(180deg, #c0392b 0%, #962d22 100%) !important;
}
.ne-node-warning-icon {
color: #f1c40f;
font-size: 14px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
}
/* ==================== Node Header 节点头部 ==================== */
.ne-node-header {
display: flex;
@@ -263,3 +281,32 @@
0 0 20px rgba(255, 167, 38, 0.7);
}
}
/* ==================== Group Node 组节点 ==================== */
.ne-node.ne-group-node {
border: 2px dashed rgba(100, 149, 237, 0.6);
background: rgba(40, 60, 90, 0.85);
}
.ne-node.ne-group-node:hover {
border-color: rgba(100, 149, 237, 0.8);
}
.ne-node.ne-group-node.selected {
border-color: #6495ed;
box-shadow: 0 0 0 1px #6495ed,
0 0 16px rgba(100, 149, 237, 0.5),
0 4px 12px rgba(0, 0, 0, 0.5);
}
.ne-group-header {
background: linear-gradient(90deg, #3a5f8a 0%, #2a4a6a 100%) !important;
}
.ne-group-hint {
padding: 8px 12px;
font-size: 11px;
color: #8899aa;
font-style: italic;
text-align: center;
}

View File

@@ -0,0 +1,60 @@
/**
* Group Box Styles
* 组框样式
*/
/* ==================== Group Box Container 组框容器 ==================== */
.ne-group-box {
position: absolute;
background: var(--group-color, rgba(100, 149, 237, 0.15));
border: 2px dashed rgba(100, 149, 237, 0.5);
border-radius: 8px;
pointer-events: auto;
z-index: 0;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.ne-group-box:hover {
border-color: rgba(100, 149, 237, 0.7);
}
.ne-group-box.selected {
border-color: var(--ne-node-border-selected, #e5a020);
box-shadow: 0 0 0 1px var(--ne-node-border-selected, #e5a020),
0 0 12px rgba(229, 160, 32, 0.3);
}
.ne-group-box.dragging {
opacity: 0.9;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
/* ==================== Group Box Header 组框头部 ==================== */
.ne-group-box-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 28px;
display: flex;
align-items: center;
padding: 0 10px;
background: rgba(100, 149, 237, 0.3);
border-radius: 6px 6px 0 0;
cursor: grab;
user-select: none;
}
.ne-group-box-header:active {
cursor: grabbing;
}
.ne-group-box-title {
color: #fff;
font-size: 12px;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -0,0 +1,55 @@
/**
* @zh 节点编辑器 CSS 样式文本
* @en Node Editor CSS style text
*
* @zh 此文件在构建时由 vite 插件自动生成
* @en This file is auto-generated by vite plugin during build
*/
// Placeholder - will be replaced by vite plugin during build
export const nodeEditorCssText = '__NODE_EDITOR_CSS_PLACEHOLDER__';
/**
* @zh 将 CSS 注入到指定的根节点(支持 Shadow DOM
* @en Inject CSS into specified root node (supports Shadow DOM)
*
* @param root - @zh 目标根节点Document 或 ShadowRoot@en Target root node (Document or ShadowRoot)
* @param styleId - @zh 样式标签的 ID @en ID for the style tag
* @returns @zh 创建的 style 元素 @en The created style element
*
* @example
* ```typescript
* // Inject into Shadow DOM
* const shadowRoot = element.attachShadow({ mode: 'open' });
* injectNodeEditorStyles(shadowRoot);
*
* // Inject into document (with custom ID)
* injectNodeEditorStyles(document, 'my-editor-styles');
* ```
*/
export function injectNodeEditorStyles(
root: Document | ShadowRoot | DocumentFragment,
styleId: string = 'esengine-node-editor-styles'
): HTMLStyleElement | null {
// Check if already injected
const existingStyle = (root as any).getElementById?.(styleId) ||
(root as any).querySelector?.(`#${styleId}`);
if (existingStyle) {
return existingStyle as HTMLStyleElement;
}
// Create and inject style element
const style = document.createElement('style');
style.id = styleId;
style.textContent = nodeEditorCssText;
if ('head' in root) {
// Document
(root as Document).head.appendChild(style);
} else {
// ShadowRoot or DocumentFragment
root.appendChild(style);
}
return style;
}

View File

@@ -6,6 +6,7 @@
@import './variables.css';
@import './Canvas.css';
@import './GraphNode.css';
@import './GroupNode.css';
@import './NodePin.css';
@import './Connection.css';
@import './ContextMenu.css';

View File

@@ -4,12 +4,14 @@ import dts from 'vite-plugin-dts';
import react from '@vitejs/plugin-react';
/**
* Custom plugin: Convert CSS to self-executing style injection code
* CSS
* Custom plugin: Handle CSS for node editor
* CSS
*
* This plugin does two things:
* 1. Auto-injects CSS into document.head for normal usage
* 2. Replaces placeholder in cssText.ts with actual CSS for Shadow DOM usage
*/
function injectCSSPlugin(): any {
let cssCounter = 0;
return {
name: 'inject-css-plugin',
enforce: 'post' as const,
@@ -23,19 +25,28 @@ function injectCSSPlugin(): any {
const cssChunk = bundle[cssFile];
if (!cssChunk || !cssChunk.source) continue;
const cssContent = cssChunk.source;
const styleId = `esengine-node-editor-style-${cssCounter++}`;
const cssContent = cssChunk.source as string;
const styleId = 'esengine-node-editor-styles';
// Generate style injection code (生成样式注入代码)
const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${JSON.stringify(cssContent)};document.head.appendChild(s);}}})();`;
// Inject into index.js (注入到 index.js)
// Process all JS bundles (处理所有 JS 包)
for (const jsKey of bundleKeys) {
if (!jsKey.endsWith('.js')) continue;
if (!jsKey.endsWith('.js') && !jsKey.endsWith('.cjs')) continue;
const jsChunk = bundle[jsKey];
if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue;
if (jsKey === 'index.js') {
// Replace CSS placeholder with actual CSS content
// 将 CSS 占位符替换为实际的 CSS 内容
// Match both single and double quotes (ESM uses single, CJS uses double)
jsChunk.code = jsChunk.code.replace(
/['"]__NODE_EDITOR_CSS_PLACEHOLDER__['"]/g,
JSON.stringify(cssContent)
);
// Auto-inject CSS for index bundles (为 index 包自动注入 CSS)
if (jsKey === 'index.js' || jsKey === 'index.cjs') {
jsChunk.code = injectCode + '\n' + jsChunk.code;
}
}
@@ -65,8 +76,11 @@ export default defineConfig({
entry: {
index: resolve(__dirname, 'src/index.ts')
},
formats: ['es'],
fileName: (format, entryName) => `${entryName}.js`
formats: ['es', 'cjs'],
fileName: (format, entryName) => {
if (format === 'cjs') return `${entryName}.cjs`;
return `${entryName}.js`;
}
},
rollupOptions: {
external: [

Some files were not shown because too many files have changed in this diff Show More