Compare commits

...

16 Commits

Author SHA1 Message Date
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
141 changed files with 12208 additions and 2084 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

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

@@ -28,13 +28,13 @@ 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');
// Get input (using evaluateInput)
const value = context.evaluateInput(node.id, 'value', 0) as number;
// Execute logic
const result = value * 2;
@@ -100,29 +100,58 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
};
```
## Example: Input Handler Node
## Example: ECS Component Operation Node
```typescript
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
import type { Entity } from '@esengine/ecs-framework';
import { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
import { ExecutionContext, ExecutionResult } from '@esengine/blueprint';
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
// Custom heal node
const HealEntityTemplate: BlueprintNodeTemplate = {
type: 'HealEntity',
title: 'Heal Entity',
category: 'gameplay',
color: '#22aa22',
description: 'Heal an entity with HealthComponent',
keywords: ['heal', 'health', 'restore'],
menuPath: ['Gameplay', 'Combat', 'Heal Entity'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Target' },
{ name: 'amount', type: 'float', displayName: 'Amount', defaultValue: 10 }
],
isPure: true
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'newHealth', type: 'float', displayName: 'New Health' }
]
};
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
@RegisterNode(HealEntityTemplate)
class HealEntityExecutor 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 } };
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const amount = context.evaluateInput(node.id, 'amount', 10) as number;
if (!entity || entity.isDestroyed) {
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
}
// Get HealthComponent
const health = entity.components.find(c =>
(c.constructor as any).__componentName__ === 'Health'
) as any;
if (health) {
health.current = Math.min(health.current + amount, health.max);
return {
outputs: { newHealth: health.current },
nextExec: 'exec'
};
}
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
}
}
```

View File

@@ -3,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,8 +1,9 @@
---
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
@@ -10,405 +11,141 @@ title: "Blueprint Visual Scripting"
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
## Documentation Navigation
| 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
- [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,118 @@
---
title: "Built-in Nodes"
description: "Blueprint built-in node reference"
title: "ECS Node Reference"
description: "Blueprint built-in ECS operation nodes"
---
## Event Nodes
Lifecycle events as blueprint entry points:
| Node | Description |
|------|-------------|
| `EventBeginPlay` | Triggered when blueprint starts |
| `EventTick` | Triggered each frame |
| `EventTick` | Triggered each frame, receives deltaTime |
| `EventEndPlay` | Triggered when blueprint stops |
| `EventCollision` | Triggered on collision |
| `EventInput` | Triggered on input event |
| `EventTimer` | Triggered by timer |
| `EventMessage` | Triggered by custom message |
## Entity Nodes
ECS entity operations:
| Node | Description | Type |
|------|-------------|------|
| `Get Self` | Get entity owning this blueprint | Pure |
| `Create Entity` | Create new entity in scene | Execution |
| `Destroy Entity` | Destroy specified entity | Execution |
| `Destroy Self` | Destroy self entity | Execution |
| `Is Valid` | Check if entity is valid | Pure |
| `Get Entity Name` | Get entity name | Pure |
| `Set Entity Name` | Set entity name | Execution |
| `Get Entity Tag` | Get entity tag | Pure |
| `Set Entity Tag` | Set entity tag | Execution |
| `Set Active` | Set entity active state | Execution |
| `Is Active` | Check if entity is active | Pure |
| `Find Entity By Name` | Find entity by name | Pure |
| `Find Entities By Tag` | Find all entities by tag | Pure |
| `Get Entity ID` | Get entity unique ID | Pure |
| `Find Entity By ID` | Find entity by ID | Pure |
## Component Nodes
ECS component operations:
| Node | Description | Type |
|------|-------------|------|
| `Has Component` | Check if entity has specified component | Pure |
| `Get Component` | Get component from entity | Pure |
| `Get All Components` | Get all components from entity | Pure |
| `Remove Component` | Remove component | Execution |
| `Get Component Property` | Get component property value | Pure |
| `Set Component Property` | Set component property value | Execution |
| `Get Component Type` | Get component type name | Pure |
| `Get Owner Entity` | Get owning entity from component | Pure |
## Flow Control Nodes
Control execution flow:
| 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 |
| `For Loop` | Loop execution |
| `For Each` | Iterate array |
| `While Loop` | Conditional loop |
| `Do Once` | Execute only once |
| `Flip Flop` | Alternate between two branches |
| `Gate` | Toggleable execution gate |
## Time Nodes
| Node | Description |
|------|-------------|
| `Delay` | Delay execution |
| `GetDeltaTime` | Get frame delta time |
| `GetTime` | Get runtime |
| `SetTimer` | Set timer |
| `ClearTimer` | Clear timer |
| Node | Description | Type |
|------|-------------|------|
| `Delay` | Delay execution | Execution |
| `Get Delta Time` | Get frame delta time | Pure |
| `Get Time` | Get total runtime | Pure |
## Math Nodes
| Node | Description |
|------|-------------|
| `Add` | Addition |
| `Subtract` | Subtraction |
| `Multiply` | Multiplication |
| `Divide` | Division |
| `Add` / `Subtract` / `Multiply` / `Divide` | Basic operations |
| `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 |
## Entity Nodes
| Node | Description |
|------|-------------|
| `GetSelf` | Get current entity |
| `GetComponent` | Get component |
| `HasComponent` | Check component |
| `AddComponent` | Add component |
| `RemoveComponent` | Remove component |
| `SpawnEntity` | Create entity |
| `DestroyEntity` | Destroy entity |
## Variable Nodes
| Node | Description |
|------|-------------|
| `GetVariable` | Get variable value |
| `SetVariable` | Set variable value |
## Debug Nodes
| Node | Description |
|------|-------------|
| `Print` | Print to console |
| `DrawDebugLine` | Draw debug line |
| `DrawDebugPoint` | Draw debug point |
| `Breakpoint` | Debug breakpoint |
## Auto-generated Component Nodes
Components marked with `@BlueprintExpose` decorator auto-generate nodes:
```typescript
@ECSComponent('Transform')
@BlueprintExpose({ displayName: 'Transform', category: 'core' })
export class TransformComponent extends Component {
@BlueprintProperty({ displayName: 'X Position' })
x: number = 0;
@BlueprintProperty({ displayName: 'Y Position' })
y: number = 0;
@BlueprintMethod({ displayName: 'Translate' })
translate(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
}
```
Generated nodes:
- **Get Transform** - Get Transform component
- **Get X Position** / **Set X Position** - Access x property
- **Get Y Position** / **Set Y Position** - Access y property
- **Translate** - Call translate method

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

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

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

