diff --git a/docs/src/content/docs/en/modules/blueprint/custom-nodes.md b/docs/src/content/docs/en/modules/blueprint/custom-nodes.md index fba9501b..bd23d0ba 100644 --- a/docs/src/content/docs/en/modules/blueprint/custom-nodes.md +++ b/docs/src/content/docs/en/modules/blueprint/custom-nodes.md @@ -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(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' }; } } ``` diff --git a/docs/src/content/docs/en/modules/blueprint/examples.md b/docs/src/content/docs/en/modules/blueprint/examples.md index bb7d2274..625dabc3 100644 --- a/docs/src/content/docs/en/modules/blueprint/examples.md +++ b/docs/src/content/docs/en/modules/blueprint/examples.md @@ -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(node.id, 'target'); - const amount = context.getInput(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 diff --git a/docs/src/content/docs/en/modules/blueprint/index.md b/docs/src/content/docs/en/modules/blueprint/index.md index 3210b7ca..c16edfba 100644 --- a/docs/src/content/docs/en/modules/blueprint/index.md +++ b/docs/src/content/docs/en/modules/blueprint/index.md @@ -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(nodeId: string, pinName: string): T; - setOutput(nodeId: string, pinName: string, value: unknown): void; - getVariable(name: string): T; - setVariable(name: string, value: unknown): void; -} -``` - -### Execution Result - -```typescript -interface ExecutionResult { - outputs?: Record; // 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(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 { - 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 diff --git a/docs/src/content/docs/en/modules/blueprint/nodes.md b/docs/src/content/docs/en/modules/blueprint/nodes.md index af087b6e..540c11dc 100644 --- a/docs/src/content/docs/en/modules/blueprint/nodes.md +++ b/docs/src/content/docs/en/modules/blueprint/nodes.md @@ -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 diff --git a/docs/src/content/docs/en/modules/blueprint/vm.md b/docs/src/content/docs/en/modules/blueprint/vm.md index 52545d27..29bf55e6 100644 --- a/docs/src/content/docs/en/modules/blueprint/vm.md +++ b/docs/src/content/docs/en/modules/blueprint/vm.md @@ -45,7 +45,7 @@ interface ExecutionContext { time: number; // Total runtime // Get input value - getInput(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 diff --git a/docs/src/content/docs/modules/blueprint/custom-nodes.md b/docs/src/content/docs/modules/blueprint/custom-nodes.md index c799afb8..66000ec7 100644 --- a/docs/src/content/docs/modules/blueprint/custom-nodes.md +++ b/docs/src/content/docs/modules/blueprint/custom-nodes.md @@ -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(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' }; } } ``` diff --git a/docs/src/content/docs/modules/blueprint/examples.md b/docs/src/content/docs/modules/blueprint/examples.md index 646acd68..9c330694 100644 --- a/docs/src/content/docs/modules/blueprint/examples.md +++ b/docs/src/content/docs/modules/blueprint/examples.md @@ -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(node.id, 'target'); - const amount = context.getInput(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 节点输出中间值 // 在编辑器中设置断点 diff --git a/docs/src/content/docs/modules/blueprint/index.md b/docs/src/content/docs/modules/blueprint/index.md index 94c5b3e4..1b9d637d 100644 --- a/docs/src/content/docs/modules/blueprint/index.md +++ b/docs/src/content/docs/modules/blueprint/index.md @@ -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 游戏逻辑示例 diff --git a/docs/src/content/docs/modules/blueprint/nodes.md b/docs/src/content/docs/modules/blueprint/nodes.md index 40e37a89..ec98341c 100644 --- a/docs/src/content/docs/modules/blueprint/nodes.md +++ b/docs/src/content/docs/modules/blueprint/nodes.md @@ -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 方法 diff --git a/docs/src/content/docs/modules/blueprint/vm.md b/docs/src/content/docs/modules/blueprint/vm.md index 9e3139a5..aa5353ae 100644 --- a/docs/src/content/docs/modules/blueprint/vm.md +++ b/docs/src/content/docs/modules/blueprint/vm.md @@ -45,7 +45,7 @@ interface ExecutionContext { time: number; // 总运行时间 // 获取输入值 - getInput(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 }); +} ``` ## 序列化 diff --git a/packages/framework/blueprint/src/index.ts b/packages/framework/blueprint/src/index.ts index 552fdad4..78143ff6 100644 --- a/packages/framework/blueprint/src/index.ts +++ b/packages/framework/blueprint/src/index.ts @@ -14,23 +14,27 @@ * - Auto component node generation (using decorators) * - Runtime blueprint execution * - * @example 基础使用 | Basic usage: + * @example 基础使用 | Basic Usage: * ```typescript - * import { - * createBlueprintSystem, - * registerAllComponentNodes - * } from '@esengine/blueprint'; + * import { BlueprintSystem, BlueprintComponent } from '@esengine/blueprint'; + * import { Scene, Core } from '@esengine/ecs-framework'; * - * // 注册所有标记的组件节点 | Register all marked component nodes - * registerAllComponentNodes(); + * // 创建场景并添加蓝图系统 + * const scene = new Scene(); + * scene.addSystem(new BlueprintSystem()); + * Core.setScene(scene); * - * // 创建蓝图系统 | Create blueprint system - * const blueprintSystem = createBlueprintSystem(scene); + * // 为实体添加蓝图 + * const entity = scene.createEntity('Player'); + * const blueprint = new BlueprintComponent(); + * blueprint.blueprintAsset = await loadBlueprintAsset('player.bp'); + * entity.addComponent(blueprint); * ``` * - * @example 标记组件 | Mark components: + * @example 标记组件 | Mark Components: * ```typescript * import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint'; + * import { Component, ECSComponent } from '@esengine/ecs-framework'; * * @ECSComponent('Health') * @BlueprintExpose({ displayName: '生命值' }) @@ -69,19 +73,8 @@ 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 { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint'; // Re-export registry for convenience diff --git a/packages/framework/blueprint/src/runtime/BlueprintComponent.ts b/packages/framework/blueprint/src/runtime/BlueprintComponent.ts index 7595b0e7..96b33ae2 100644 --- a/packages/framework/blueprint/src/runtime/BlueprintComponent.ts +++ b/packages/framework/blueprint/src/runtime/BlueprintComponent.ts @@ -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; } } diff --git a/packages/framework/blueprint/src/runtime/BlueprintSystem.ts b/packages/framework/blueprint/src/runtime/BlueprintSystem.ts index 083a74cd..572eb052 100644 --- a/packages/framework/blueprint/src/runtime/BlueprintSystem.ts +++ b/packages/framework/blueprint/src/runtime/BlueprintSystem.ts @@ -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 -): 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 -): 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(); + } } }