@@ -28,13 +28,13 @@ 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');
// 获取输入(使用 evaluateInput
const value = context.evaluateInput(node.id, 'value', 0) as number;
// 执行逻辑
const result = value * 2;
@@ -100,29 +100,58 @@ const PureNodeTemplate: BlueprintNodeTemplate = {
};
```
## 实际示例:输入处理节点
## 实际示例:ECS 组件操作节点
```typescript
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
import type { Entity } from '@esengine/ecs-framework';
import { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
import { ExecutionContext, ExecutionResult } from '@esengine/blueprint';
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
// 自定义治疗节点
const HealEntityTemplate: BlueprintNodeTemplate = {
type: 'HealEntity',
title: 'Heal Entity',
category: 'gameplay',
color: '#22aa22',
description: 'Heal an entity with HealthComponent',
keywords: ['heal', 'health', 'restore'],
menuPath: ['Gameplay', 'Combat', 'Heal Entity'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Target' },
{ name: 'amount', type: 'float', displayName: 'Amount', defaultValue: 10 }
],
isPure: true
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'newHealth', type: 'float', displayName: 'New Health' }
]
};
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
@RegisterNode(HealEntityTemplate)
class HealEntityExecutor 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 } };
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const amount = context.evaluateInput(node.id, 'amount', 10) as number;
if (!entity || entity.isDestroyed) {
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
}
// 获取 HealthComponent
const health = entity.components.find(c =>
(c.constructor as any).__componentName__ === 'Health'
) as any;
if (health) {
health.current = Math.min(health.current + amount, health.max);
return {
outputs: { newHealth: health.current },
nextExec: 'exec'
};
}
return { outputs: { newHealth: 0 }, nextExec: 'exec' };
}
}
```

View File

@@ -3,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,9 +1,9 @@
---
title: "蓝图可视化脚本 (Blueprint)"
description: "完整的可视化脚本系统"
description: "与 ECS 框架深度集成的可视化脚本系统"
---
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合
`@esengine/blueprint` 提供与 ECS 框架深度集成的可视化脚本系统,支持通过节点式编程控制实体行为
## 安装
@@ -11,104 +11,141 @@ description: "完整的可视化脚本系统"
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 集成和最佳实践
- [虚拟机 API](./vm) - BlueprintVM 与 ECS 集成
- [ECS 节点参考](./nodes) - 内置 ECS 操作节点
- [自定义节点](./custom-nodes) - 创建自定义 ECS 节点
- [蓝图组合](./composition) - 片段复用
- [实际示例](./examples) - ECS 游戏逻辑示例

View File

@@ -1,107 +1,118 @@
---
title: "内置节点"
description: "蓝图内置节点参考"
title: "ECS 节点参考"
description: "蓝图内置 ECS 操作节点"
---
## 事件节点
生命周期事件,作为蓝图执行的入口点:
| 节点 | 说明 |
|------|------|
| `EventBeginPlay` | 蓝图启动时触发 |
| `EventTick` | 每帧触发 |
| `EventTick` | 每帧触发,接收 deltaTime |
| `EventEndPlay` | 蓝图停止时触发 |
| `EventCollision` | 碰撞时触发 |
| `EventInput` | 输入事件触发 |
| `EventTimer` | 定时器触发 |
| `EventMessage` | 自定义消息触发 |
## 流程控制节点
## 实体节点 (Entity)
操作 ECS 实体:
| 节点 | 说明 | 类型 |
|------|------|------|
| `Get Self` | 获取拥有此蓝图的实体 | 纯节点 |
| `Create Entity` | 在场景中创建新实体 | 执行节点 |
| `Destroy Entity` | 销毁指定实体 | 执行节点 |
| `Destroy Self` | 销毁自身实体 | 执行节点 |
| `Is Valid` | 检查实体是否有效 | 纯节点 |
| `Get Entity Name` | 获取实体名称 | 纯节点 |
| `Set Entity Name` | 设置实体名称 | 执行节点 |
| `Get Entity Tag` | 获取实体标签 | 纯节点 |
| `Set Entity Tag` | 设置实体标签 | 执行节点 |
| `Set Active` | 设置实体激活状态 | 执行节点 |
| `Is Active` | 检查实体是否激活 | 纯节点 |
| `Find Entity By Name` | 按名称查找实体 | 纯节点 |
| `Find Entities By Tag` | 按标签查找所有实体 | 纯节点 |
| `Get Entity ID` | 获取实体唯一 ID | 纯节点 |
| `Find Entity By ID` | 按 ID 查找实体 | 纯节点 |
## 组件节点 (Component)
操作 ECS 组件:
| 节点 | 说明 | 类型 |
|------|------|------|
| `Has Component` | 检查实体是否有指定组件 | 纯节点 |
| `Get Component` | 获取实体的组件 | 纯节点 |
| `Get All Components` | 获取实体所有组件 | 纯节点 |
| `Remove Component` | 移除组件 | 执行节点 |
| `Get Component Property` | 获取组件属性值 | 纯节点 |
| `Set Component Property` | 设置组件属性值 | 执行节点 |
| `Get Component Type` | 获取组件类型名称 | 纯节点 |
| `Get Owner Entity` | 从组件获取所属实体 | 纯节点 |
## 流程控制节点 (Flow)
控制执行流程:
| 节点 | 说明 |
|------|------|
| `Branch` | 条件分支 (if/else) |
| `Sequence` | 顺序执行多个输出 |
| `ForLoop` | 循环执行 |
| `WhileLoop` | 条件循环 |
| `DoOnce` | 只执行一次 |
| `FlipFlop` | 交替执行两个分支 |
| `For Loop` | 循环执行 |
| `For Each` | 遍历数组 |
| `While Loop` | 条件循环 |
| `Do Once` | 只执行一次 |
| `Flip Flop` | 交替执行两个分支 |
| `Gate` | 可开关的执行门 |
## 时间节点
## 时间节点 (Time)
| 节点 | 说明 | 类型 |
|------|------|------|
| `Delay` | 延迟执行 | 执行节点 |
| `Get Delta Time` | 获取帧间隔时间 | 纯节点 |
| `Get Time` | 获取运行总时间 | 纯节点 |
## 数学节点 (Math)
| 节点 | 说明 |
|------|------|
| `Delay` | 延迟执行 |
| `GetDeltaTime` | 获取帧间隔 |
| `GetTime` | 获取运行时间 |
| `SetTimer` | 设置定时器 |
| `ClearTimer` | 清除定时器 |
## 数学节点
| 节点 | 说明 |
|------|------|
| `Add` | 加法 |
| `Subtract` | 减法 |
| `Multiply` | 乘法 |
| `Divide` | 除法 |
| `Add` / `Subtract` / `Multiply` / `Divide` | 四则运算 |
| `Abs` | 绝对值 |
| `Clamp` | 限制范围 |
| `Lerp` | 线性插值 |
| `Min` / `Max` | 最小/最大值 |
| `Sin` / `Cos` | 三角函数 |
| `Sqrt` | 平方根 |
| `Power` | 幂运算 |
## 逻辑节点
## 调试节点 (Debug)
| 节点 | 说明 |
|------|------|
| `And` | 逻辑与 |
| `Or` | 逻辑或 |
| `Not` | 逻辑非 |
| `Equal` | 相等比较 |
| `NotEqual` | 不等比较 |
| `Greater` | 大于比较 |
| `Less` | 小于比较 |
| `Print` | 输出到控制台 |
## 向量节点
## 自动生成的组件节点
| 节点 | 说明 |
|------|------|
| `MakeVector2` | 创建 2D 向量 |
| `BreakVector2` | 分解 2D 向量 |
| `VectorAdd` | 向量加法 |
| `VectorSubtract` | 向量减法 |
| `VectorMultiply` | 向量乘法 |
| `VectorLength` | 向量长度 |
| `VectorNormalize` | 向量归一化 |
| `VectorDistance` | 向量距离 |
使用 `@BlueprintExpose` 装饰器标记的组件会自动生成节点:
## 实体节点
```typescript
@ECSComponent('Transform')
@BlueprintExpose({ displayName: '变换', category: 'core' })
export class TransformComponent extends Component {
@BlueprintProperty({ displayName: 'X 坐标' })
x: number = 0;
| 节点 | 说明 |
|------|------|
| `GetSelf` | 获取当前实体 |
| `GetComponent` | 获取组件 |
| `HasComponent` | 检查组件 |
| `AddComponent` | 添加组件 |
| `RemoveComponent` | 移除组件 |
| `SpawnEntity` | 创建实体 |
| `DestroyEntity` | 销毁实体 |
@BlueprintProperty({ displayName: 'Y 坐标' })
y: number = 0;
## 变量节点
@BlueprintMethod({ displayName: '移动' })
translate(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
}
```
| 节点 | 说明 |
|------|------|
| `GetVariable` | 获取变量值 |
| `SetVariable` | 设置变量值 |
## 调试节点
| 节点 | 说明 |
|------|------|
| `Print` | 打印到控制台 |
| `DrawDebugLine` | 绘制调试线 |
| `DrawDebugPoint` | 绘制调试点 |
| `Breakpoint` | 调试断点 |
生成的节点:
- **Get Transform** - 获取 Transform 组件
- **Get X 坐标** / **Set X 坐标** - 访问 x 属性
- **Get Y 坐标** / **Set Y 坐标** - 访问 y 属性
- **移动** - 调用 translate 方法

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

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

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

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

View File

@@ -0,0 +1,37 @@
# @esengine/node-editor
## 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.2.2",
"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

@@ -1,4 +1,4 @@
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';
@@ -127,6 +127,25 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null);
const [hoveredPin, setHoveredPin] = useState<Pin | null>(null);
// 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]);
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]);
/**
* Converts screen coordinates to canvas coordinates
*
@@ -146,21 +165,51 @@ 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 +221,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]);

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

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

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

View File

@@ -1,5 +1,53 @@
# @esengine/blueprint
## 4.3.0
### Minor Changes
- [#435](https://github.com/esengine/esengine/pull/435) [`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e) Thanks [@esengine](https://github.com/esengine)! - feat(blueprint): 添加 Add Component 节点支持 + 变量节点 + ECS 模式重构
新功能:
- 为每个 @BlueprintExpose 组件自动生成 Add_ComponentName 节点
- Add 节点支持设置初始属性值
- 添加通用 ECS_AddComponent 节点用于动态添加组件
- @BlueprintExpose 装饰的组件自动注册,无需手动调用 registerComponentClass()
- 添加变量节点GetVariable, SetVariable, GetBoolVariable, GetFloatVariable, GetIntVariable, GetStringVariable
重构:
- BlueprintComponent 使用 @ECSComponent 装饰器注册
- BlueprintSystem 继承标准 System 基类
- 简化组件 API优化 VM 生命周期管理
- ExecutionContext.getComponentClass() 自动查找 @BlueprintExpose 注册的组件
## 4.2.0
### Minor Changes
- [#433](https://github.com/esengine/esengine/pull/433) [`2e84942`](https://github.com/esengine/esengine/commit/2e84942ea14c5326620398add05840fa8bea16f8) Thanks [@esengine](https://github.com/esengine)! - feat(blueprint): 添加 Add Component 节点支持 + ECS 模式重构
新功能:
- 为每个 @BlueprintExpose 组件自动生成 Add_ComponentName 节点
- Add 节点支持设置初始属性值
- 添加通用 ECS_AddComponent 节点用于动态添加组件
- 添加 registerComponentClass() 用于手动注册组件类
重构:
- BlueprintComponent 使用 @ECSComponent 装饰器注册
- BlueprintSystem 继承标准 System 基类
- 简化组件 API优化 VM 生命周期管理
## 4.1.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(blueprint): 重构装饰器系统,移除 Reflect 依赖 | Refactor decorator system, remove Reflect dependency
**@esengine/blueprint**
- 移除 `Reflect.getMetadata` 依赖,装饰器现在要求显式指定类型 | Removed `Reflect.getMetadata` dependency, decorators now require explicit type specification
- 简化 `BlueprintProperty``BlueprintMethod` 装饰器的元数据结构 | Simplified metadata structure for `BlueprintProperty` and `BlueprintMethod` decorators
- 新增 `inferPinType` 工具函数用于类型推断 | Added `inferPinType` utility function for type inference
- 优化组件节点生成器以适配新的元数据结构 | Optimized component node generator for new metadata structure
## 4.0.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/blueprint",
"version": "4.0.1",
"version": "4.3.0",
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -1,67 +0,0 @@
/**
* @zh ESEngine 蓝图插件
* @en ESEngine Blueprint Plugin
*
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
*
* @en This file contains code for integrating with ESEngine engine-core.
* Not needed when using other engines like Cocos/Laya.
*/
import type { IRuntimePlugin, ModuleManifest, IRuntimeModule } from '@esengine/engine-core';
/**
* @zh 蓝图运行时模块
* @en Blueprint Runtime Module
*
* @zh 注意:蓝图使用自定义系统 (IBlueprintSystem) 而非 EntitySystem
* 因此这里不实现 createSystems。蓝图系统应使用 createBlueprintSystem(scene) 手动创建。
*
* @en Note: Blueprint uses a custom system (IBlueprintSystem) instead of EntitySystem,
* so createSystems is not implemented here. Blueprint systems should be created
* manually using createBlueprintSystem(scene).
*/
class BlueprintRuntimeModule implements IRuntimeModule {
async onInitialize(): Promise<void> {
// Blueprint system initialization
}
onDestroy(): void {
// Cleanup
}
}
/**
* @zh 蓝图的插件清单
* @en Plugin manifest for Blueprint
*/
const manifest: ModuleManifest = {
id: 'blueprint',
name: '@esengine/blueprint',
displayName: 'Blueprint',
version: '1.0.0',
description: '可视化脚本系统',
category: 'AI',
icon: 'Workflow',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
dependencies: ['core'],
exports: {
components: ['BlueprintComponent'],
systems: ['BlueprintSystem']
},
requiresWasm: false
};
/**
* @zh 蓝图插件
* @en Blueprint Plugin
*/
export const BlueprintPlugin: IRuntimePlugin = {
manifest,
runtimeModule: new BlueprintRuntimeModule()
};
export { BlueprintRuntimeModule };

View File

@@ -1,37 +0,0 @@
/**
* @zh ESEngine 集成入口
* @en ESEngine integration entry point
*
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
*
* @en This module contains all code required for ESEngine engine-core integration.
* When using other engines like Cocos/Laya, just import the main module.
*
* @example ESEngine 使用方式 / ESEngine usage:
* ```typescript
* import { BlueprintPlugin } from '@esengine/blueprint/esengine';
*
* // Register with ESEngine plugin system
* engine.registerPlugin(BlueprintPlugin);
* ```
*
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
* ```typescript
* import {
* createBlueprintSystem,
* createBlueprintComponentData
* } from '@esengine/blueprint';
*
* // Create blueprint system for your scene
* const blueprintSystem = createBlueprintSystem(scene);
*
* // Add to your game loop
* function update(dt) {
* blueprintSystem.process(blueprintEntities, dt);
* }
* ```
*/
// Runtime module and plugin
export { BlueprintPlugin, BlueprintRuntimeModule } from './BlueprintPlugin';

View File

@@ -1,32 +1,51 @@
/**
* @esengine/blueprint - Visual scripting system for ECS Framework
*
* @zh 蓝图可视化脚本系统 - 可与任何 ECS 框架配合使用
* @en Visual scripting system - works with any ECS framework
* @zh 蓝图可视化脚本系统 - ECS 框架深度集成
* @en Visual scripting system - Deep integration with ECS framework
*
* @zh 此包是通用的可视化脚本实现,可以与任何 ECS 框架配合使用。
* 对于 ESEngine 集成,请从 '@esengine/blueprint/esengine' 导入插件。
* @zh 此包提供完整的可视化脚本功能:
* - 内置 ECS 操作节点Entity、Component、Flow
* - 组件自动节点生成(使用装饰器标记)
* - 运行时蓝图执行
*
* @en This package is a generic visual scripting implementation that works with any ECS framework.
* For ESEngine integration, import the plugin from '@esengine/blueprint/esengine'.
* @en This package provides complete visual scripting features:
* - Built-in ECS operation nodes (Entity, Component, Flow)
* - Auto component node generation (using decorators)
* - Runtime blueprint execution
*
* @example Cocos/Laya/通用 ECS 使用方式:
* @example 基础使用 | Basic Usage:
* ```typescript
* import {
* createBlueprintSystem,
* createBlueprintComponentData
* } from '@esengine/blueprint';
* import { BlueprintSystem, BlueprintComponent } from '@esengine/blueprint';
* import { Scene, Core } from '@esengine/ecs-framework';
*
* // Create blueprint system for your scene
* const blueprintSystem = createBlueprintSystem(scene);
* // 创建场景并添加蓝图系统
* const scene = new Scene();
* scene.addSystem(new BlueprintSystem());
* Core.setScene(scene);
*
* // Create component data
* const componentData = createBlueprintComponentData();
* componentData.blueprintAsset = loadedAsset;
* // 为实体添加蓝图
* const entity = scene.createEntity('Player');
* const blueprint = new BlueprintComponent();
* blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
* entity.addComponent(blueprint);
* ```
*
* // Add to your game loop
* function update(dt) {
* blueprintSystem.process(blueprintEntities, dt);
* @example 标记组件 | Mark Components:
* ```typescript
* import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
* import { Component, ECSComponent } from '@esengine/ecs-framework';
*
* @ECSComponent('Health')
* @BlueprintExpose({ displayName: '生命值' })
* export class HealthComponent extends Component {
* @BlueprintProperty({ displayName: '当前生命值' })
* current: number = 100;
*
* @BlueprintMethod({ displayName: '治疗' })
* heal(amount: number): void {
* this.current += amount;
* }
* }
* ```
*
@@ -45,23 +64,45 @@ export * from './triggers';
// Composition
export * from './composition';
// Nodes (import to register)
// Registry (decorators & auto-generation)
export * from './registry';
// Nodes (import to register built-in nodes)
import './nodes';
// Re-export commonly used items
export { NodeRegistry, RegisterNode } from './runtime/NodeRegistry';
export { BlueprintVM } from './runtime/BlueprintVM';
export {
createBlueprintComponentData,
initializeBlueprintVM,
startBlueprint,
stopBlueprint,
tickBlueprint,
cleanupBlueprint
} from './runtime/BlueprintComponent';
export {
createBlueprintSystem,
triggerBlueprintEvent,
triggerCustomBlueprintEvent
} from './runtime/BlueprintSystem';
export { BlueprintComponent } from './runtime/BlueprintComponent';
export { BlueprintSystem } from './runtime/BlueprintSystem';
export { ExecutionContext } from './runtime/ExecutionContext';
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';
// Component registration helper
import { ExecutionContext } from './runtime/ExecutionContext';
import type { Component } from '@esengine/ecs-framework';
/**
* @zh 注册组件类以支持在蓝图中动态创建
* @en Register a component class for dynamic creation in blueprints
*
* @example
* ```typescript
* import { registerComponentClass } from '@esengine/blueprint';
* import { MyComponent } from './MyComponent';
*
* registerComponentClass('MyComponent', MyComponent);
* ```
*/
export function registerComponentClass(typeName: string, componentClass: new () => Component): void {
ExecutionContext.registerComponentClass(typeName, componentClass);
}
// Re-export registry for convenience
export {
BlueprintExpose,
BlueprintProperty,
BlueprintMethod,
registerAllComponentNodes,
registerComponentNodes
} from './registry';

View File

@@ -0,0 +1,416 @@
/**
* @zh ECS 组件操作节点
* @en ECS Component Operation Nodes
*
* @zh 提供蓝图中对 ECS 组件的完整操作支持
* @en Provides complete ECS component operations in blueprint
*/
import type { Entity, Component } from '@esengine/ecs-framework';
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Add Component (Generic) | 添加组件(通用)
// ============================================================================
export const AddComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_AddComponent',
title: 'Add Component',
category: 'component',
color: '#1e8b8b',
description: 'Adds a component to an entity by type name (按类型名称为实体添加组件)',
keywords: ['component', 'add', 'create', 'attach'],
menuPath: ['ECS', 'Component', 'Add Component'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'success', type: 'bool', displayName: 'Success' }
]
};
@RegisterNode(AddComponentTemplate)
export class AddComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { component: null, success: false }, nextExec: 'exec' };
}
// Check if component already exists
const existing = entity.components.find(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
if (existing) {
return { outputs: { component: existing, success: false }, nextExec: 'exec' };
}
// Try to create component from registry
const ComponentClass = context.getComponentClass?.(componentType);
if (!ComponentClass) {
console.warn(`[Blueprint] Component type not found: ${componentType}`);
return { outputs: { component: null, success: false }, nextExec: 'exec' };
}
try {
const component = new ComponentClass();
entity.addComponent(component);
return { outputs: { component, success: true }, nextExec: 'exec' };
} catch (error) {
console.error(`[Blueprint] Failed to add component ${componentType}:`, error);
return { outputs: { component: null, success: false }, nextExec: 'exec' };
}
}
}
// ============================================================================
// Has Component | 是否有组件
// ============================================================================
export const HasComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_HasComponent',
title: 'Has Component',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Checks if an entity has a component of the specified type (检查实体是否拥有指定类型的组件)',
keywords: ['component', 'has', 'check', 'exists', 'contains'],
menuPath: ['ECS', 'Component', 'Has Component'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'hasComponent', type: 'bool', displayName: 'Has Component' }
]
};
@RegisterNode(HasComponentTemplate)
export class HasComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { hasComponent: false } };
}
const hasIt = entity.components.some(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
return { outputs: { hasComponent: hasIt } };
}
}
// ============================================================================
// Get Component | 获取组件
// ============================================================================
export const GetComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetComponent',
title: 'Get Component',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets a component from an entity by type name (按类型名称从实体获取组件)',
keywords: ['component', 'get', 'find', 'access'],
menuPath: ['ECS', 'Component', 'Get Component'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(GetComponentTemplate)
export class GetComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { component: null, found: false } };
}
const component = entity.components.find(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
return {
outputs: {
component: component ?? null,
found: component != null
}
};
}
}
// ============================================================================
// Get All Components | 获取所有组件
// ============================================================================
export const GetAllComponentsTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetAllComponents',
title: 'Get All Components',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets all components from an entity (获取实体的所有组件)',
keywords: ['component', 'get', 'all', 'list'],
menuPath: ['ECS', 'Component', 'Get All Components'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'components', type: 'array', displayName: 'Components', arrayType: 'component' },
{ name: 'count', type: 'int', displayName: 'Count' }
]
};
@RegisterNode(GetAllComponentsTemplate)
export class GetAllComponentsExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
if (!entity || entity.isDestroyed) {
return { outputs: { components: [], count: 0 } };
}
const components = [...entity.components];
return {
outputs: {
components,
count: components.length
}
};
}
}
// ============================================================================
// Remove Component | 移除组件
// ============================================================================
export const RemoveComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_RemoveComponent',
title: 'Remove Component',
category: 'component',
color: '#8b1e1e',
description: 'Removes a component from an entity (从实体移除组件)',
keywords: ['component', 'remove', 'delete', 'destroy'],
menuPath: ['ECS', 'Component', 'Remove Component'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'removed', type: 'bool', displayName: 'Removed' }
]
};
@RegisterNode(RemoveComponentTemplate)
export class RemoveComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { removed: false }, nextExec: 'exec' };
}
const component = entity.components.find(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
if (component) {
entity.removeComponent(component);
return { outputs: { removed: true }, nextExec: 'exec' };
}
return { outputs: { removed: false }, nextExec: 'exec' };
}
}
// ============================================================================
// Get Component Property | 获取组件属性
// ============================================================================
export const GetComponentPropertyTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetComponentProperty',
title: 'Get Component Property',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets a property value from a component (从组件获取属性值)',
keywords: ['component', 'property', 'get', 'value', 'field'],
menuPath: ['ECS', 'Component', 'Get Property'],
inputs: [
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'propertyName', type: 'string', displayName: 'Property Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'any', displayName: 'Value' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(GetComponentPropertyTemplate)
export class GetComponentPropertyExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
const propertyName = context.evaluateInput(node.id, 'propertyName', '') as string;
if (!component || !propertyName) {
return { outputs: { value: null, found: false } };
}
if (propertyName in component) {
return {
outputs: {
value: (component as any)[propertyName],
found: true
}
};
}
return { outputs: { value: null, found: false } };
}
}
// ============================================================================
// Set Component Property | 设置组件属性
// ============================================================================
export const SetComponentPropertyTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetComponentProperty',
title: 'Set Component Property',
category: 'component',
color: '#1e8b8b',
description: 'Sets a property value on a component (设置组件的属性值)',
keywords: ['component', 'property', 'set', 'value', 'field', 'modify'],
menuPath: ['ECS', 'Component', 'Set Property'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'propertyName', type: 'string', displayName: 'Property Name', defaultValue: '' },
{ name: 'value', type: 'any', displayName: 'Value' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'success', type: 'bool', displayName: 'Success' }
]
};
@RegisterNode(SetComponentPropertyTemplate)
export class SetComponentPropertyExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
const propertyName = context.evaluateInput(node.id, 'propertyName', '') as string;
const value = context.evaluateInput(node.id, 'value', null);
if (!component || !propertyName) {
return { outputs: { success: false }, nextExec: 'exec' };
}
if (propertyName in component) {
(component as any)[propertyName] = value;
return { outputs: { success: true }, nextExec: 'exec' };
}
return { outputs: { success: false }, nextExec: 'exec' };
}
}
// ============================================================================
// Get Component Type Name | 获取组件类型名称
// ============================================================================
export const GetComponentTypeNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetComponentTypeName',
title: 'Get Component Type',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets the type name of a component (获取组件的类型名称)',
keywords: ['component', 'type', 'name', 'class'],
menuPath: ['ECS', 'Component', 'Get Type Name'],
inputs: [
{ name: 'component', type: 'component', displayName: 'Component' }
],
outputs: [
{ name: 'typeName', type: 'string', displayName: 'Type Name' }
]
};
@RegisterNode(GetComponentTypeNameTemplate)
export class GetComponentTypeNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
if (!component) {
return { outputs: { typeName: '' } };
}
const typeName = (component.constructor as any).__componentName__ ?? component.constructor.name;
return { outputs: { typeName } };
}
}
// ============================================================================
// Get Entity From Component | 从组件获取实体
// ============================================================================
export const GetEntityFromComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityFromComponent',
title: 'Get Owner Entity',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets the entity that owns a component (获取拥有组件的实体)',
keywords: ['component', 'entity', 'owner', 'parent'],
menuPath: ['ECS', 'Component', 'Get Owner Entity'],
inputs: [
{ name: 'component', type: 'component', displayName: 'Component' }
],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(GetEntityFromComponentTemplate)
export class GetEntityFromComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
if (!component || component.entityId == null) {
return { outputs: { entity: null, found: false } };
}
const entity = context.scene.findEntityById(component.entityId);
return {
outputs: {
entity: entity ?? null,
found: entity != null
}
};
}
}

View File

@@ -0,0 +1,485 @@
/**
* @zh ECS 实体操作节点
* @en ECS Entity Operation Nodes
*
* @zh 提供蓝图中对 ECS 实体的完整操作支持
* @en Provides complete ECS entity operations in blueprint
*/
import type { Entity } from '@esengine/ecs-framework';
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Self Entity | 自身实体
// ============================================================================
export const GetSelfTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetSelf',
title: 'Get Self',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the entity that owns this blueprint (获取拥有此蓝图的实体)',
keywords: ['self', 'this', 'owner', 'entity', 'me'],
menuPath: ['ECS', 'Entity', 'Get Self'],
inputs: [],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Self' }
]
};
@RegisterNode(GetSelfTemplate)
export class GetSelfExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return { outputs: { entity: context.entity } };
}
}
// ============================================================================
// Create Entity | 创建实体
// ============================================================================
export const CreateEntityTemplate: BlueprintNodeTemplate = {
type: 'ECS_CreateEntity',
title: 'Create Entity',
category: 'entity',
color: '#1e5a8b',
description: 'Creates a new entity in the scene (在场景中创建新实体)',
keywords: ['entity', 'create', 'spawn', 'new', 'instantiate'],
menuPath: ['ECS', 'Entity', 'Create Entity'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'name', type: 'string', displayName: 'Name', defaultValue: 'NewEntity' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' }
]
};
@RegisterNode(CreateEntityTemplate)
export class CreateEntityExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const name = context.evaluateInput(node.id, 'name', 'NewEntity') as string;
const entity = context.scene.createEntity(name);
return { outputs: { entity }, nextExec: 'exec' };
}
}
// ============================================================================
// Destroy Entity | 销毁实体
// ============================================================================
export const DestroyEntityTemplate: BlueprintNodeTemplate = {
type: 'ECS_DestroyEntity',
title: 'Destroy Entity',
category: 'entity',
color: '#8b1e1e',
description: 'Destroys an entity from the scene (从场景中销毁实体)',
keywords: ['entity', 'destroy', 'remove', 'delete', 'kill'],
menuPath: ['ECS', 'Entity', 'Destroy Entity'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(DestroyEntityTemplate)
export class DestroyEntityExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', null) as Entity | null;
if (entity && !entity.isDestroyed) {
entity.destroy();
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Destroy Self | 销毁自身
// ============================================================================
export const DestroySelfTemplate: BlueprintNodeTemplate = {
type: 'ECS_DestroySelf',
title: 'Destroy Self',
category: 'entity',
color: '#8b1e1e',
description: 'Destroys the entity that owns this blueprint (销毁拥有此蓝图的实体)',
keywords: ['self', 'destroy', 'suicide', 'remove', 'delete'],
menuPath: ['ECS', 'Entity', 'Destroy Self'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' }
],
outputs: []
};
@RegisterNode(DestroySelfTemplate)
export class DestroySelfExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
if (!context.entity.isDestroyed) {
context.entity.destroy();
}
return { nextExec: null };
}
}
// ============================================================================
// Is Valid | 是否有效
// ============================================================================
export const IsValidTemplate: BlueprintNodeTemplate = {
type: 'ECS_IsValid',
title: 'Is Valid',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Checks if an entity reference is valid and not destroyed (检查实体引用是否有效且未被销毁)',
keywords: ['entity', 'valid', 'null', 'check', 'exists', 'alive'],
menuPath: ['ECS', 'Entity', 'Is Valid'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'isValid', type: 'bool', displayName: 'Is Valid' }
]
};
@RegisterNode(IsValidTemplate)
export class IsValidExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', null) as Entity | null;
const isValid = entity != null && !entity.isDestroyed;
return { outputs: { isValid } };
}
}
// ============================================================================
// Get Entity Name | 获取实体名称
// ============================================================================
export const GetEntityNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityName',
title: 'Get Entity Name',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the name of an entity (获取实体的名称)',
keywords: ['entity', 'name', 'get', 'string'],
menuPath: ['ECS', 'Entity', 'Get Name'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'name', type: 'string', displayName: 'Name' }
]
};
@RegisterNode(GetEntityNameTemplate)
export class GetEntityNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { name: entity?.name ?? '' } };
}
}
// ============================================================================
// Set Entity Name | 设置实体名称
// ============================================================================
export const SetEntityNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetEntityName',
title: 'Set Entity Name',
category: 'entity',
color: '#1e5a8b',
description: 'Sets the name of an entity (设置实体的名称)',
keywords: ['entity', 'name', 'set', 'rename'],
menuPath: ['ECS', 'Entity', 'Set Name'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'name', type: 'string', displayName: 'Name', defaultValue: '' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(SetEntityNameTemplate)
export class SetEntityNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const name = context.evaluateInput(node.id, 'name', '') as string;
if (entity && !entity.isDestroyed) {
entity.name = name;
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Get Entity Tag | 获取实体标签
// ============================================================================
export const GetEntityTagTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityTag',
title: 'Get Entity Tag',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the tag of an entity (获取实体的标签)',
keywords: ['entity', 'tag', 'get', 'category'],
menuPath: ['ECS', 'Entity', 'Get Tag'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'tag', type: 'int', displayName: 'Tag' }
]
};
@RegisterNode(GetEntityTagTemplate)
export class GetEntityTagExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { tag: entity?.tag ?? 0 } };
}
}
// ============================================================================
// Set Entity Tag | 设置实体标签
// ============================================================================
export const SetEntityTagTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetEntityTag',
title: 'Set Entity Tag',
category: 'entity',
color: '#1e5a8b',
description: 'Sets the tag of an entity (设置实体的标签)',
keywords: ['entity', 'tag', 'set', 'category'],
menuPath: ['ECS', 'Entity', 'Set Tag'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'tag', type: 'int', displayName: 'Tag', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(SetEntityTagTemplate)
export class SetEntityTagExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const tag = context.evaluateInput(node.id, 'tag', 0) as number;
if (entity && !entity.isDestroyed) {
entity.tag = tag;
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Set Entity Active | 设置实体激活状态
// ============================================================================
export const SetEntityActiveTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetEntityActive',
title: 'Set Active',
category: 'entity',
color: '#1e5a8b',
description: 'Sets whether an entity is active (设置实体是否激活)',
keywords: ['entity', 'active', 'enable', 'disable', 'visible'],
menuPath: ['ECS', 'Entity', 'Set Active'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'active', type: 'bool', displayName: 'Active', defaultValue: true }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(SetEntityActiveTemplate)
export class SetEntityActiveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const active = context.evaluateInput(node.id, 'active', true) as boolean;
if (entity && !entity.isDestroyed) {
entity.active = active;
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Is Entity Active | 实体是否激活
// ============================================================================
export const IsEntityActiveTemplate: BlueprintNodeTemplate = {
type: 'ECS_IsEntityActive',
title: 'Is Active',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Checks if an entity is active (检查实体是否激活)',
keywords: ['entity', 'active', 'enabled', 'check'],
menuPath: ['ECS', 'Entity', 'Is Active'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'isActive', type: 'bool', displayName: 'Is Active' }
]
};
@RegisterNode(IsEntityActiveTemplate)
export class IsEntityActiveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { isActive: entity?.active ?? false } };
}
}
// ============================================================================
// Find Entity By Name | 按名称查找实体
// ============================================================================
export const FindEntityByNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_FindEntityByName',
title: 'Find Entity By Name',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Finds an entity by name in the scene (在场景中按名称查找实体)',
keywords: ['entity', 'find', 'name', 'search', 'get', 'lookup'],
menuPath: ['ECS', 'Entity', 'Find By Name'],
inputs: [
{ name: 'name', type: 'string', displayName: 'Name', defaultValue: '' }
],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(FindEntityByNameTemplate)
export class FindEntityByNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const name = context.evaluateInput(node.id, 'name', '') as string;
const entity = context.scene.findEntity(name);
return {
outputs: {
entity: entity ?? null,
found: entity != null
}
};
}
}
// ============================================================================
// Find Entities By Tag | 按标签查找实体
// ============================================================================
export const FindEntitiesByTagTemplate: BlueprintNodeTemplate = {
type: 'ECS_FindEntitiesByTag',
title: 'Find Entities By Tag',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Finds all entities with a specific tag (查找所有具有特定标签的实体)',
keywords: ['entity', 'find', 'tag', 'search', 'get', 'all'],
menuPath: ['ECS', 'Entity', 'Find By Tag'],
inputs: [
{ name: 'tag', type: 'int', displayName: 'Tag', defaultValue: 0 }
],
outputs: [
{ name: 'entities', type: 'array', displayName: 'Entities', arrayType: 'entity' },
{ name: 'count', type: 'int', displayName: 'Count' }
]
};
@RegisterNode(FindEntitiesByTagTemplate)
export class FindEntitiesByTagExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const tag = context.evaluateInput(node.id, 'tag', 0) as number;
const entities = context.scene.findEntitiesByTag(tag);
return {
outputs: {
entities,
count: entities.length
}
};
}
}
// ============================================================================
// Get Entity ID | 获取实体 ID
// ============================================================================
export const GetEntityIdTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityId',
title: 'Get Entity ID',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the unique ID of an entity (获取实体的唯一ID)',
keywords: ['entity', 'id', 'identifier', 'unique'],
menuPath: ['ECS', 'Entity', 'Get ID'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'id', type: 'int', displayName: 'ID' }
]
};
@RegisterNode(GetEntityIdTemplate)
export class GetEntityIdExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { id: entity?.id ?? -1 } };
}
}
// ============================================================================
// Find Entity By ID | 按 ID 查找实体
// ============================================================================
export const FindEntityByIdTemplate: BlueprintNodeTemplate = {
type: 'ECS_FindEntityById',
title: 'Find Entity By ID',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Finds an entity by its unique ID (通过唯一ID查找实体)',
keywords: ['entity', 'find', 'id', 'identifier'],
menuPath: ['ECS', 'Entity', 'Find By ID'],
inputs: [
{ name: 'id', type: 'int', displayName: 'ID', defaultValue: 0 }
],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(FindEntityByIdTemplate)
export class FindEntityByIdExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const id = context.evaluateInput(node.id, 'id', 0) as number;
const entity = context.scene.findEntityById(id);
return {
outputs: {
entity: entity ?? null,
found: entity != null
}
};
}
}

View File

@@ -0,0 +1,301 @@
/**
* @zh 流程控制节点
* @en Flow Control Nodes
*
* @zh 提供蓝图中的流程控制支持(分支、循环等)
* @en Provides flow control in blueprint (branch, loop, etc.)
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Branch | 分支
// ============================================================================
export const BranchTemplate: BlueprintNodeTemplate = {
type: 'Flow_Branch',
title: 'Branch',
category: 'flow',
color: '#4a4a4a',
description: 'Executes one of two paths based on a condition (根据条件执行两条路径之一)',
keywords: ['if', 'branch', 'condition', 'switch', 'else'],
menuPath: ['Flow', 'Branch'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'condition', type: 'bool', displayName: 'Condition', defaultValue: false }
],
outputs: [
{ name: 'true', type: 'exec', displayName: 'True' },
{ name: 'false', type: 'exec', displayName: 'False' }
]
};
@RegisterNode(BranchTemplate)
export class BranchExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const condition = context.evaluateInput(node.id, 'condition', false) as boolean;
return { nextExec: condition ? 'true' : 'false' };
}
}
// ============================================================================
// Sequence | 序列
// ============================================================================
export const SequenceTemplate: BlueprintNodeTemplate = {
type: 'Flow_Sequence',
title: 'Sequence',
category: 'flow',
color: '#4a4a4a',
description: 'Executes multiple outputs in order (按顺序执行多个输出)',
keywords: ['sequence', 'order', 'serial', 'chain'],
menuPath: ['Flow', 'Sequence'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' }
],
outputs: [
{ name: 'then0', type: 'exec', displayName: 'Then 0' },
{ name: 'then1', type: 'exec', displayName: 'Then 1' },
{ name: 'then2', type: 'exec', displayName: 'Then 2' },
{ name: 'then3', type: 'exec', displayName: 'Then 3' }
]
};
@RegisterNode(SequenceTemplate)
export class SequenceExecutor implements INodeExecutor {
private currentIndex = 0;
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
const outputs = ['then0', 'then1', 'then2', 'then3'];
const nextPin = outputs[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % outputs.length;
if (this.currentIndex === 0) {
return { nextExec: null };
}
return { nextExec: nextPin };
}
}
// ============================================================================
// Do Once | 只执行一次
// ============================================================================
export const DoOnceTemplate: BlueprintNodeTemplate = {
type: 'Flow_DoOnce',
title: 'Do Once',
category: 'flow',
color: '#4a4a4a',
description: 'Executes the output only once, subsequent calls are ignored (只执行一次,后续调用被忽略)',
keywords: ['once', 'single', 'first', 'one'],
menuPath: ['Flow', 'Do Once'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'reset', type: 'exec', displayName: 'Reset' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(DoOnceTemplate)
export class DoOnceExecutor implements INodeExecutor {
private executed = false;
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
const inputPin = node.data._lastInputPin as string | undefined;
if (inputPin === 'reset') {
this.executed = false;
return { nextExec: null };
}
if (this.executed) {
return { nextExec: null };
}
this.executed = true;
return { nextExec: 'exec' };
}
}
// ============================================================================
// Flip Flop | 触发器
// ============================================================================
export const FlipFlopTemplate: BlueprintNodeTemplate = {
type: 'Flow_FlipFlop',
title: 'Flip Flop',
category: 'flow',
color: '#4a4a4a',
description: 'Alternates between two outputs on each execution (每次执行时在两个输出之间交替)',
keywords: ['flip', 'flop', 'toggle', 'alternate', 'switch'],
menuPath: ['Flow', 'Flip Flop'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' }
],
outputs: [
{ name: 'a', type: 'exec', displayName: 'A' },
{ name: 'b', type: 'exec', displayName: 'B' },
{ name: 'isA', type: 'bool', displayName: 'Is A' }
]
};
@RegisterNode(FlipFlopTemplate)
export class FlipFlopExecutor implements INodeExecutor {
private isA = true;
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
const currentIsA = this.isA;
this.isA = !this.isA;
return {
outputs: { isA: currentIsA },
nextExec: currentIsA ? 'a' : 'b'
};
}
}
// ============================================================================
// Gate | 门
// ============================================================================
export const GateTemplate: BlueprintNodeTemplate = {
type: 'Flow_Gate',
title: 'Gate',
category: 'flow',
color: '#4a4a4a',
description: 'Controls execution flow with open/close state (通过开/关状态控制执行流)',
keywords: ['gate', 'open', 'close', 'block', 'allow'],
menuPath: ['Flow', 'Gate'],
inputs: [
{ name: 'exec', type: 'exec', displayName: 'Enter' },
{ name: 'open', type: 'exec', displayName: 'Open' },
{ name: 'close', type: 'exec', displayName: 'Close' },
{ name: 'toggle', type: 'exec', displayName: 'Toggle' },
{ name: 'startOpen', type: 'bool', displayName: 'Start Open', defaultValue: true }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: 'Exit' }
]
};
@RegisterNode(GateTemplate)
export class GateExecutor implements INodeExecutor {
private isOpen: boolean | null = null;
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
if (this.isOpen === null) {
this.isOpen = context.evaluateInput(node.id, 'startOpen', true) as boolean;
}
const inputPin = node.data._lastInputPin as string | undefined;
switch (inputPin) {
case 'open':
this.isOpen = true;
return { nextExec: null };
case 'close':
this.isOpen = false;
return { nextExec: null };
case 'toggle':
this.isOpen = !this.isOpen;
return { nextExec: null };
default:
return { nextExec: this.isOpen ? 'exec' : null };
}
}
}
// ============================================================================
// For Loop | For 循环
// ============================================================================
export const ForLoopTemplate: BlueprintNodeTemplate = {
type: 'Flow_ForLoop',
title: 'For Loop',
category: 'flow',
color: '#4a4a4a',
description: 'Executes the loop body for each index in range (对范围内的每个索引执行循环体)',
keywords: ['for', 'loop', 'iterate', 'repeat', 'count'],
menuPath: ['Flow', 'For Loop'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'start', type: 'int', displayName: 'Start', defaultValue: 0 },
{ name: 'end', type: 'int', displayName: 'End', defaultValue: 10 }
],
outputs: [
{ name: 'loopBody', type: 'exec', displayName: 'Loop Body' },
{ name: 'completed', type: 'exec', displayName: 'Completed' },
{ name: 'index', type: 'int', displayName: 'Index' }
]
};
@RegisterNode(ForLoopTemplate)
export class ForLoopExecutor implements INodeExecutor {
private currentIndex = 0;
private endIndex = 0;
private isRunning = false;
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
if (!this.isRunning) {
this.currentIndex = context.evaluateInput(node.id, 'start', 0) as number;
this.endIndex = context.evaluateInput(node.id, 'end', 10) as number;
this.isRunning = true;
}
if (this.currentIndex < this.endIndex) {
const index = this.currentIndex;
this.currentIndex++;
return {
outputs: { index },
nextExec: 'loopBody'
};
}
this.isRunning = false;
return {
outputs: { index: this.endIndex },
nextExec: 'completed'
};
}
}
// ============================================================================
// While Loop | While 循环
// ============================================================================
export const WhileLoopTemplate: BlueprintNodeTemplate = {
type: 'Flow_WhileLoop',
title: 'While Loop',
category: 'flow',
color: '#4a4a4a',
description: 'Executes the loop body while condition is true (当条件为真时执行循环体)',
keywords: ['while', 'loop', 'repeat', 'condition'],
menuPath: ['Flow', 'While Loop'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'condition', type: 'bool', displayName: 'Condition', defaultValue: true }
],
outputs: [
{ name: 'loopBody', type: 'exec', displayName: 'Loop Body' },
{ name: 'completed', type: 'exec', displayName: 'Completed' }
]
};
@RegisterNode(WhileLoopTemplate)
export class WhileLoopExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const condition = context.evaluateInput(node.id, 'condition', true) as boolean;
if (condition) {
return { nextExec: 'loopBody' };
}
return { nextExec: 'completed' };
}
}

View File

@@ -0,0 +1,16 @@
/**
* @zh ECS 核心节点
* @en ECS Core Nodes
*
* @zh 提供与 ECS 框架交互的蓝图节点
* @en Provides blueprint nodes for ECS framework interaction
*/
// Entity operations | 实体操作
export * from './EntityNodes';
// Component operations | 组件操作
export * from './ComponentNodes';
// Flow control | 流程控制
export * from './FlowNodes';

View File

@@ -17,13 +17,19 @@ export const EventBeginPlayTemplate: BlueprintNodeTemplate = {
category: 'event',
color: '#CC0000',
description: 'Triggered once when the blueprint starts executing (蓝图开始执行时触发一次)',
keywords: ['start', 'begin', 'init', 'event'],
keywords: ['start', 'begin', 'init', 'event', 'self'],
menuPath: ['Events', 'Begin Play'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'self',
type: 'entity',
displayName: 'Self'
}
]
};
@@ -34,11 +40,12 @@ export const EventBeginPlayTemplate: BlueprintNodeTemplate = {
*/
@RegisterNode(EventBeginPlayTemplate)
export class EventBeginPlayExecutor implements INodeExecutor {
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
// Event nodes just trigger execution flow
// 事件节点只触发执行流
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec'
nextExec: 'exec',
outputs: {
self: context.entity
}
};
}
}

View File

@@ -1,118 +0,0 @@
/**
* @zh 碰撞事件节点 - 碰撞发生时触发
* @en Event Collision Node - Triggered on collision events
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventCollisionEnter 节点模板
* @en EventCollisionEnter node template
*/
export const EventCollisionEnterTemplate: BlueprintNodeTemplate = {
type: 'EventCollisionEnter',
title: 'Event Collision Enter',
category: 'event',
color: '#CC0000',
description: 'Triggered when collision starts / 碰撞开始时触发',
keywords: ['collision', 'enter', 'hit', 'overlap', 'event'],
menuPath: ['Event', 'Collision', 'Enter'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'otherEntityId',
type: 'string',
displayName: 'Other Entity'
},
{
name: 'pointX',
type: 'float',
displayName: 'Point X'
},
{
name: 'pointY',
type: 'float',
displayName: 'Point Y'
},
{
name: 'normalX',
type: 'float',
displayName: 'Normal X'
},
{
name: 'normalY',
type: 'float',
displayName: 'Normal Y'
}
]
};
/**
* @zh EventCollisionEnter 节点执行器
* @en EventCollisionEnter node executor
*/
@RegisterNode(EventCollisionEnterTemplate)
export class EventCollisionEnterExecutor implements INodeExecutor {
execute(_node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
otherEntityId: '',
pointX: 0,
pointY: 0,
normalX: 0,
normalY: 0
}
};
}
}
/**
* @zh EventCollisionExit 节点模板
* @en EventCollisionExit node template
*/
export const EventCollisionExitTemplate: BlueprintNodeTemplate = {
type: 'EventCollisionExit',
title: 'Event Collision Exit',
category: 'event',
color: '#CC0000',
description: 'Triggered when collision ends / 碰撞结束时触发',
keywords: ['collision', 'exit', 'end', 'separate', 'event'],
menuPath: ['Event', 'Collision', 'Exit'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'otherEntityId',
type: 'string',
displayName: 'Other Entity'
}
]
};
/**
* @zh EventCollisionExit 节点执行器
* @en EventCollisionExit node executor
*/
@RegisterNode(EventCollisionExitTemplate)
export class EventCollisionExitExecutor implements INodeExecutor {
execute(_node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
otherEntityId: ''
}
};
}
}

View File

@@ -17,13 +17,19 @@ export const EventEndPlayTemplate: BlueprintNodeTemplate = {
category: 'event',
color: '#CC0000',
description: 'Triggered once when the blueprint stops executing (蓝图停止执行时触发一次)',
keywords: ['stop', 'end', 'destroy', 'event'],
keywords: ['stop', 'end', 'destroy', 'event', 'self'],
menuPath: ['Events', 'End Play'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'self',
type: 'entity',
displayName: 'Self'
}
]
};
@@ -34,9 +40,12 @@ export const EventEndPlayTemplate: BlueprintNodeTemplate = {
*/
@RegisterNode(EventEndPlayTemplate)
export class EventEndPlayExecutor implements INodeExecutor {
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec'
nextExec: 'exec',
outputs: {
self: context.entity
}
};
}
}

View File

@@ -1,79 +0,0 @@
/**
* @zh 输入事件节点 - 输入触发时触发
* @en Event Input Node - Triggered on input events
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventInput 节点模板
* @en EventInput node template
*/
export const EventInputTemplate: BlueprintNodeTemplate = {
type: 'EventInput',
title: 'Event Input',
category: 'event',
color: '#CC0000',
description: 'Triggered when input action occurs / 输入动作发生时触发',
keywords: ['input', 'key', 'button', 'action', 'event'],
menuPath: ['Event', 'Input'],
inputs: [
{
name: 'action',
type: 'string',
displayName: 'Action',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'action',
type: 'string',
displayName: 'Action'
},
{
name: 'value',
type: 'float',
displayName: 'Value'
},
{
name: 'pressed',
type: 'bool',
displayName: 'Pressed'
},
{
name: 'released',
type: 'bool',
displayName: 'Released'
}
]
};
/**
* @zh EventInput 节点执行器
* @en EventInput node executor
*
* @zh 注意:事件节点的输出由 VM 在触发时通过 setOutputs 设置
* @en Note: Event node outputs are set by VM via setOutputs when triggered
*/
@RegisterNode(EventInputTemplate)
export class EventInputExecutor implements INodeExecutor {
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
action: node.data?.action ?? '',
value: 0,
pressed: false,
released: false
}
};
}
}

View File

@@ -1,70 +0,0 @@
/**
* @zh 消息事件节点 - 接收消息时触发
* @en Event Message Node - Triggered when message is received
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventMessage 节点模板
* @en EventMessage node template
*/
export const EventMessageTemplate: BlueprintNodeTemplate = {
type: 'EventMessage',
title: 'Event Message',
category: 'event',
color: '#CC0000',
description: 'Triggered when a message is received / 接收到消息时触发',
keywords: ['message', 'receive', 'broadcast', 'event', 'signal'],
menuPath: ['Event', 'Message'],
inputs: [
{
name: 'messageName',
type: 'string',
displayName: 'Message Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'messageName',
type: 'string',
displayName: 'Message'
},
{
name: 'senderId',
type: 'string',
displayName: 'Sender ID'
},
{
name: 'payload',
type: 'any',
displayName: 'Payload'
}
]
};
/**
* @zh EventMessage 节点执行器
* @en EventMessage node executor
*/
@RegisterNode(EventMessageTemplate)
export class EventMessageExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
messageName: node.data?.messageName ?? '',
senderId: '',
payload: null
}
};
}
}

View File

@@ -1,132 +0,0 @@
/**
* @zh 状态事件节点 - 状态机状态变化时触发
* @en Event State Node - Triggered on state machine state changes
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventStateEnter 节点模板
* @en EventStateEnter node template
*/
export const EventStateEnterTemplate: BlueprintNodeTemplate = {
type: 'EventStateEnter',
title: 'Event State Enter',
category: 'event',
color: '#CC0000',
description: 'Triggered when entering a state / 进入状态时触发',
keywords: ['state', 'enter', 'fsm', 'machine', 'event'],
menuPath: ['Event', 'State', 'Enter'],
inputs: [
{
name: 'stateName',
type: 'string',
displayName: 'State Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'stateMachineId',
type: 'string',
displayName: 'State Machine'
},
{
name: 'currentState',
type: 'string',
displayName: 'Current State'
},
{
name: 'previousState',
type: 'string',
displayName: 'Previous State'
}
]
};
/**
* @zh EventStateEnter 节点执行器
* @en EventStateEnter node executor
*/
@RegisterNode(EventStateEnterTemplate)
export class EventStateEnterExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
stateMachineId: '',
currentState: node.data?.stateName ?? '',
previousState: ''
}
};
}
}
/**
* @zh EventStateExit 节点模板
* @en EventStateExit node template
*/
export const EventStateExitTemplate: BlueprintNodeTemplate = {
type: 'EventStateExit',
title: 'Event State Exit',
category: 'event',
color: '#CC0000',
description: 'Triggered when exiting a state / 退出状态时触发',
keywords: ['state', 'exit', 'leave', 'fsm', 'machine', 'event'],
menuPath: ['Event', 'State', 'Exit'],
inputs: [
{
name: 'stateName',
type: 'string',
displayName: 'State Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'stateMachineId',
type: 'string',
displayName: 'State Machine'
},
{
name: 'currentState',
type: 'string',
displayName: 'Current State'
},
{
name: 'previousState',
type: 'string',
displayName: 'Previous State'
}
]
};
/**
* @zh EventStateExit 节点执行器
* @en EventStateExit node executor
*/
@RegisterNode(EventStateExitTemplate)
export class EventStateExitExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
stateMachineId: '',
currentState: '',
previousState: node.data?.stateName ?? ''
}
};
}
}

View File

@@ -17,7 +17,8 @@ export const EventTickTemplate: BlueprintNodeTemplate = {
category: 'event',
color: '#CC0000',
description: 'Triggered every frame during execution (执行期间每帧触发)',
keywords: ['update', 'frame', 'tick', 'event'],
keywords: ['update', 'frame', 'tick', 'event', 'self'],
menuPath: ['Events', 'Tick'],
inputs: [],
outputs: [
{
@@ -25,6 +26,11 @@ export const EventTickTemplate: BlueprintNodeTemplate = {
type: 'exec',
displayName: ''
},
{
name: 'self',
type: 'entity',
displayName: 'Self'
},
{
name: 'deltaTime',
type: 'float',
@@ -43,6 +49,7 @@ export class EventTickExecutor implements INodeExecutor {
return {
nextExec: 'exec',
outputs: {
self: context.entity,
deltaTime: context.deltaTime
}
};

View File

@@ -1,70 +0,0 @@
/**
* @zh 定时器事件节点 - 定时器触发时调用
* @en Event Timer Node - Triggered when timer fires
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventTimer 节点模板
* @en EventTimer node template
*/
export const EventTimerTemplate: BlueprintNodeTemplate = {
type: 'EventTimer',
title: 'Event Timer',
category: 'event',
color: '#CC0000',
description: 'Triggered when a timer fires / 定时器触发时执行',
keywords: ['timer', 'delay', 'schedule', 'event', 'interval'],
menuPath: ['Event', 'Timer'],
inputs: [
{
name: 'timerId',
type: 'string',
displayName: 'Timer ID',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'timerId',
type: 'string',
displayName: 'Timer ID'
},
{
name: 'isRepeating',
type: 'bool',
displayName: 'Is Repeating'
},
{
name: 'timesFired',
type: 'int',
displayName: 'Times Fired'
}
]
};
/**
* @zh EventTimer 节点执行器
* @en EventTimer node executor
*/
@RegisterNode(EventTimerTemplate)
export class EventTimerExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
timerId: node.data?.timerId ?? '',
isRepeating: false,
timesFired: 0
}
};
}
}

View File

@@ -1,16 +1,8 @@
/**
* @zh 事件节点 - 蓝图执行的入口点
* @en Event Nodes - Entry points for blueprint execution
* @zh 生命周期事件节点 - 蓝图执行的入口点
* @en Lifecycle Event Nodes - Entry points for blueprint execution
*/
// 生命周期事件 | Lifecycle events
export * from './EventBeginPlay';
export * from './EventTick';
export * from './EventEndPlay';
// 触发器事件 | Trigger events
export * from './EventInput';
export * from './EventCollision';
export * from './EventMessage';
export * from './EventTimer';
export * from './EventState';

View File

@@ -1,11 +1,38 @@
/**
* Blueprint Nodes - All node definitions and executors
* 蓝图节点 - 所有节点定义和执行器
* @zh 蓝图节点 - 所有节点定义和执行器
* @en Blueprint Nodes - All node definitions and executors
*
* @zh 节点分类:
* - events: 生命周期事件BeginPlay, Tick, EndPlay
* - ecs: ECS 操作Entity, Component, Flow
* - variables: 变量读写
* - math: 数学运算
* - time: 时间工具
* - debug: 调试工具
*
* @en Node categories:
* - events: Lifecycle events (BeginPlay, Tick, EndPlay)
* - ecs: ECS operations (Entity, Component, Flow)
* - variables: Variable get/set
* - math: Math operations
* - time: Time utilities
* - debug: Debug utilities
*/
// Import all nodes to trigger registration
// 导入所有节点以触发注册
// Lifecycle events | 生命周期事件
export * from './events';
export * from './debug';
export * from './time';
// ECS operations | ECS 操作
export * from './ecs';
// Variables | 变量
export * from './variables';
// Math operations | 数学运算
export * from './math';
// Time utilities | 时间工具
export * from './time';
// Debug utilities | 调试工具
export * from './debug';

View File

@@ -0,0 +1,189 @@
/**
* @zh 变量节点 - 读取和设置蓝图变量
* @en Variable Nodes - Get and set blueprint variables
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Get Variable | 获取变量
// ============================================================================
export const GetVariableTemplate: BlueprintNodeTemplate = {
type: 'GetVariable',
title: 'Get Variable',
category: 'variable',
color: '#4a9c6d',
isPure: true,
description: 'Gets the value of a variable (获取变量的值)',
keywords: ['variable', 'get', 'read', 'value'],
menuPath: ['Variable', 'Get Variable'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'any', displayName: 'Value' }
]
};
@RegisterNode(GetVariableTemplate)
export class GetVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
if (!variableName) {
return { outputs: { value: null } };
}
const value = context.getVariable(variableName);
return { outputs: { value } };
}
}
// ============================================================================
// Set Variable | 设置变量
// ============================================================================
export const SetVariableTemplate: BlueprintNodeTemplate = {
type: 'SetVariable',
title: 'Set Variable',
category: 'variable',
color: '#4a9c6d',
description: 'Sets the value of a variable (设置变量的值)',
keywords: ['variable', 'set', 'write', 'assign', 'value'],
menuPath: ['Variable', 'Set Variable'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' },
{ name: 'value', type: 'any', displayName: 'Value' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'value', type: 'any', displayName: 'Value' }
]
};
@RegisterNode(SetVariableTemplate)
export class SetVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.evaluateInput(node.id, 'value', null);
if (!variableName) {
return { outputs: { value: null }, nextExec: 'exec' };
}
context.setVariable(variableName, value);
return { outputs: { value }, nextExec: 'exec' };
}
}
// ============================================================================
// Get Variable By Name (typed variants) | 按名称获取变量(类型变体)
// ============================================================================
export const GetBoolVariableTemplate: BlueprintNodeTemplate = {
type: 'GetBoolVariable',
title: 'Get Bool',
category: 'variable',
color: '#8b1e3f',
isPure: true,
description: 'Gets a boolean variable (获取布尔变量)',
keywords: ['variable', 'get', 'bool', 'boolean'],
menuPath: ['Variable', 'Get Bool'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'bool', displayName: 'Value' }
]
};
@RegisterNode(GetBoolVariableTemplate)
export class GetBoolVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: Boolean(value) } };
}
}
export const GetFloatVariableTemplate: BlueprintNodeTemplate = {
type: 'GetFloatVariable',
title: 'Get Float',
category: 'variable',
color: '#39c5bb',
isPure: true,
description: 'Gets a float variable (获取浮点变量)',
keywords: ['variable', 'get', 'float', 'number'],
menuPath: ['Variable', 'Get Float'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'float', displayName: 'Value' }
]
};
@RegisterNode(GetFloatVariableTemplate)
export class GetFloatVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: Number(value) || 0 } };
}
}
export const GetIntVariableTemplate: BlueprintNodeTemplate = {
type: 'GetIntVariable',
title: 'Get Int',
category: 'variable',
color: '#1c8b8b',
isPure: true,
description: 'Gets an integer variable (获取整数变量)',
keywords: ['variable', 'get', 'int', 'integer', 'number'],
menuPath: ['Variable', 'Get Int'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'int', displayName: 'Value' }
]
};
@RegisterNode(GetIntVariableTemplate)
export class GetIntVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: Math.floor(Number(value) || 0) } };
}
}
export const GetStringVariableTemplate: BlueprintNodeTemplate = {
type: 'GetStringVariable',
title: 'Get String',
category: 'variable',
color: '#e91e8c',
isPure: true,
description: 'Gets a string variable (获取字符串变量)',
keywords: ['variable', 'get', 'string', 'text'],
menuPath: ['Variable', 'Get String'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'string', displayName: 'Value' }
]
};
@RegisterNode(GetStringVariableTemplate)
export class GetStringVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: String(value ?? '') } };
}
}

View File

@@ -0,0 +1,6 @@
/**
* @zh 变量节点导出
* @en Variable nodes export
*/
export * from './VariableNodes';

View File

@@ -0,0 +1,334 @@
/**
* @zh 蓝图装饰器 - 用于标记可在蓝图中使用的组件、属性和方法
* @en Blueprint Decorators - Mark components, properties and methods for blueprint use
*
* @example
* ```typescript
* import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
*
* @ECSComponent('Health')
* @BlueprintExpose({ displayName: '生命值组件', category: 'gameplay' })
* export class HealthComponent extends Component {
*
* @BlueprintProperty({ displayName: '当前生命值', type: 'float' })
* current: number = 100;
*
* @BlueprintProperty({ displayName: '最大生命值', type: 'float', readonly: true })
* max: number = 100;
*
* @BlueprintMethod({
* displayName: '治疗',
* params: [{ name: 'amount', type: 'float' }]
* })
* heal(amount: number): void {
* this.current = Math.min(this.current + amount, this.max);
* }
*
* @BlueprintMethod({
* displayName: '受伤',
* params: [{ name: 'amount', type: 'float' }],
* returnType: 'bool'
* })
* takeDamage(amount: number): boolean {
* this.current -= amount;
* return this.current <= 0;
* }
* }
* ```
*/
import type { BlueprintPinType } from '../types/pins';
// ============================================================================
// Types | 类型定义
// ============================================================================
/**
* @zh 参数定义
* @en Parameter definition
*/
export interface BlueprintParamDef {
/** @zh 参数名称 @en Parameter name */
name: string;
/** @zh 显示名称 @en Display name */
displayName?: string;
/** @zh 引脚类型 @en Pin type */
type?: BlueprintPinType;
/** @zh 默认值 @en Default value */
defaultValue?: unknown;
}
/**
* @zh 蓝图暴露选项
* @en Blueprint expose options
*/
export interface BlueprintExposeOptions {
/** @zh 组件显示名称 @en Component display name */
displayName?: string;
/** @zh 组件描述 @en Component description */
description?: string;
/** @zh 组件分类 @en Component category */
category?: string;
/** @zh 组件颜色 @en Component color */
color?: string;
/** @zh 组件图标 @en Component icon */
icon?: string;
}
/**
* @zh 蓝图属性选项
* @en Blueprint property options
*/
export interface BlueprintPropertyOptions {
/** @zh 属性显示名称 @en Property display name */
displayName?: string;
/** @zh 属性描述 @en Property description */
description?: string;
/** @zh 引脚类型 @en Pin type */
type?: BlueprintPinType;
/** @zh 是否只读(不生成 Set 节点)@en Readonly (no Set node generated) */
readonly?: boolean;
/** @zh 默认值 @en Default value */
defaultValue?: unknown;
}
/**
* @zh 蓝图方法选项
* @en Blueprint method options
*/
export interface BlueprintMethodOptions {
/** @zh 方法显示名称 @en Method display name */
displayName?: string;
/** @zh 方法描述 @en Method description */
description?: string;
/** @zh 是否是纯函数(无副作用)@en Is pure function (no side effects) */
isPure?: boolean;
/** @zh 参数列表 @en Parameter list */
params?: BlueprintParamDef[];
/** @zh 返回值类型 @en Return type */
returnType?: BlueprintPinType;
}
/**
* @zh 属性元数据
* @en Property metadata
*/
export interface PropertyMetadata {
propertyKey: string;
displayName: string;
description?: string;
pinType: BlueprintPinType;
readonly: boolean;
defaultValue?: unknown;
}
/**
* @zh 方法元数据
* @en Method metadata
*/
export interface MethodMetadata {
methodKey: string;
displayName: string;
description?: string;
isPure: boolean;
params: BlueprintParamDef[];
returnType: BlueprintPinType;
}
/**
* @zh 组件蓝图元数据
* @en Component blueprint metadata
*/
export interface ComponentBlueprintMetadata extends BlueprintExposeOptions {
componentName: string;
properties: PropertyMetadata[];
methods: MethodMetadata[];
}
// ============================================================================
// Registry | 注册表
// ============================================================================
/**
* @zh 已注册的蓝图组件
* @en Registered blueprint components
*/
const registeredComponents = new Map<Function, ComponentBlueprintMetadata>();
/**
* @zh 获取所有已注册的蓝图组件
* @en Get all registered blueprint components
*/
export function getRegisteredBlueprintComponents(): Map<Function, ComponentBlueprintMetadata> {
return registeredComponents;
}
/**
* @zh 获取组件的蓝图元数据
* @en Get blueprint metadata for a component
*/
export function getBlueprintMetadata(componentClass: Function): ComponentBlueprintMetadata | undefined {
return registeredComponents.get(componentClass);
}
/**
* @zh 清除所有注册的蓝图组件(用于测试)
* @en Clear all registered blueprint components (for testing)
*/
export function clearRegisteredComponents(): void {
registeredComponents.clear();
}
// ============================================================================
// Internal Helpers | 内部辅助函数
// ============================================================================
function getOrCreateMetadata(constructor: Function): ComponentBlueprintMetadata {
let metadata = registeredComponents.get(constructor);
if (!metadata) {
metadata = {
componentName: (constructor as any).__componentName__ ?? constructor.name,
properties: [],
methods: []
};
registeredComponents.set(constructor, metadata);
}
return metadata;
}
// ============================================================================
// Decorators | 装饰器
// ============================================================================
/**
* @zh 标记组件可在蓝图中使用
* @en Mark component as usable in blueprint
*
* @example
* ```typescript
* @ECSComponent('Player')
* @BlueprintExpose({ displayName: '玩家', category: 'gameplay' })
* export class PlayerComponent extends Component { }
* ```
*/
export function BlueprintExpose(options: BlueprintExposeOptions = {}): ClassDecorator {
return function (target: Function) {
const metadata = getOrCreateMetadata(target);
Object.assign(metadata, options);
metadata.componentName = (target as any).__componentName__ ?? target.name;
return target as any;
};
}
/**
* @zh 标记属性可在蓝图中访问
* @en Mark property as accessible in blueprint
*
* @example
* ```typescript
* @BlueprintProperty({ displayName: '生命值', type: 'float' })
* health: number = 100;
*
* @BlueprintProperty({ displayName: '名称', type: 'string', readonly: true })
* name: string = 'Player';
* ```
*/
export function BlueprintProperty(options: BlueprintPropertyOptions = {}): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol) {
const key = String(propertyKey);
const metadata = getOrCreateMetadata(target.constructor);
const propMeta: PropertyMetadata = {
propertyKey: key,
displayName: options.displayName ?? key,
description: options.description,
pinType: options.type ?? 'any',
readonly: options.readonly ?? false,
defaultValue: options.defaultValue
};
const existingIndex = metadata.properties.findIndex(p => p.propertyKey === key);
if (existingIndex >= 0) {
metadata.properties[existingIndex] = propMeta;
} else {
metadata.properties.push(propMeta);
}
};
}
/**
* @zh 标记方法可在蓝图中调用
* @en Mark method as callable in blueprint
*
* @example
* ```typescript
* @BlueprintMethod({
* displayName: '攻击',
* params: [
* { name: 'target', type: 'entity' },
* { name: 'damage', type: 'float' }
* ],
* returnType: 'bool'
* })
* attack(target: Entity, damage: number): boolean { }
*
* @BlueprintMethod({ displayName: '获取速度', isPure: true, returnType: 'float' })
* getSpeed(): number { return this.speed; }
* ```
*/
export function BlueprintMethod(options: BlueprintMethodOptions = {}): MethodDecorator {
return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const key = String(propertyKey);
const metadata = getOrCreateMetadata(target.constructor);
const methodMeta: MethodMetadata = {
methodKey: key,
displayName: options.displayName ?? key,
description: options.description,
isPure: options.isPure ?? false,
params: options.params ?? [],
returnType: options.returnType ?? 'any'
};
const existingIndex = metadata.methods.findIndex(m => m.methodKey === key);
if (existingIndex >= 0) {
metadata.methods[existingIndex] = methodMeta;
} else {
metadata.methods.push(methodMeta);
}
return descriptor;
};
}
// ============================================================================
// Utility Functions | 工具函数
// ============================================================================
/**
* @zh 从 TypeScript 类型名推断蓝图引脚类型
* @en Infer blueprint pin type from TypeScript type name
*/
export function inferPinType(typeName: string): BlueprintPinType {
const typeMap: Record<string, BlueprintPinType> = {
'number': 'float',
'Number': 'float',
'string': 'string',
'String': 'string',
'boolean': 'bool',
'Boolean': 'bool',
'Entity': 'entity',
'Component': 'component',
'Vector2': 'vector2',
'Vec2': 'vector2',
'Vector3': 'vector3',
'Vec3': 'vector3',
'Color': 'color',
'Array': 'array',
'Object': 'object',
'void': 'exec',
'undefined': 'exec'
};
return typeMap[typeName] ?? 'any';
}

View File

@@ -0,0 +1,447 @@
/**
* @zh 组件节点生成器 - 自动为标记的组件生成蓝图节点
* @en Component Node Generator - Auto-generate blueprint nodes for marked components
*
* @zh 根据 @BlueprintExpose、@BlueprintProperty、@BlueprintMethod 装饰器
* 自动生成对应的 Get/Set/Call 节点并注册到 NodeRegistry
*
* @en Based on @BlueprintExpose, @BlueprintProperty, @BlueprintMethod decorators,
* auto-generate corresponding Get/Set/Call nodes and register to NodeRegistry
*/
import type { Component, Entity } from '@esengine/ecs-framework';
import type { BlueprintNodeTemplate, BlueprintNode } from '../types/nodes';
import type { BlueprintPinType } from '../types/pins';
import type { ExecutionContext, ExecutionResult } from '../runtime/ExecutionContext';
import type { INodeExecutor } from '../runtime/NodeRegistry';
import { NodeRegistry } from '../runtime/NodeRegistry';
import {
getRegisteredBlueprintComponents,
type ComponentBlueprintMetadata,
type PropertyMetadata,
type MethodMetadata
} from './BlueprintDecorators';
// ============================================================================
// Node Generator | 节点生成器
// ============================================================================
/**
* @zh 为组件生成所有蓝图节点
* @en Generate all blueprint nodes for a component
*/
export function generateComponentNodes(
componentClass: Function,
metadata: ComponentBlueprintMetadata
): void {
const { componentName, properties, methods } = metadata;
const category = metadata.category ?? 'component';
const color = metadata.color ?? '#1e8b8b';
// Generate Add/Get component nodes
generateAddComponentNode(componentClass, componentName, metadata, color);
generateGetComponentNode(componentClass, componentName, metadata, color);
for (const prop of properties) {
generatePropertyGetNode(componentName, prop, category, color);
if (!prop.readonly) {
generatePropertySetNode(componentName, prop, category, color);
}
}
for (const method of methods) {
generateMethodCallNode(componentName, method, category, color);
}
}
/**
* @zh 生成 Add Component 节点
* @en Generate Add Component node
*/
function generateAddComponentNode(
componentClass: Function,
componentName: string,
metadata: ComponentBlueprintMetadata,
color: string
): void {
const nodeType = `Add_${componentName}`;
const displayName = metadata.displayName ?? componentName;
// Build input pins for initial property values
const propertyInputs: BlueprintNodeTemplate['inputs'] = [];
const propertyDefaults: Record<string, unknown> = {};
for (const prop of metadata.properties) {
if (!prop.readonly) {
propertyInputs.push({
name: prop.propertyKey,
type: prop.pinType,
displayName: prop.displayName,
defaultValue: prop.defaultValue
});
propertyDefaults[prop.propertyKey] = prop.defaultValue;
}
}
const template: BlueprintNodeTemplate = {
type: nodeType,
title: `Add ${displayName}`,
category: 'component',
color,
description: `Adds ${displayName} component to entity (为实体添加 ${displayName} 组件)`,
keywords: ['add', 'component', 'create', componentName.toLowerCase()],
menuPath: ['Components', displayName, `Add ${displayName}`],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
...propertyInputs
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'component', type: 'component', displayName: displayName },
{ name: 'success', type: 'bool', displayName: 'Success' }
]
};
const propertyKeys = metadata.properties
.filter(p => !p.readonly)
.map(p => p.propertyKey);
const executor: INodeExecutor = {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
if (!entity || entity.isDestroyed) {
return { outputs: { component: null, success: false }, nextExec: 'exec' };
}
// Check if component already exists
const existing = entity.components.find(c =>
c.constructor === componentClass ||
c.constructor.name === componentName ||
(c.constructor as any).__componentName__ === componentName
);
if (existing) {
// Component already exists, return it
return { outputs: { component: existing, success: false }, nextExec: 'exec' };
}
try {
// Create new component instance
const component = new (componentClass as new () => Component)();
// Set initial property values from inputs
for (const key of propertyKeys) {
const value = context.evaluateInput(node.id, key, propertyDefaults[key]);
if (value !== undefined) {
(component as any)[key] = value;
}
}
// Add to entity
entity.addComponent(component);
return { outputs: { component, success: true }, nextExec: 'exec' };
} catch (error) {
console.error(`[Blueprint] Failed to add ${componentName}:`, error);
return { outputs: { component: null, success: false }, nextExec: 'exec' };
}
}
};
NodeRegistry.instance.register(template, executor);
}
/**
* @zh 生成 Get Component 节点
* @en Generate Get Component node
*/
function generateGetComponentNode(
componentClass: Function,
componentName: string,
metadata: ComponentBlueprintMetadata,
color: string
): void {
const nodeType = `Get_${componentName}`;
const displayName = metadata.displayName ?? componentName;
const template: BlueprintNodeTemplate = {
type: nodeType,
title: `Get ${displayName}`,
category: 'component',
color,
isPure: true,
description: `Gets ${displayName} component from entity (从实体获取 ${displayName} 组件)`,
keywords: ['get', 'component', componentName.toLowerCase()],
menuPath: ['Components', displayName, `Get ${displayName}`],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'component', type: 'component', displayName: displayName },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
const executor: INodeExecutor = {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
if (!entity || entity.isDestroyed) {
return { outputs: { component: null, found: false } };
}
const component = entity.components.find(c =>
c.constructor === componentClass ||
c.constructor.name === componentName ||
(c.constructor as any).__componentName__ === componentName
);
return {
outputs: {
component: component ?? null,
found: component != null
}
};
}
};
NodeRegistry.instance.register(template, executor);
}
/**
* @zh 生成属性 Get 节点
* @en Generate property Get node
*/
function generatePropertyGetNode(
componentName: string,
prop: PropertyMetadata,
category: string,
color: string
): void {
const nodeType = `Get_${componentName}_${prop.propertyKey}`;
const { displayName, pinType } = prop;
const template: BlueprintNodeTemplate = {
type: nodeType,
title: `Get ${displayName}`,
subtitle: componentName,
category: category as any,
color,
isPure: true,
description: prop.description ?? `Gets ${displayName} from ${componentName} (从 ${componentName} 获取 ${displayName})`,
keywords: ['get', 'property', componentName.toLowerCase(), prop.propertyKey.toLowerCase()],
menuPath: ['Components', componentName, `Get ${displayName}`],
inputs: [
{ name: 'component', type: 'component', displayName: componentName }
],
outputs: [
{ name: 'value', type: pinType, displayName }
]
};
const propertyKey = prop.propertyKey;
const defaultValue = prop.defaultValue;
const executor: INodeExecutor = {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
if (!component) {
return { outputs: { value: defaultValue ?? null } };
}
const value = (component as any)[propertyKey];
return { outputs: { value } };
}
};
NodeRegistry.instance.register(template, executor);
}
/**
* @zh 生成属性 Set 节点
* @en Generate property Set node
*/
function generatePropertySetNode(
componentName: string,
prop: PropertyMetadata,
category: string,
color: string
): void {
const nodeType = `Set_${componentName}_${prop.propertyKey}`;
const { displayName, pinType, defaultValue } = prop;
const template: BlueprintNodeTemplate = {
type: nodeType,
title: `Set ${displayName}`,
subtitle: componentName,
category: category as any,
color,
description: prop.description ?? `Sets ${displayName} on ${componentName} (设置 ${componentName}${displayName})`,
keywords: ['set', 'property', componentName.toLowerCase(), prop.propertyKey.toLowerCase()],
menuPath: ['Components', componentName, `Set ${displayName}`],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'component', type: 'component', displayName: componentName },
{ name: 'value', type: pinType, displayName, defaultValue }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
const propertyKey = prop.propertyKey;
const executor: INodeExecutor = {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
const value = context.evaluateInput(node.id, 'value', defaultValue);
if (component) {
(component as any)[propertyKey] = value;
}
return { nextExec: 'exec' };
}
};
NodeRegistry.instance.register(template, executor);
}
/**
* @zh 生成方法调用节点
* @en Generate method call node
*/
function generateMethodCallNode(
componentName: string,
method: MethodMetadata,
category: string,
color: string
): void {
const nodeType = `Call_${componentName}_${method.methodKey}`;
const { displayName, isPure, params, returnType } = method;
const inputs: BlueprintNodeTemplate['inputs'] = [];
if (!isPure) {
inputs.push({ name: 'exec', type: 'exec', displayName: '' });
}
inputs.push({ name: 'component', type: 'component', displayName: componentName });
const paramNames: string[] = [];
for (const param of params) {
inputs.push({
name: param.name,
type: param.type ?? 'any',
displayName: param.displayName ?? param.name,
defaultValue: param.defaultValue
});
paramNames.push(param.name);
}
const outputs: BlueprintNodeTemplate['outputs'] = [];
if (!isPure) {
outputs.push({ name: 'exec', type: 'exec', displayName: '' });
}
if (returnType !== 'exec' && returnType !== 'any') {
outputs.push({
name: 'result',
type: returnType as BlueprintPinType,
displayName: 'Result'
});
}
const template: BlueprintNodeTemplate = {
type: nodeType,
title: displayName,
subtitle: componentName,
category: category as any,
color,
isPure,
description: method.description ?? `Calls ${displayName} on ${componentName} (调用 ${componentName}${displayName})`,
keywords: ['call', 'method', componentName.toLowerCase(), method.methodKey.toLowerCase()],
menuPath: ['Components', componentName, displayName],
inputs,
outputs
};
const methodKey = method.methodKey;
const executor: INodeExecutor = {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
if (!component) {
return isPure ? { outputs: { result: null } } : { nextExec: 'exec' };
}
const args: unknown[] = paramNames.map(name =>
context.evaluateInput(node.id, name, undefined)
);
const fn = (component as any)[methodKey];
if (typeof fn !== 'function') {
console.warn(`Method ${methodKey} not found on component ${componentName}`);
return isPure ? { outputs: { result: null } } : { nextExec: 'exec' };
}
const result = fn.apply(component, args);
return isPure
? { outputs: { result } }
: { outputs: { result }, nextExec: 'exec' };
}
};
NodeRegistry.instance.register(template, executor);
}
// ============================================================================
// Registration | 注册
// ============================================================================
/**
* @zh 注册所有已标记的组件节点
* @en Register all marked component nodes
*
* @zh 应该在蓝图系统初始化时调用,会扫描所有使用 @BlueprintExpose 装饰的组件
* 并自动生成对应的蓝图节点
*
* @en Should be called during blueprint system initialization, scans all components
* decorated with @BlueprintExpose and auto-generates corresponding blueprint nodes
*/
export function registerAllComponentNodes(): void {
const components = getRegisteredBlueprintComponents();
for (const [componentClass, metadata] of components) {
try {
generateComponentNodes(componentClass, metadata);
console.log(`[Blueprint] Registered component: ${metadata.componentName} (${metadata.properties.length} properties, ${metadata.methods.length} methods)`);
} catch (error) {
console.error(`[Blueprint] Failed to register component ${metadata.componentName}:`, error);
}
}
console.log(`[Blueprint] Registered ${components.size} component(s)`);
}
/**
* @zh 手动注册单个组件
* @en Manually register a single component
*/
export function registerComponentNodes(componentClass: Function): void {
const components = getRegisteredBlueprintComponents();
const metadata = components.get(componentClass);
if (!metadata) {
console.warn(`[Blueprint] Component ${componentClass.name} is not marked with @BlueprintExpose`);
return;
}
generateComponentNodes(componentClass, metadata);
}

View File

@@ -0,0 +1,69 @@
/**
* @zh 蓝图注册系统
* @en Blueprint Registry System
*
* @zh 提供组件自动节点生成功能,用户只需使用装饰器标记组件,
* 即可自动在蓝图编辑器中生成对应的 Get/Set/Call 节点
*
* @en Provides automatic node generation for components. Users only need to
* mark components with decorators, and corresponding Get/Set/Call nodes
* will be auto-generated in the blueprint editor
*
* @example
* ```typescript
* // 1. 定义组件时使用装饰器 | Define component with decorators
* @ECSComponent('Health')
* @BlueprintExpose({ displayName: '生命值', category: 'gameplay' })
* export class HealthComponent extends Component {
* @BlueprintProperty({ displayName: '当前生命值', type: 'float' })
* current: number = 100;
*
* @BlueprintMethod({
* displayName: '治疗',
* params: [{ name: 'amount', type: 'float' }]
* })
* heal(amount: number): void {
* this.current = Math.min(this.current + amount, 100);
* }
* }
*
* // 2. 初始化蓝图系统时注册 | Register when initializing blueprint system
* import { registerAllComponentNodes } from '@esengine/blueprint';
* registerAllComponentNodes();
*
* // 3. 现在蓝图编辑器中会出现以下节点:
* // Now these nodes appear in blueprint editor:
* // - Get Health获取组件
* // - Get 当前生命值(获取属性)
* // - Set 当前生命值(设置属性)
* // - 治疗(调用方法)
* ```
*/
// Decorators | 装饰器
export {
BlueprintExpose,
BlueprintProperty,
BlueprintMethod,
getRegisteredBlueprintComponents,
getBlueprintMetadata,
clearRegisteredComponents,
inferPinType
} from './BlueprintDecorators';
export type {
BlueprintParamDef,
BlueprintExposeOptions,
BlueprintPropertyOptions,
BlueprintMethodOptions,
PropertyMetadata,
MethodMetadata,
ComponentBlueprintMetadata
} from './BlueprintDecorators';
// Node Generator | 节点生成器
export {
generateComponentNodes,
registerAllComponentNodes,
registerComponentNodes
} from './ComponentNodeGenerator';

View File

@@ -1,116 +1,117 @@
/**
* Blueprint Component - Attaches a blueprint to an entity
* 蓝图组件 - 将蓝图附加到实体
* @zh 蓝图组件 - 将蓝图附加到实体
* @en Blueprint Component - Attaches a blueprint to an entity
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import { Component, ECSComponent, type Entity, type IScene } from '@esengine/ecs-framework';
import { BlueprintAsset } from '../types/blueprint';
import { BlueprintVM } from './BlueprintVM';
/**
* Component interface for ECS integration
* 用于 ECS 集成的组件接口
* @zh 蓝图组件,用于将可视化脚本附加到 ECS 实体
* @en Blueprint component for attaching visual scripts to ECS entities
*
* @example
* ```typescript
* const entity = scene.createEntity('Player');
* const blueprint = new BlueprintComponent();
* blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
* blueprint.autoStart = true;
* entity.addComponent(blueprint);
* ```
*/
export interface IBlueprintComponent {
/** Entity ID this component belongs to (此组件所属的实体ID) */
entityId: number | null;
@ECSComponent('Blueprint')
export class BlueprintComponent extends Component {
/**
* @zh 蓝图资产引用
* @en Blueprint asset reference
*/
blueprintAsset: BlueprintAsset | null = null;
/** Blueprint asset reference (蓝图资产引用) */
blueprintAsset: BlueprintAsset | null;
/**
* @zh 用于序列化的蓝图资产路径
* @en Blueprint asset path for serialization
*/
blueprintPath: string = '';
/** Blueprint asset path for serialization (用于序列化的蓝图资产路径) */
blueprintPath: string;
/**
* @zh 实体创建时自动开始执行
* @en Auto-start execution when entity is created
*/
autoStart: boolean = true;
/** Auto-start execution when entity is created (实体创建时自动开始执行) */
autoStart: boolean;
/**
* @zh 启用 VM 调试模式
* @en Enable debug mode for VM
*/
debug: boolean = false;
/** Enable debug mode for VM (启用 VM 调试模式) */
debug: boolean;
/**
* @zh 运行时 VM 实例
* @en Runtime VM instance
*/
vm: BlueprintVM | null = null;
/** Runtime VM instance (运行时 VM 实例) */
vm: BlueprintVM | null;
/**
* @zh 蓝图是否已启动
* @en Whether the blueprint has started
*/
isStarted: boolean = false;
/** Whether the blueprint has started (蓝图是否已启动) */
isStarted: boolean;
}
/**
* @zh 初始化蓝图 VM
* @en Initialize blueprint VM
*/
initialize(entity: Entity, scene: IScene): void {
if (!this.blueprintAsset) return;
/**
* Creates a blueprint component data object
* 创建蓝图组件数据对象
*/
export function createBlueprintComponentData(): IBlueprintComponent {
return {
entityId: null,
blueprintAsset: null,
blueprintPath: '',
autoStart: true,
debug: false,
vm: null,
isStarted: false
};
}
/**
* Initialize the VM for a blueprint component
* 为蓝图组件初始化 VM
*/
export function initializeBlueprintVM(
component: IBlueprintComponent,
entity: Entity,
scene: IScene
): void {
if (!component.blueprintAsset) {
return;
this.vm = new BlueprintVM(this.blueprintAsset, entity, scene);
this.vm.debug = this.debug;
}
// Create VM instance
// 创建 VM 实例
component.vm = new BlueprintVM(component.blueprintAsset, entity, scene);
component.vm.debug = component.debug;
}
/**
* Start blueprint execution
* 开始蓝图执行
*/
export function startBlueprint(component: IBlueprintComponent): void {
if (component.vm && !component.isStarted) {
component.vm.start();
component.isStarted = true;
}
}
/**
* Stop blueprint execution
* 停止蓝图执行
*/
export function stopBlueprint(component: IBlueprintComponent): void {
if (component.vm && component.isStarted) {
component.vm.stop();
component.isStarted = false;
}
}
/**
* Update blueprint execution
* 更新蓝图执行
*/
export function tickBlueprint(component: IBlueprintComponent, deltaTime: number): void {
if (component.vm && component.isStarted) {
component.vm.tick(deltaTime);
}
}
/**
* Clean up blueprint resources
* 清理蓝图资源
*/
export function cleanupBlueprint(component: IBlueprintComponent): void {
if (component.vm) {
if (component.isStarted) {
component.vm.stop();
/**
* @zh 开始执行蓝图
* @en Start blueprint execution
*/
start(): void {
if (this.vm && !this.isStarted) {
this.vm.start();
this.isStarted = true;
}
}
/**
* @zh 停止执行蓝图
* @en Stop blueprint execution
*/
stop(): void {
if (this.vm && this.isStarted) {
this.vm.stop();
this.isStarted = false;
}
}
/**
* @zh 更新蓝图
* @en Update blueprint
*/
tick(deltaTime: number): void {
if (this.vm && this.isStarted) {
this.vm.tick(deltaTime);
}
}
/**
* @zh 清理蓝图资源
* @en Cleanup blueprint resources
*/
cleanup(): void {
if (this.vm) {
if (this.isStarted) {
this.vm.stop();
}
this.vm = null;
this.isStarted = false;
}
component.vm = null;
component.isStarted = false;
}
}

View File

@@ -1,121 +1,86 @@
/**
* Blueprint Execution System - Manages blueprint lifecycle and execution
* 蓝图执行系统 - 管理蓝图生命周期和执行
* @zh 蓝图系统 - 处理所有带有 BlueprintComponent 的实体
* @en Blueprint System - Processes all entities with BlueprintComponent
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import {
IBlueprintComponent,
initializeBlueprintVM,
startBlueprint,
tickBlueprint,
cleanupBlueprint
} from './BlueprintComponent';
import { EntitySystem, Matcher, ECSSystem, type Entity, Time } from '@esengine/ecs-framework';
import { BlueprintComponent } from './BlueprintComponent';
import { registerAllComponentNodes } from '../registry';
/**
* Blueprint system interface for engine integration
* 用于引擎集成的蓝图系统接口
* @zh 蓝图执行系统
* @en Blueprint execution system
*
* @zh 自动处理所有带有 BlueprintComponent 的实体,管理蓝图的初始化、执行和清理
* @en Automatically processes all entities with BlueprintComponent, manages blueprint initialization, execution and cleanup
*
* @example
* ```typescript
* import { BlueprintSystem } from '@esengine/blueprint';
*
* // 添加到场景
* scene.addSystem(new BlueprintSystem());
*
* // 为实体添加蓝图
* const entity = scene.createEntity('Player');
* const blueprint = new BlueprintComponent();
* blueprint.blueprintAsset = await loadBlueprintAsset('player.bp');
* entity.addComponent(blueprint);
* ```
*/
export interface IBlueprintSystem {
/** Process entities with blueprint components (处理带有蓝图组件的实体) */
process(entities: IBlueprintEntity[], deltaTime: number): void;
@ECSSystem('BlueprintSystem')
export class BlueprintSystem extends EntitySystem {
private _componentsRegistered = false;
/** Called when entity is added to system (实体添加到系统时调用) */
onEntityAdded(entity: IBlueprintEntity): void;
constructor() {
super(Matcher.all(BlueprintComponent));
}
/** Called when entity is removed from system (实体从系统移除时调用) */
onEntityRemoved(entity: IBlueprintEntity): void;
}
/**
* Entity with blueprint component
* 带有蓝图组件的实体
*/
export interface IBlueprintEntity extends Entity {
/** Blueprint component data (蓝图组件数据) */
blueprintComponent: IBlueprintComponent;
}
/**
* Creates a blueprint execution system
* 创建蓝图执行系统
*/
export function createBlueprintSystem(scene: IScene): IBlueprintSystem {
return {
process(entities: IBlueprintEntity[], deltaTime: number): void {
for (const entity of entities) {
const component = entity.blueprintComponent;
// Skip if no blueprint asset loaded
// 如果没有加载蓝图资产则跳过
if (!component.blueprintAsset) {
continue;
}
// Initialize VM if needed
// 如果需要则初始化 VM
if (!component.vm) {
initializeBlueprintVM(component, entity, scene);
}
// Auto-start if enabled
// 如果启用则自动启动
if (component.autoStart && !component.isStarted) {
startBlueprint(component);
}
// Tick the blueprint
// 更新蓝图
tickBlueprint(component, deltaTime);
}
},
onEntityAdded(entity: IBlueprintEntity): void {
const component = entity.blueprintComponent;
if (component.blueprintAsset) {
initializeBlueprintVM(component, entity, scene);
if (component.autoStart) {
startBlueprint(component);
}
}
},
onEntityRemoved(entity: IBlueprintEntity): void {
cleanupBlueprint(entity.blueprintComponent);
/**
* @zh 系统初始化时注册所有组件节点
* @en Register all component nodes when system initializes
*/
protected override onInitialize(): void {
if (!this._componentsRegistered) {
registerAllComponentNodes();
this._componentsRegistered = true;
}
};
}
}
/**
* Utility to manually trigger blueprint events
* 手动触发蓝图事件的工具
*/
export function triggerBlueprintEvent(
entity: IBlueprintEntity,
eventType: string,
data?: Record<string, unknown>
): void {
const vm = entity.blueprintComponent.vm;
/**
* @zh 处理所有带有蓝图组件的实体
* @en Process all entities with blueprint components
*/
protected override process(entities: readonly Entity[]): void {
const dt = Time.deltaTime;
if (vm && entity.blueprintComponent.isStarted) {
vm.triggerEvent(eventType, data);
}
}
/**
* Utility to trigger custom events by name
* 按名称触发自定义事件的工具
*/
export function triggerCustomBlueprintEvent(
entity: IBlueprintEntity,
eventName: string,
data?: Record<string, unknown>
): void {
const vm = entity.blueprintComponent.vm;
if (vm && entity.blueprintComponent.isStarted) {
vm.triggerCustomEvent(eventName, data);
for (const entity of entities) {
const blueprint = entity.getComponent(BlueprintComponent);
if (!blueprint?.blueprintAsset) continue;
// 初始化 VM
if (!blueprint.vm) {
blueprint.initialize(entity, this.scene!);
}
// 自动启动
if (blueprint.autoStart && !blueprint.isStarted) {
blueprint.start();
}
// 每帧更新
blueprint.tick(dt);
}
}
/**
* @zh 实体移除时清理蓝图资源
* @en Cleanup blueprint resources when entity is removed
*/
protected override onRemoved(entity: Entity): void {
const blueprint = entity.getComponent(BlueprintComponent);
if (blueprint) {
blueprint.cleanup();
}
}
}

View File

@@ -3,9 +3,10 @@
* 执行上下文 - 蓝图执行的运行时上下文
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import type { Entity, IScene, Component } from '@esengine/ecs-framework';
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
import { BlueprintAsset } from '../types/blueprint';
import { getRegisteredBlueprintComponents } from '../registry/BlueprintDecorators';
/**
* Result of node execution
@@ -72,6 +73,9 @@ export class ExecutionContext {
/** Global variables (shared) (全局变量,共享) */
private static _globalVariables: Map<string, unknown> = new Map();
/** Component class registry (组件类注册表) */
private static _componentRegistry: Map<string, new () => Component> = new Map();
/** Node output cache for current execution (当前执行的节点输出缓存) */
private _outputCache: Map<string, Record<string, unknown>> = new Map();
@@ -267,4 +271,49 @@ export class ExecutionContext {
static clearGlobalVariables(): void {
ExecutionContext._globalVariables.clear();
}
/**
* Get a component class by name
* 通过名称获取组件类
*
* @zh 首先检查 @BlueprintExpose 装饰的组件,然后检查手动注册的组件
* @en First checks @BlueprintExpose decorated components, then manually registered ones
*/
getComponentClass(typeName: string): (new () => Component) | undefined {
// First check registered blueprint components
const blueprintComponents = getRegisteredBlueprintComponents();
for (const [componentClass, metadata] of blueprintComponents) {
if (metadata.componentName === typeName ||
componentClass.name === typeName) {
return componentClass as new () => Component;
}
}
// Then check manual registry
return ExecutionContext._componentRegistry.get(typeName);
}
/**
* Register a component class for dynamic creation
* 注册组件类以支持动态创建
*/
static registerComponentClass(typeName: string, componentClass: new () => Component): void {
ExecutionContext._componentRegistry.set(typeName, componentClass);
}
/**
* Unregister a component class
* 取消注册组件类
*/
static unregisterComponentClass(typeName: string): void {
ExecutionContext._componentRegistry.delete(typeName);
}
/**
* Get all registered component classes
* 获取所有已注册的组件类
*/
static getRegisteredComponentClasses(): Map<string, new () => Component> {
return new Map(ExecutionContext._componentRegistry);
}
}

View File

@@ -87,10 +87,21 @@ export interface BlueprintAsset {
}
/**
* Creates an empty blueprint asset
* 创建空蓝图资产
* Creates an empty blueprint asset with default Event Begin Play node
* 创建带有默认 Event Begin Play 节点的空蓝图资产
*/
export function createEmptyBlueprint(name: string): BlueprintAsset {
export function createEmptyBlueprint(name: string, includeBeginPlay: boolean = true): BlueprintAsset {
const nodes: BlueprintNode[] = [];
if (includeBeginPlay) {
nodes.push({
id: 'node_beginplay_1',
type: 'EventBeginPlay',
position: { x: 100, y: 200 },
data: {}
});
}
return {
version: 1,
type: 'blueprint',
@@ -100,7 +111,7 @@ export function createEmptyBlueprint(name: string): BlueprintAsset {
modifiedAt: Date.now()
},
variables: [],
nodes: [],
nodes,
connections: []
};
}

View File

@@ -1,5 +1,26 @@
# @esengine/fsm
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0
### Patch Changes
- Updated dependencies [[`2e84942`](https://github.com/esengine/esengine/commit/2e84942ea14c5326620398add05840fa8bea16f8)]:
- @esengine/blueprint@4.2.0
## 5.0.0
### Patch Changes
- Updated dependencies [[`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac)]:
- @esengine/blueprint@4.1.0
## 4.0.1
### Patch Changes

View File

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

View File

@@ -1,5 +1,26 @@
# @esengine/network
## 8.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 7.0.0
### Patch Changes
- Updated dependencies [[`2e84942`](https://github.com/esengine/esengine/commit/2e84942ea14c5326620398add05840fa8bea16f8)]:
- @esengine/blueprint@4.2.0
## 6.0.0
### Patch Changes
- Updated dependencies [[`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac)]:
- @esengine/blueprint@4.1.0
## 5.0.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/network",
"version": "5.0.3",
"version": "8.0.0",
"description": "Network synchronization for multiplayer games",
"esengine": {
"plugin": true,

View File

@@ -1,5 +1,26 @@
# @esengine/pathfinding
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0
### Patch Changes
- Updated dependencies [[`2e84942`](https://github.com/esengine/esengine/commit/2e84942ea14c5326620398add05840fa8bea16f8)]:
- @esengine/blueprint@4.2.0
## 5.0.0
### Patch Changes
- Updated dependencies [[`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac)]:
- @esengine/blueprint@4.1.0
## 4.0.1
### Patch Changes

View File

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

View File

@@ -1,5 +1,26 @@
# @esengine/procgen
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0
### Patch Changes
- Updated dependencies [[`2e84942`](https://github.com/esengine/esengine/commit/2e84942ea14c5326620398add05840fa8bea16f8)]:
- @esengine/blueprint@4.2.0
## 5.0.0
### Patch Changes
- Updated dependencies [[`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac)]:
- @esengine/blueprint@4.1.0
## 4.0.1
### Patch Changes

View File

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

View File

@@ -32,6 +32,8 @@
"build": "tsup && tsc --emitDeclarationOnly",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"lint": "eslint src --max-warnings 0",
"lint:fix": "eslint src --fix",
"clean": "rimraf dist"
},
"dependencies": {},

View File

@@ -11,11 +11,11 @@ import type {
ApiOutput,
MsgData,
Packet,
ConnectionStatus,
} from '../types'
import { RpcError, ErrorCode } from '../types'
import { json } from '../codec/json'
import type { Codec } from '../codec/types'
ConnectionStatus
} from '../types';
import { RpcError, ErrorCode } from '../types';
import { json } from '../codec/json';
import type { Codec } from '../codec/types';
// ============================================================================
// Re-exports | 类型重导出
@@ -29,9 +29,9 @@ export type {
ApiOutput,
MsgData,
ConnectionStatus,
Codec,
}
export { RpcError, ErrorCode }
Codec
};
export { RpcError, ErrorCode };
// ============================================================================
// Types | 类型定义
@@ -133,11 +133,11 @@ const PacketType = {
ApiResponse: 1,
ApiError: 2,
Message: 3,
Heartbeat: 9,
} as const
Heartbeat: 9
} as const;
const defaultWebSocketFactory: WebSocketFactory = (url) =>
new WebSocket(url) as unknown as WebSocketAdapter
new WebSocket(url) as unknown as WebSocketAdapter;
// ============================================================================
// RpcClient Class | RPC 客户端类
@@ -164,34 +164,34 @@ interface PendingCall {
* ```
*/
export class RpcClient<P extends ProtocolDef> {
private readonly _url: string
private readonly _codec: Codec
private readonly _timeout: number
private readonly _reconnectInterval: number
private readonly _wsFactory: WebSocketFactory
private readonly _options: RpcClientOptions
private readonly _url: string;
private readonly _codec: Codec;
private readonly _timeout: number;
private readonly _reconnectInterval: number;
private readonly _wsFactory: WebSocketFactory;
private readonly _options: RpcClientOptions;
private _ws: WebSocketAdapter | null = null
private _status: ConnectionStatus = 'closed'
private _callIdCounter = 0
private _shouldReconnect: boolean
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null
private _ws: WebSocketAdapter | null = null;
private _status: ConnectionStatus = 'closed';
private _callIdCounter = 0;
private _shouldReconnect: boolean;
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private readonly _pendingCalls = new Map<number, PendingCall>()
private readonly _msgHandlers = new Map<string, Set<(data: unknown) => void>>()
private readonly _pendingCalls = new Map<number, PendingCall>();
private readonly _msgHandlers = new Map<string, Set<(data: unknown) => void>>();
constructor(
_protocol: P,
url: string,
options: RpcClientOptions = {}
) {
this._url = url
this._options = options
this._codec = options.codec ?? json()
this._timeout = options.timeout ?? 30000
this._shouldReconnect = options.autoReconnect ?? true
this._reconnectInterval = options.reconnectInterval ?? 3000
this._wsFactory = options.webSocketFactory ?? defaultWebSocketFactory
this._url = url;
this._options = options;
this._codec = options.codec ?? json();
this._timeout = options.timeout ?? 30000;
this._shouldReconnect = options.autoReconnect ?? true;
this._reconnectInterval = options.reconnectInterval ?? 3000;
this._wsFactory = options.webSocketFactory ?? defaultWebSocketFactory;
}
/**
@@ -199,7 +199,7 @@ export class RpcClient<P extends ProtocolDef> {
* @en Connection status
*/
get status(): ConnectionStatus {
return this._status
return this._status;
}
/**
@@ -207,7 +207,7 @@ export class RpcClient<P extends ProtocolDef> {
* @en Whether connected
*/
get isConnected(): boolean {
return this._status === 'open'
return this._status === 'open';
}
/**
@@ -217,38 +217,38 @@ export class RpcClient<P extends ProtocolDef> {
connect(): Promise<this> {
return new Promise((resolve, reject) => {
if (this._status === 'open' || this._status === 'connecting') {
resolve(this)
return
resolve(this);
return;
}
this._status = 'connecting'
this._ws = this._wsFactory(this._url)
this._status = 'connecting';
this._ws = this._wsFactory(this._url);
this._ws.onopen = () => {
this._status = 'open'
this._options.onConnect?.()
resolve(this)
}
this._status = 'open';
this._options.onConnect?.();
resolve(this);
};
this._ws.onclose = (e) => {
this._status = 'closed'
this._rejectAllPending()
this._options.onDisconnect?.(e.reason)
this._scheduleReconnect()
}
this._status = 'closed';
this._rejectAllPending();
this._options.onDisconnect?.(e.reason);
this._scheduleReconnect();
};
this._ws.onerror = () => {
const err = new Error('WebSocket error')
this._options.onError?.(err)
const err = new Error('WebSocket error');
this._options.onError?.(err);
if (this._status === 'connecting') {
reject(err)
reject(err);
}
}
};
this._ws.onmessage = (e) => {
this._handleMessage(e.data as string | ArrayBuffer)
}
})
this._handleMessage(e.data as string | ArrayBuffer);
};
});
}
/**
@@ -256,12 +256,12 @@ export class RpcClient<P extends ProtocolDef> {
* @en Disconnect
*/
disconnect(): void {
this._shouldReconnect = false
this._clearReconnectTimer()
this._shouldReconnect = false;
this._clearReconnectTimer();
if (this._ws) {
this._status = 'closing'
this._ws.close()
this._ws = null
this._status = 'closing';
this._ws.close();
this._ws = null;
}
}
@@ -275,25 +275,25 @@ export class RpcClient<P extends ProtocolDef> {
): Promise<ApiOutput<P['api'][K]>> {
return new Promise((resolve, reject) => {
if (this._status !== 'open') {
reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Not connected'))
return
reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Not connected'));
return;
}
const id = ++this._callIdCounter
const id = ++this._callIdCounter;
const timer = setTimeout(() => {
this._pendingCalls.delete(id)
reject(new RpcError(ErrorCode.TIMEOUT, 'Request timeout'))
}, this._timeout)
this._pendingCalls.delete(id);
reject(new RpcError(ErrorCode.TIMEOUT, 'Request timeout'));
}, this._timeout);
this._pendingCalls.set(id, {
resolve: resolve as (v: unknown) => void,
reject,
timer,
})
timer
});
const packet: Packet = [PacketType.ApiRequest, id, name as string, input]
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer)
})
const packet: Packet = [PacketType.ApiRequest, id, name as string, input];
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer);
});
}
/**
@@ -301,9 +301,9 @@ export class RpcClient<P extends ProtocolDef> {
* @en Send message
*/
send<K extends MsgNames<P>>(name: K, data: MsgData<P['msg'][K]>): void {
if (this._status !== 'open') return
const packet: Packet = [PacketType.Message, name as string, data]
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer)
if (this._status !== 'open') return;
const packet: Packet = [PacketType.Message, name as string, data];
this._ws!.send(this._codec.encode(packet) as string | ArrayBuffer);
}
/**
@@ -314,14 +314,14 @@ export class RpcClient<P extends ProtocolDef> {
name: K,
handler: (data: MsgData<P['msg'][K]>) => void
): this {
const key = name as string
let handlers = this._msgHandlers.get(key)
const key = name as string;
let handlers = this._msgHandlers.get(key);
if (!handlers) {
handlers = new Set()
this._msgHandlers.set(key, handlers)
handlers = new Set();
this._msgHandlers.set(key, handlers);
}
handlers.add(handler as (data: unknown) => void)
return this
handlers.add(handler as (data: unknown) => void);
return this;
}
/**
@@ -332,13 +332,13 @@ export class RpcClient<P extends ProtocolDef> {
name: K,
handler?: (data: MsgData<P['msg'][K]>) => void
): this {
const key = name as string
const key = name as string;
if (handler) {
this._msgHandlers.get(key)?.delete(handler as (data: unknown) => void)
this._msgHandlers.get(key)?.delete(handler as (data: unknown) => void);
} else {
this._msgHandlers.delete(key)
this._msgHandlers.delete(key);
}
return this
return this;
}
/**
@@ -350,10 +350,10 @@ export class RpcClient<P extends ProtocolDef> {
handler: (data: MsgData<P['msg'][K]>) => void
): this {
const wrapper = (data: MsgData<P['msg'][K]>) => {
this.off(name, wrapper)
handler(data)
}
return this.on(name, wrapper)
this.off(name, wrapper);
handler(data);
};
return this.on(name, wrapper);
}
// ========================================================================
@@ -362,52 +362,52 @@ export class RpcClient<P extends ProtocolDef> {
private _handleMessage(raw: string | ArrayBuffer): void {
try {
const data = typeof raw === 'string' ? raw : new Uint8Array(raw)
const packet = this._codec.decode(data)
const type = packet[0]
const data = typeof raw === 'string' ? raw : new Uint8Array(raw);
const packet = this._codec.decode(data);
const type = packet[0];
switch (type) {
case PacketType.ApiResponse:
this._handleApiResponse(packet as [number, number, unknown])
break
this._handleApiResponse(packet as [number, number, unknown]);
break;
case PacketType.ApiError:
this._handleApiError(packet as [number, number, string, string])
break
this._handleApiError(packet as [number, number, string, string]);
break;
case PacketType.Message:
this._handleMsg(packet as [number, string, unknown])
break
this._handleMsg(packet as [number, string, unknown]);
break;
}
} catch (err) {
this._options.onError?.(err as Error)
this._options.onError?.(err as Error);
}
}
private _handleApiResponse([, id, result]: [number, number, unknown]): void {
const pending = this._pendingCalls.get(id)
const pending = this._pendingCalls.get(id);
if (pending) {
clearTimeout(pending.timer)
this._pendingCalls.delete(id)
pending.resolve(result)
clearTimeout(pending.timer);
this._pendingCalls.delete(id);
pending.resolve(result);
}
}
private _handleApiError([, id, code, message]: [number, number, string, string]): void {
const pending = this._pendingCalls.get(id)
const pending = this._pendingCalls.get(id);
if (pending) {
clearTimeout(pending.timer)
this._pendingCalls.delete(id)
pending.reject(new RpcError(code, message))
clearTimeout(pending.timer);
this._pendingCalls.delete(id);
pending.reject(new RpcError(code, message));
}
}
private _handleMsg([, path, data]: [number, string, unknown]): void {
const handlers = this._msgHandlers.get(path)
const handlers = this._msgHandlers.get(path);
if (handlers) {
for (const handler of handlers) {
try {
handler(data)
handler(data);
} catch (err) {
this._options.onError?.(err as Error)
this._options.onError?.(err as Error);
}
}
}
@@ -415,25 +415,25 @@ export class RpcClient<P extends ProtocolDef> {
private _rejectAllPending(): void {
for (const [, pending] of this._pendingCalls) {
clearTimeout(pending.timer)
pending.reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Connection closed'))
clearTimeout(pending.timer);
pending.reject(new RpcError(ErrorCode.CONNECTION_CLOSED, 'Connection closed'));
}
this._pendingCalls.clear()
this._pendingCalls.clear();
}
private _scheduleReconnect(): void {
if (this._shouldReconnect && !this._reconnectTimer) {
this._reconnectTimer = setTimeout(() => {
this._reconnectTimer = null
this.connect().catch(() => {})
}, this._reconnectInterval)
this._reconnectTimer = null;
this.connect().catch(() => {});
}, this._reconnectInterval);
}
}
private _clearReconnectTimer(): void {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null;
}
}
}
@@ -457,5 +457,5 @@ export function connect<P extends ProtocolDef>(
url: string,
options: RpcClientOptions = {}
): Promise<RpcClient<P>> {
return new RpcClient(protocol, url, options).connect()
return new RpcClient(protocol, url, options).connect();
}

View File

@@ -3,7 +3,7 @@
* @en Codec Module
*/
export type { Codec } from './types'
export { json } from './json'
export { msgpack } from './msgpack'
export { textEncode, textDecode } from './polyfill'
export type { Codec } from './types';
export { json } from './json';
export { msgpack } from './msgpack';
export { textEncode, textDecode } from './polyfill';

View File

@@ -3,9 +3,9 @@
* @en JSON Codec
*/
import type { Packet } from '../types'
import type { Codec } from './types'
import { textDecode } from './polyfill'
import type { Packet } from '../types';
import type { Codec } from './types';
import { textDecode } from './polyfill';
/**
* @zh 创建 JSON 编解码器
@@ -17,14 +17,14 @@ import { textDecode } from './polyfill'
export function json(): Codec {
return {
encode(packet: Packet): string {
return JSON.stringify(packet)
return JSON.stringify(packet);
},
decode(data: string | Uint8Array): Packet {
const str = typeof data === 'string'
? data
: textDecode(data)
return JSON.parse(str) as Packet
},
}
: textDecode(data);
return JSON.parse(str) as Packet;
}
};
}

View File

@@ -3,10 +3,10 @@
* @en MessagePack Codec
*/
import { Packr, Unpackr } from 'msgpackr'
import type { Packet } from '../types'
import type { Codec } from './types'
import { textEncode } from './polyfill'
import { Packr, Unpackr } from 'msgpackr';
import type { Packet } from '../types';
import type { Codec } from './types';
import { textEncode } from './polyfill';
/**
* @zh 创建 MessagePack 编解码器
@@ -16,19 +16,19 @@ import { textEncode } from './polyfill'
* @en Suitable for production, smaller size and faster speed
*/
export function msgpack(): Codec {
const encoder = new Packr({ structuredClone: true })
const decoder = new Unpackr({ structuredClone: true })
const encoder = new Packr({ structuredClone: true });
const decoder = new Unpackr({ structuredClone: true });
return {
encode(packet: Packet): Uint8Array {
return encoder.pack(packet)
return encoder.pack(packet);
},
decode(data: string | Uint8Array): Packet {
const buf = typeof data === 'string'
? textEncode(data)
: data
return decoder.unpack(buf) as Packet
},
}
: data;
return decoder.unpack(buf) as Packet;
}
};
}

View File

@@ -12,38 +12,38 @@
*/
function getTextEncoder(): { encode(str: string): Uint8Array } {
if (typeof TextEncoder !== 'undefined') {
return new TextEncoder()
return new TextEncoder();
}
return {
encode(str: string): Uint8Array {
const utf8: number[] = []
const utf8: number[] = [];
for (let i = 0; i < str.length; i++) {
let charCode = str.charCodeAt(i)
let charCode = str.charCodeAt(i);
if (charCode < 0x80) {
utf8.push(charCode)
utf8.push(charCode);
} else if (charCode < 0x800) {
utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f))
utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f));
} else if (charCode >= 0xd800 && charCode <= 0xdbff) {
i++
const low = str.charCodeAt(i)
charCode = 0x10000 + ((charCode - 0xd800) << 10) + (low - 0xdc00)
i++;
const low = str.charCodeAt(i);
charCode = 0x10000 + ((charCode - 0xd800) << 10) + (low - 0xdc00);
utf8.push(
0xf0 | (charCode >> 18),
0x80 | ((charCode >> 12) & 0x3f),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f)
)
);
} else {
utf8.push(
0xe0 | (charCode >> 12),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f)
)
);
}
}
return new Uint8Array(utf8)
},
}
return new Uint8Array(utf8);
}
};
}
/**
@@ -52,55 +52,55 @@ function getTextEncoder(): { encode(str: string): Uint8Array } {
*/
function getTextDecoder(): { decode(data: Uint8Array): string } {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder()
return new TextDecoder();
}
return {
decode(data: Uint8Array): string {
let str = ''
let i = 0
let str = '';
let i = 0;
while (i < data.length) {
const byte1 = data[i++]
const byte1 = data[i++];
if (byte1 < 0x80) {
str += String.fromCharCode(byte1)
str += String.fromCharCode(byte1);
} else if ((byte1 & 0xe0) === 0xc0) {
const byte2 = data[i++]
str += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f))
const byte2 = data[i++];
str += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f));
} else if ((byte1 & 0xf0) === 0xe0) {
const byte2 = data[i++]
const byte3 = data[i++]
const byte2 = data[i++];
const byte3 = data[i++];
str += String.fromCharCode(
((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)
)
);
} else if ((byte1 & 0xf8) === 0xf0) {
const byte2 = data[i++]
const byte3 = data[i++]
const byte4 = data[i++]
const byte2 = data[i++];
const byte3 = data[i++];
const byte4 = data[i++];
const codePoint =
((byte1 & 0x07) << 18) |
((byte2 & 0x3f) << 12) |
((byte3 & 0x3f) << 6) |
(byte4 & 0x3f)
const offset = codePoint - 0x10000
(byte4 & 0x3f);
const offset = codePoint - 0x10000;
str += String.fromCharCode(
0xd800 + (offset >> 10),
0xdc00 + (offset & 0x3ff)
)
);
}
}
return str
},
}
return str;
}
};
}
const encoder = getTextEncoder()
const decoder = getTextDecoder()
const encoder = getTextEncoder();
const decoder = getTextDecoder();
/**
* @zh 将字符串编码为 UTF-8 字节数组
* @en Encode string to UTF-8 byte array
*/
export function textEncode(str: string): Uint8Array {
return encoder.encode(str)
return encoder.encode(str);
}
/**
@@ -108,5 +108,5 @@ export function textEncode(str: string): Uint8Array {
* @en Decode UTF-8 byte array to string
*/
export function textDecode(data: Uint8Array): string {
return decoder.decode(data)
return decoder.decode(data);
}

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