docs: restructure documentation with modular sub-pages (#363)
* docs: split Entity docs into sub-modules and fix Starlight CI - Split monolithic entity.md into 4 focused sub-documents: - guide/entity/index.md - Overview and basic concepts - guide/entity/component-operations.md - Component API operations - guide/entity/entity-handle.md - EntityHandle system for safe references - guide/entity/lifecycle.md - Lifecycle and persistence management - Created bilingual versions (Chinese and English) - Updated sidebar configuration in astro.config.mjs - Fixed CI workflow for Starlight migration: - Updated docs.yml to upload from docs/dist instead of .vitepress/dist - Updated package.json scripts to use pnpm filter for docs - Added docs directory to pnpm-workspace.yaml - Renamed docs package to @esengine/docs - Documented missing Entity APIs: - createComponent() method - addComponents() batch method - getComponentByType() with inheritance support - markDirty() for change detection * docs: split Network docs and fix API errors - Split network module into focused sub-documents: - modules/network/index.md - Overview and quick start - modules/network/client.md - Client-side usage - modules/network/server.md - Server-side GameServer/Room - modules/network/sync.md - Interpolation and prediction - modules/network/api.md - Complete API reference - Fixed incorrect API documentation: - localClientId → clientId - ENetworkState enum values (strings → numbers) - connect() method signature - Removed non-existent localPlayerId property - Fixed onConnected callback signature - Created bilingual versions (Chinese and English) - Updated sidebar configuration - Updated pnpm-lock.yaml for docs workspace * docs(worker-system): split into focused sub-modules Split 773-line worker-system.md into 5 focused documents: - index.md: Core features and quick start - configuration.md: IWorkerSystemConfig and processing modes - examples.md: Complete particle physics implementation - wechat.md: WeChat Mini Game limitations and solutions - best-practices.md: Performance optimization tips Updated sidebar config to reflect new structure. Created both Chinese and English versions. * docs(scene): split into focused sub-modules Split 666-line scene.md into 7 focused documents: - index.md: Overview and quick start - lifecycle.md: Scene lifecycle methods - entity-management.md: Entity creation, find, destroy - system-management.md: System add, remove, control - events.md: Event system usage - debugging.md: Stats, performance monitoring - best-practices.md: Design patterns and examples Updated sidebar config to reflect new structure. Created both Chinese and English versions. * docs(plugin-system): split into focused sub-modules Split 645-line plugin-system.md into 7 focused documents: - index.md: Overview and quick start - development.md: IPlugin interface and lifecycle - services-systems.md: Register services and add systems - dependencies.md: Dependency management - management.md: Plugin management via Core/PluginManager - examples.md: Complete plugin examples - best-practices.md: Design guidelines and FAQ Updated sidebar config to reflect new structure. Created both Chinese and English versions. * docs(behavior-tree): add English docs and expand sidebar navigation - Add 12 English behavior-tree documentation pages - Update sidebar config to show behavior-tree sub-navigation - Include: overview, getting-started, core-concepts, custom-actions, editor-guide, editor-workflow, asset-management, advanced-usage, best-practices, cocos-integration, laya-integration, nodejs-usage * docs(modules): split spatial and timer module docs Spatial module (602 lines -> 5 files): - index.md: Overview and quick start - spatial-index.md: Grid index, range queries, raycasting API - aoi.md: Area of Interest management - examples.md: Combat, MMO sync, AI perception examples - utilities.md: Geometry detection, performance tips Timer module (481 lines -> 4 files): - index.md: Overview and core concepts - api.md: Complete timer and cooldown API - examples.md: Skill cooldowns, DOT, buff systems - best-practices.md: Usage tips, ECS integration Also includes English versions and sidebar navigation updates. * docs: split FSM, pathfinding, blueprint, procgen module docs - FSM: Split into index, api, examples (3 files) - Pathfinding: Split into index, grid-map, navmesh, smoothing, examples (5 files) - Blueprint: Split into index, vm, custom-nodes, nodes, composition, examples (6 files) - Procgen: Split into index, noise, random, sampling, examples (5 files) - Added English versions for all split modules - Updated sidebar navigation with sub-menus for all modules
This commit is contained in:
@@ -1,645 +0,0 @@
|
||||
---
|
||||
title: "Plugin System"
|
||||
---
|
||||
|
||||
The plugin system allows you to extend ECS Framework functionality in a modular way. Through plugins, you can encapsulate specific features (such as network synchronization, physics engines, debugging tools, etc.) and reuse them across multiple projects.
|
||||
|
||||
## Overview
|
||||
|
||||
### What is a Plugin
|
||||
|
||||
A plugin is a class that implements the `IPlugin` interface and can be dynamically installed into the framework at runtime. Plugins can:
|
||||
|
||||
- Register custom services to the service container
|
||||
- Add systems to scenes
|
||||
- Register custom components
|
||||
- Extend framework functionality
|
||||
|
||||
### Advantages of Plugins
|
||||
|
||||
- **Modular**: Encapsulate functionality as independent modules, improving code maintainability
|
||||
- **Reusable**: Same plugin can be used across multiple projects
|
||||
- **Decoupled**: Core framework separated from extended functionality
|
||||
- **Hot-swappable**: Dynamically install and uninstall plugins at runtime
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Creating Your First Plugin
|
||||
|
||||
Create a simple debug plugin:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// Can register services, add systems, etc. here
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Installing a Plugin
|
||||
|
||||
Use `Core.installPlugin()` to install a plugin:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Install plugin
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### Uninstalling a Plugin
|
||||
|
||||
```typescript
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### Getting Plugin Instance
|
||||
|
||||
```typescript
|
||||
// Get installed plugin
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Development
|
||||
|
||||
### IPlugin Interface
|
||||
|
||||
All plugins must implement the `IPlugin` interface:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// Unique plugin name
|
||||
readonly name: string;
|
||||
|
||||
// Plugin version (semver recommended)
|
||||
readonly version: string;
|
||||
|
||||
// Dependencies on other plugins (optional)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// Called when plugin is installed
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// Called when plugin is uninstalled
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
#### install Method
|
||||
|
||||
Called when the plugin is installed, used to initialize the plugin:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. Access current scene
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. Add systems
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. Other initialization logic
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall Method
|
||||
|
||||
Called when the plugin is uninstalled, used to clean up resources:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clean up service
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
// Release other resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Plugins
|
||||
|
||||
Plugin `install` and `uninstall` methods both support async:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// Async resource loading
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// Initialize service with loaded config
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// Async cleanup
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// Save plugin state
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### Registering Services
|
||||
|
||||
Plugins can register their own services to the service container:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// Resolve and use service
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service container automatically calls service's dispose method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Systems
|
||||
|
||||
Plugins can add custom systems to scenes:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Physics simulation logic
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove system
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Declaring Dependencies
|
||||
|
||||
Plugins can declare dependencies on other plugins:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// Declare dependency on base physics plugin
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Can safely use services provided by physics-plugin
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Checking
|
||||
|
||||
The framework automatically checks dependency relationships and throws errors if dependencies are unmet:
|
||||
|
||||
```typescript
|
||||
// Error: physics-plugin not installed
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// Correct: Install dependency first
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### Uninstall Order
|
||||
|
||||
The framework checks dependency relationships, preventing uninstallation of plugins that other plugins depend on:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// Error: physics-plugin is required by advanced-physics
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// Correct: Uninstall dependent plugin first
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## Plugin Management
|
||||
|
||||
### Managing via Core
|
||||
|
||||
The Core class provides convenient plugin management methods:
|
||||
|
||||
```typescript
|
||||
// Install plugin
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Get plugin instance
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### Managing via PluginManager
|
||||
|
||||
You can also use the PluginManager service directly:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// Get all plugins
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// Get plugin metadata
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// Get all plugin metadata
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Plugin Examples
|
||||
|
||||
### Network Sync Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// Handle network messages
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// Auto connect
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service will auto dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Analysis Plugin
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// Periodically output performance report
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- Plugin names use lowercase letters and hyphens: `my-awesome-plugin`
|
||||
- Version numbers follow semantic versioning: `1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // Good
|
||||
readonly version = '1.0.0'; // Good
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Cleanup
|
||||
|
||||
Always clean up all resources created by the plugin in `uninstall`:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Add timer
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// Add event listener
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clean up timer
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Handle errors properly in plugins to avoid affecting the entire application:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// Operation that might fail
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // Re-throw to let framework know installation failed
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// Cleanup failure shouldn't block uninstall
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// Load configuration
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Allow users to configure plugin behavior:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Plugin Installation Failed
|
||||
|
||||
**Problem**: Plugin throws error during installation
|
||||
|
||||
**Causes**:
|
||||
- Dependencies not met
|
||||
- Exception in install method
|
||||
- Service registration conflict
|
||||
|
||||
**Solutions**:
|
||||
1. Check if dependencies are installed
|
||||
2. Check error logs
|
||||
3. Ensure service names don't conflict
|
||||
|
||||
### Plugin Still Has Side Effects After Uninstall
|
||||
|
||||
**Problem**: After uninstalling plugin, plugin functionality is still running
|
||||
|
||||
**Cause**: Resources not properly cleaned up in uninstall method
|
||||
|
||||
**Solution**: Ensure cleanup in uninstall:
|
||||
- Timers
|
||||
- Event listeners
|
||||
- WebSocket connections
|
||||
- System references
|
||||
|
||||
### When to Use Plugins
|
||||
|
||||
**Good for plugins**:
|
||||
- Optional features (debug tools, performance analysis)
|
||||
- Third-party integrations (network libraries, physics engines)
|
||||
- Functionality modules reused across projects
|
||||
|
||||
**Not suitable for plugins**:
|
||||
- Core game logic
|
||||
- Simple utility classes
|
||||
- Project-specific features
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Service Container](./service-container/) - Using service container in plugins
|
||||
- [System Architecture](./system/) - Adding systems in plugins
|
||||
- [Quick Start](./getting-started/) - Core initialization and basic usage
|
||||
151
docs/src/content/docs/en/guide/plugin-system/best-practices.md
Normal file
151
docs/src/content/docs/en/guide/plugin-system/best-practices.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Plugin design guidelines and common issues"
|
||||
---
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
// Use lowercase letters and hyphens
|
||||
readonly name = 'my-awesome-plugin'; // OK
|
||||
|
||||
// Follow semantic versioning
|
||||
readonly version = '1.0.0'; // OK
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Cleanup
|
||||
|
||||
Always clean up all resources created by the plugin in `uninstall`:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Add timer
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// Add event listener
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clear timer
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // Re-throw to let framework know installation failed
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// Don't block uninstall even if cleanup fails
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() { /* ... */ }
|
||||
private async cleanup() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Allow users to configure plugin behavior:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Plugin Installation Failed
|
||||
|
||||
**Causes**:
|
||||
- Dependencies not satisfied
|
||||
- Exception in install method
|
||||
- Service registration conflict
|
||||
|
||||
**Solutions**:
|
||||
1. Check if dependencies are installed
|
||||
2. Review error logs
|
||||
3. Ensure service names don't conflict
|
||||
|
||||
### Side Effects After Uninstall
|
||||
|
||||
**Cause**: Resources not properly cleaned in uninstall
|
||||
|
||||
**Solution**: Ensure uninstall cleans up:
|
||||
- Timers
|
||||
- Event listeners
|
||||
- WebSocket connections
|
||||
- System references
|
||||
|
||||
### When to Use Plugins
|
||||
|
||||
| Good for Plugins | Not Good for Plugins |
|
||||
|------------------|---------------------|
|
||||
| Optional features (debug tools, profiling) | Core game logic |
|
||||
| Third-party integration (network libs, physics) | Simple utilities |
|
||||
| Cross-project reusable modules | Project-specific features |
|
||||
106
docs/src/content/docs/en/guide/plugin-system/dependencies.md
Normal file
106
docs/src/content/docs/en/guide/plugin-system/dependencies.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: "Dependency Management"
|
||||
description: "Declare and check plugin dependencies"
|
||||
---
|
||||
|
||||
## Declaring Dependencies
|
||||
|
||||
Plugins can declare dependencies on other plugins:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// Declare dependency on base physics plugin
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Can safely use services from physics-plugin
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Checking
|
||||
|
||||
The framework automatically checks dependencies and throws an error if not satisfied:
|
||||
|
||||
```typescript
|
||||
// Error: physics-plugin not installed
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// Correct: install dependency first
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
## Uninstall Order
|
||||
|
||||
The framework checks dependencies to prevent uninstalling plugins required by others:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// Error: physics-plugin is required by advanced-physics
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// Correct: uninstall dependent plugin first
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## Dependency Graph Example
|
||||
|
||||
```
|
||||
physics-plugin (base)
|
||||
↑
|
||||
advanced-physics (depends on physics-plugin)
|
||||
↑
|
||||
game-physics (depends on advanced-physics)
|
||||
```
|
||||
|
||||
Install order: `physics-plugin` → `advanced-physics` → `game-physics`
|
||||
|
||||
Uninstall order: `game-physics` → `advanced-physics` → `physics-plugin`
|
||||
|
||||
## Multiple Dependencies
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
// Declare multiple dependencies
|
||||
readonly dependencies = [
|
||||
'physics-plugin',
|
||||
'network-plugin',
|
||||
'audio-plugin'
|
||||
] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// All dependencies are available
|
||||
const physics = services.resolve(PhysicsService);
|
||||
const network = services.resolve(NetworkService);
|
||||
const audio = services.resolve(AudioService);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
```
|
||||
139
docs/src/content/docs/en/guide/plugin-system/development.md
Normal file
139
docs/src/content/docs/en/guide/plugin-system/development.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "Plugin Development"
|
||||
description: "IPlugin interface and lifecycle"
|
||||
---
|
||||
|
||||
## IPlugin Interface
|
||||
|
||||
All plugins must implement the `IPlugin` interface:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// Unique plugin name
|
||||
readonly name: string;
|
||||
|
||||
// Plugin version (semver recommended)
|
||||
readonly version: string;
|
||||
|
||||
// Dependencies on other plugins (optional)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// Called when plugin is installed
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// Called when plugin is uninstalled
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
### install Method
|
||||
|
||||
Called when the plugin is installed, used for initialization:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. Access current scene
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. Add systems
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. Other initialization
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### uninstall Method
|
||||
|
||||
Called when the plugin is uninstalled, used for cleanup:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup service
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
// Release other resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Plugins
|
||||
|
||||
Both `install` and `uninstall` methods support async:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// Async load resources
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// Initialize service with loaded config
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// Async cleanup
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// Save plugin state
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
## Lifecycle Flow
|
||||
|
||||
```
|
||||
Install: Core.installPlugin(plugin)
|
||||
↓
|
||||
Dependency check: Verify dependencies are satisfied
|
||||
↓
|
||||
Call install(): Register services, add systems
|
||||
↓
|
||||
State update: Mark as installed
|
||||
|
||||
Uninstall: Core.uninstallPlugin(name)
|
||||
↓
|
||||
Dependency check: Verify not required by other plugins
|
||||
↓
|
||||
Call uninstall(): Cleanup resources
|
||||
↓
|
||||
State update: Remove from plugin list
|
||||
```
|
||||
188
docs/src/content/docs/en/guide/plugin-system/examples.md
Normal file
188
docs/src/content/docs/en/guide/plugin-system/examples.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
title: "Example Plugins"
|
||||
description: "Complete plugin implementation examples"
|
||||
---
|
||||
|
||||
## Network Sync Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// Handle network messages
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// Auto connect
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service will auto-dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Analysis Plugin
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// Periodic performance report
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Tools Plugin
|
||||
|
||||
```typescript
|
||||
class DebugToolsPlugin implements IPlugin {
|
||||
readonly name = 'debug-tools';
|
||||
readonly version = '1.0.0';
|
||||
private debugUI?: DebugUI;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Create debug UI
|
||||
this.debugUI = new DebugUI();
|
||||
this.debugUI.mount(document.body);
|
||||
|
||||
// Register hotkey
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
// Add debug system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new DebugRenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove UI
|
||||
if (this.debugUI) {
|
||||
this.debugUI.unmount();
|
||||
this.debugUI = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F12') {
|
||||
this.debugUI?.toggle();
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Audio Plugin
|
||||
|
||||
```typescript
|
||||
class AudioPlugin implements IPlugin {
|
||||
readonly name = 'audio';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: { volume: number }) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const audioService = new AudioService(this.config);
|
||||
services.registerInstance(AudioService, audioService);
|
||||
|
||||
// Add audio system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new AudioSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Stop all audio
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.stopAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AudioPlugin({ volume: 0.8 }));
|
||||
```
|
||||
|
||||
## Input Manager Plugin
|
||||
|
||||
```typescript
|
||||
class InputPlugin implements IPlugin {
|
||||
readonly name = 'input';
|
||||
readonly version = '1.0.0';
|
||||
private inputManager?: InputManager;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.inputManager = new InputManager();
|
||||
services.registerInstance(InputManager, this.inputManager);
|
||||
|
||||
// Bind default keys
|
||||
this.inputManager.bind('jump', ['Space', 'KeyW']);
|
||||
this.inputManager.bind('attack', ['MouseLeft', 'KeyJ']);
|
||||
|
||||
// Add input system
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new InputSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
if (this.inputManager) {
|
||||
this.inputManager.dispose();
|
||||
this.inputManager = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
85
docs/src/content/docs/en/guide/plugin-system/index.md
Normal file
85
docs/src/content/docs/en/guide/plugin-system/index.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Plugin System"
|
||||
description: "Extend ECS Framework in a modular way"
|
||||
---
|
||||
|
||||
The plugin system allows you to extend ECS Framework functionality in a modular way. Through plugins, you can encapsulate specific features (like network sync, physics engines, debug tools) and reuse them across multiple projects.
|
||||
|
||||
## What is a Plugin
|
||||
|
||||
A plugin is a class that implements the `IPlugin` interface and can be dynamically installed into the framework at runtime. Plugins can:
|
||||
|
||||
- Register custom services to the service container
|
||||
- Add systems to scenes
|
||||
- Register custom components
|
||||
- Extend framework functionality
|
||||
|
||||
## Plugin Benefits
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Modular** | Encapsulate functionality as independent modules |
|
||||
| **Reusable** | Use the same plugin across multiple projects |
|
||||
| **Decoupled** | Separate core framework from extensions |
|
||||
| **Hot-swappable** | Dynamically install and uninstall at runtime |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Create a Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Install a Plugin
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Install plugin
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### Uninstall a Plugin
|
||||
|
||||
```typescript
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### Get Plugin Instance
|
||||
|
||||
```typescript
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Development](./development/) - IPlugin interface and lifecycle
|
||||
- [Services & Systems](./services-systems/) - Register services and add systems
|
||||
- [Dependencies](./dependencies/) - Declare and check dependencies
|
||||
- [Management](./management/) - Manage via Core and PluginManager
|
||||
- [Examples](./examples/) - Complete examples
|
||||
- [Best Practices](./best-practices/) - Design guidelines
|
||||
93
docs/src/content/docs/en/guide/plugin-system/management.md
Normal file
93
docs/src/content/docs/en/guide/plugin-system/management.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "Plugin Management"
|
||||
description: "Manage plugins via Core and PluginManager"
|
||||
---
|
||||
|
||||
## Via Core
|
||||
|
||||
Core class provides convenient plugin management methods:
|
||||
|
||||
```typescript
|
||||
// Install plugin
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Get plugin instance
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
## Via PluginManager
|
||||
|
||||
You can also use the PluginManager service directly:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// Get all plugins
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// Get plugin metadata
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// Get all plugin metadata
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Static Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `installPlugin(plugin)` | Install plugin |
|
||||
| `uninstallPlugin(name)` | Uninstall plugin |
|
||||
| `isPluginInstalled(name)` | Check if installed |
|
||||
| `getPlugin(name)` | Get plugin instance |
|
||||
|
||||
### PluginManager Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `getAllPlugins()` | Get all plugins |
|
||||
| `getMetadata(name)` | Get plugin metadata |
|
||||
| `getAllMetadata()` | Get all plugin metadata |
|
||||
|
||||
## Plugin States
|
||||
|
||||
```typescript
|
||||
enum PluginState {
|
||||
Pending = 'pending',
|
||||
Installing = 'installing',
|
||||
Installed = 'installed',
|
||||
Uninstalling = 'uninstalling',
|
||||
Failed = 'failed'
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata Information
|
||||
|
||||
```typescript
|
||||
interface PluginMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
state: PluginState;
|
||||
dependencies?: string[];
|
||||
installedAt?: number;
|
||||
error?: Error;
|
||||
}
|
||||
```
|
||||
133
docs/src/content/docs/en/guide/plugin-system/services-systems.md
Normal file
133
docs/src/content/docs/en/guide/plugin-system/services-systems.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Services & Systems"
|
||||
description: "Register services and add systems in plugins"
|
||||
---
|
||||
|
||||
## Registering Services
|
||||
|
||||
Plugins can register their own services to the service container:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// Resolve and use service
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service container will auto-call service's dispose method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service Registration Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `registerSingleton(Type)` | Register singleton service |
|
||||
| `registerInstance(Type, instance)` | Register existing instance |
|
||||
| `registerTransient(Type)` | Create new instance per resolve |
|
||||
|
||||
## Adding Systems
|
||||
|
||||
Plugins can add custom systems to scenes:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Physics simulation logic
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove system
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Combined Usage
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private systems: EntitySystem[] = [];
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(ScoreService);
|
||||
services.registerSingleton(AudioService);
|
||||
|
||||
// 2. Add systems
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
const systems = [
|
||||
new InputSystem(),
|
||||
new MovementSystem(),
|
||||
new ScoringSystem()
|
||||
];
|
||||
|
||||
systems.forEach(system => {
|
||||
scene.addSystem(system);
|
||||
this.systems.push(system);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove all systems
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
this.systems.forEach(system => {
|
||||
scene.removeSystem(system);
|
||||
});
|
||||
}
|
||||
this.systems = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,368 +0,0 @@
|
||||
---
|
||||
title: "scene"
|
||||
---
|
||||
|
||||
# Scene Management
|
||||
|
||||
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components. Scenes provide a complete ECS runtime environment.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Scene is the core container of the ECS framework, providing:
|
||||
- Entity creation, management, and destruction
|
||||
- System registration and execution scheduling
|
||||
- Component storage and querying
|
||||
- Event system support
|
||||
- Performance monitoring and debugging information
|
||||
|
||||
## Scene Management Options
|
||||
|
||||
ECS Framework provides two scene management approaches:
|
||||
|
||||
1. **[SceneManager](./scene-manager)** - Suitable for 95% of game applications
|
||||
- Single-player games, simple multiplayer games, mobile games
|
||||
- Lightweight, simple and intuitive API
|
||||
- Supports scene transitions
|
||||
|
||||
2. **[WorldManager](./world-manager)** - Suitable for advanced multi-world isolation scenarios
|
||||
- MMO game servers, game room systems
|
||||
- Multi-World management, each World can contain multiple scenes
|
||||
- Completely isolated independent environments
|
||||
|
||||
This document focuses on the usage of the Scene class itself. For detailed information about scene managers, please refer to the corresponding documentation.
|
||||
|
||||
## Creating a Scene
|
||||
|
||||
### Inheriting the Scene Class
|
||||
|
||||
**Recommended: Inherit the Scene class to create custom scenes**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Set scene name
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// Create initial entities
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// Create player
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// Create enemies
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
// Logic when scene starts
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("Game scene unloaded");
|
||||
// Cleanup logic when scene unloads
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scene Configuration
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scene Lifecycle
|
||||
|
||||
Scene provides complete lifecycle management:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Scene initialization: setup systems and initial entities
|
||||
console.log("Scene initializing");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// Scene starts running: game logic begins execution
|
||||
console.log("Scene starting");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Scene unloading: cleanup resources
|
||||
console.log("Scene unloading");
|
||||
}
|
||||
}
|
||||
|
||||
// Using scenes (lifecycle automatically managed by framework)
|
||||
const scene = new ExampleScene();
|
||||
// Scene's initialize(), begin(), update(), end() are automatically called by the framework
|
||||
```
|
||||
|
||||
**Lifecycle Methods**:
|
||||
|
||||
1. `initialize()` - Scene initialization, setup systems and initial entities
|
||||
2. `begin()` / `onStart()` - Scene starts running
|
||||
3. `update()` - Per-frame update (called by scene manager)
|
||||
4. `end()` / `unload()` - Scene unloading, cleanup resources
|
||||
|
||||
## Entity Management
|
||||
|
||||
### Creating Entities
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// Create single entity
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// Batch create entities (high performance)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to batch-created entities
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Finding Entities
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// Find by name
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // Alias method
|
||||
|
||||
// Find by ID
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// Find by tag
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // Alias method
|
||||
|
||||
if (player) {
|
||||
console.log(`Found player: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`Found ${enemies.length} enemies`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Destroying Entities
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// Destroy all entities
|
||||
this.destroyAllEntities();
|
||||
|
||||
// Single entity destruction through the entity itself
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // Entity is automatically removed from the scene
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
### Adding and Removing Systems
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// Set system update order
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// Add more systems
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// Get system
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// Remove system
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event System
|
||||
|
||||
Scene has a built-in type-safe event system:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died event');
|
||||
// Handle player death
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('Enemy spawned event');
|
||||
// Handle enemy spawn
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete event');
|
||||
// Handle level completion
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// Send event (synchronous)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "This is a custom event",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send event (asynchronous)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "Async event data"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Scene Responsibility Separation
|
||||
|
||||
```typescript
|
||||
// Good scene design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// Only handles inventory logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, inventory, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Proper System Organization
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems by function and dependencies
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Resource Management
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// Load resources needed by the scene
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// Load texture
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// Load sound
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [SceneManager](./scene-manager) - Simple scene management for most games
|
||||
- Learn about [WorldManager](./world-manager) - For scenarios requiring multi-world isolation
|
||||
- Learn about [Persistent Entity](./persistent-entity) - Keep entities across scene transitions (v2.3.0+)
|
||||
|
||||
Scene is the core container of the ECS framework. Proper scene management makes your game architecture clearer, more modular, and easier to maintain.
|
||||
179
docs/src/content/docs/en/guide/scene/best-practices.md
Normal file
179
docs/src/content/docs/en/guide/scene/best-practices.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Scene design patterns and complete examples"
|
||||
---
|
||||
|
||||
## Scene Responsibility Separation
|
||||
|
||||
```typescript
|
||||
// Good scene design - clear responsibilities
|
||||
class MenuScene extends Scene {
|
||||
// Only handles menu-related logic
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// Only handles gameplay logic
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// Only handles inventory logic
|
||||
}
|
||||
|
||||
// Avoid this design - mixed responsibilities
|
||||
class MegaScene extends Scene {
|
||||
// Contains menu, game, inventory, and all other logic
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Management
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup resources
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('Scene resources cleaned up');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any { return null; }
|
||||
private loadSound(path: string): any { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
## Initialization Order
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. First set scene configuration
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. Then add systems (by dependency order)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. Create entities last
|
||||
this.createEntities();
|
||||
|
||||
// 4. Setup event listeners
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void { /* ... */ }
|
||||
private setupEvents(): void { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// Define components
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// Define system
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define scene
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// Create player
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// Create enemies
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('Player died!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('Game scene started');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('Game scene unloaded');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Use scene
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| Single Responsibility | Each scene handles one game state |
|
||||
| Resource Cleanup | Clean up all resources in `unload()` |
|
||||
| System Order | Add systems: Input → Logic → Render |
|
||||
| Event Decoupling | Use event system for scene communication |
|
||||
| Layered Initialization | Config → Systems → Entities → Events |
|
||||
124
docs/src/content/docs/en/guide/scene/debugging.md
Normal file
124
docs/src/content/docs/en/guide/scene/debugging.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: "Debugging & Monitoring"
|
||||
description: "Scene statistics, performance monitoring and debugging"
|
||||
---
|
||||
|
||||
Scene includes complete debugging and performance monitoring features.
|
||||
|
||||
## Scene Statistics
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`Entity count: ${stats.entityCount}`);
|
||||
console.log(`System count: ${stats.processorCount}`);
|
||||
console.log('Component storage stats:', stats.componentStorageStats);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Information
|
||||
|
||||
```typescript
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('Scene debug info:', debugInfo);
|
||||
|
||||
// Display all entity info
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`Entity ${entity.name}(${entity.id}): ${entity.componentCount} components`);
|
||||
console.log('Component types:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// Display all system info
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`System ${processor.name}: processing ${processor.entityCount} entities`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// Get performance data
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('Frame time:', perfData.frameTime);
|
||||
console.log('Entity update time:', perfData.entityUpdateTime);
|
||||
console.log('System update time:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// Get performance report
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('Performance report:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### getStats()
|
||||
|
||||
Returns scene statistics:
|
||||
|
||||
```typescript
|
||||
interface SceneStats {
|
||||
entityCount: number;
|
||||
processorCount: number;
|
||||
componentStorageStats: ComponentStorageStats;
|
||||
}
|
||||
```
|
||||
|
||||
### getDebugInfo()
|
||||
|
||||
Returns detailed debug information:
|
||||
|
||||
```typescript
|
||||
interface DebugInfo {
|
||||
entities: EntityDebugInfo[];
|
||||
processors: ProcessorDebugInfo[];
|
||||
}
|
||||
|
||||
interface EntityDebugInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
componentCount: number;
|
||||
componentTypes: string[];
|
||||
}
|
||||
|
||||
interface ProcessorDebugInfo {
|
||||
name: string;
|
||||
entityCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
### performanceMonitor
|
||||
|
||||
Performance monitor interface:
|
||||
|
||||
```typescript
|
||||
interface PerformanceMonitor {
|
||||
getPerformanceData(): PerformanceData;
|
||||
generateReport(): string;
|
||||
}
|
||||
|
||||
interface PerformanceData {
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
entityUpdateTime: number;
|
||||
systemUpdateTime: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. **Debug mode** - Enable with `Core.create({ debug: true })`
|
||||
2. **Performance analysis** - Call `getStats()` periodically
|
||||
3. **Memory monitoring** - Check `componentStorageStats` for issues
|
||||
4. **System performance** - Use `performanceMonitor` to identify slow systems
|
||||
125
docs/src/content/docs/en/guide/scene/entity-management.md
Normal file
125
docs/src/content/docs/en/guide/scene/entity-management.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: "Entity Management"
|
||||
description: "Entity creation, finding and destruction in scenes"
|
||||
---
|
||||
|
||||
Scene provides complete entity management APIs for creating, finding, and destroying entities.
|
||||
|
||||
## Creating Entities
|
||||
|
||||
### Single Entity
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// Create named entity
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Creation
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createBullets(): void {
|
||||
// Batch create entities (high performance)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// Add components to batch-created entities
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Finding Entities
|
||||
|
||||
### By Name
|
||||
|
||||
```typescript
|
||||
// Find by name (returns first match)
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // Alias
|
||||
|
||||
if (player) {
|
||||
console.log(`Found player: ${player.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### By ID
|
||||
|
||||
```typescript
|
||||
// Find by unique ID
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
if (entity) {
|
||||
console.log(`Found entity: ${entity.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
### By Tag
|
||||
|
||||
```typescript
|
||||
// Find by tag (returns array)
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // Alias
|
||||
|
||||
console.log(`Found ${enemies.length} enemies`);
|
||||
```
|
||||
|
||||
## Destroying Entities
|
||||
|
||||
### Single Entity
|
||||
|
||||
```typescript
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // Entity is automatically removed from scene
|
||||
}
|
||||
```
|
||||
|
||||
### All Entities
|
||||
|
||||
```typescript
|
||||
// Destroy all entities in scene
|
||||
this.destroyAllEntities();
|
||||
```
|
||||
|
||||
## Entity Queries
|
||||
|
||||
Scene provides a component query system:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Create test entities
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// Query through QuerySystem
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`Found ${entities.length} entities with Transform and Velocity`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `createEntity(name)` | `Entity` | Create single entity |
|
||||
| `createEntities(count, prefix)` | `Entity[]` | Batch create entities |
|
||||
| `findEntity(name)` | `Entity \| undefined` | Find by name |
|
||||
| `findEntityById(id)` | `Entity \| undefined` | Find by ID |
|
||||
| `findEntitiesByTag(tag)` | `Entity[]` | Find by tag |
|
||||
| `destroyAllEntities()` | `void` | Destroy all entities |
|
||||
122
docs/src/content/docs/en/guide/scene/events.md
Normal file
122
docs/src/content/docs/en/guide/scene/events.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "Event System"
|
||||
description: "Scene's built-in type-safe event system"
|
||||
---
|
||||
|
||||
Scene includes a built-in type-safe event system for decoupled communication within scenes.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Listening to Events
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen to events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died event');
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('Enemy spawned event');
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete event');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sending Events
|
||||
|
||||
```typescript
|
||||
public triggerGameEvent(): void {
|
||||
// Send event (synchronous)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "This is a custom event",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send event (asynchronous)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "Async event data"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `on(event, callback)` | Listen to event |
|
||||
| `once(event, callback)` | Listen once (auto-unsubscribe) |
|
||||
| `off(event, callback)` | Stop listening |
|
||||
| `emitSync(event, data)` | Send event (synchronous) |
|
||||
| `emit(event, data)` | Send event (asynchronous) |
|
||||
| `clear()` | Clear all event listeners |
|
||||
|
||||
## Event Handling Patterns
|
||||
|
||||
### Centralized Event Management
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// Pause game logic
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// Resume game logic
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// Handle player input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cleanup Event Listeners
|
||||
|
||||
Clean up event listeners on scene unload to avoid memory leaks:
|
||||
|
||||
```typescript
|
||||
public unload(): void {
|
||||
// Clear all event listeners
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Category | Example Events |
|
||||
|----------|----------------|
|
||||
| Game State | `game_start`, `game_pause`, `game_over` |
|
||||
| Player Actions | `player_died`, `player_jump`, `player_attack` |
|
||||
| Enemy Actions | `enemy_spawned`, `enemy_killed` |
|
||||
| Level Progress | `level_start`, `level_complete` |
|
||||
| UI Interaction | `button_click`, `menu_open` |
|
||||
91
docs/src/content/docs/en/guide/scene/index.md
Normal file
91
docs/src/content/docs/en/guide/scene/index.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: "Scene"
|
||||
description: "Core container of ECS framework, managing entity, system and component lifecycles"
|
||||
---
|
||||
|
||||
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components.
|
||||
|
||||
## Core Features
|
||||
|
||||
Scene is the core container of the ECS framework, providing:
|
||||
- Entity creation, management, and destruction
|
||||
- System registration and execution scheduling
|
||||
- Component storage and querying
|
||||
- Event system support
|
||||
- Performance monitoring and debugging information
|
||||
|
||||
## Scene Management Options
|
||||
|
||||
ECS Framework provides two scene management approaches:
|
||||
|
||||
| Manager | Use Case | Features |
|
||||
|---------|----------|----------|
|
||||
| **SceneManager** | 95% of games | Lightweight, scene transitions |
|
||||
| **WorldManager** | MMO servers, room systems | Multi-World, full isolation |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Inherit Scene Class
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// Add systems
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Create initial entities
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("Game scene started");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("Game scene unloaded");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scene Configuration
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running a Scene
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Lifecycle](./lifecycle/) - Scene lifecycle methods
|
||||
- [Entity Management](./entity-management/) - Create, find, destroy entities
|
||||
- [System Management](./system-management/) - System control
|
||||
- [Events](./events/) - Scene event communication
|
||||
- [Debugging](./debugging/) - Performance and debugging
|
||||
- [Best Practices](./best-practices/) - Scene design patterns
|
||||
103
docs/src/content/docs/en/guide/scene/lifecycle.md
Normal file
103
docs/src/content/docs/en/guide/scene/lifecycle.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "Scene Lifecycle"
|
||||
description: "Scene lifecycle methods and execution order"
|
||||
---
|
||||
|
||||
Scene provides complete lifecycle management for proper resource initialization and cleanup.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. Scene initialization: setup systems and initial entities
|
||||
console.log("Scene initializing");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 2. Scene starts running: game logic begins execution
|
||||
console.log("Scene starting");
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 3. Per-frame update (called by scene manager)
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 4. Scene unloading: cleanup resources
|
||||
console.log("Scene unloading");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Order
|
||||
|
||||
| Phase | Method | Description |
|
||||
|-------|--------|-------------|
|
||||
| Initialize | `initialize()` | Setup systems and initial entities |
|
||||
| Start | `begin()` / `onStart()` | Scene starts running |
|
||||
| Update | `update()` | Per-frame update (auto-called) |
|
||||
| End | `end()` / `unload()` | Cleanup resources |
|
||||
|
||||
## Lifecycle Example
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private resourcesLoaded = false;
|
||||
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 1. Add systems (by dependency order)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 2. Create initial entities
|
||||
this.createPlayer();
|
||||
this.createEnemies();
|
||||
|
||||
// 3. Setup event listeners
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
this.resourcesLoaded = true;
|
||||
console.log("Scene resources loaded, game starting");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// Cleanup event listeners
|
||||
this.eventSystem.clear();
|
||||
|
||||
// Cleanup other resources
|
||||
this.resourcesLoaded = false;
|
||||
console.log("Scene resources cleaned up");
|
||||
}
|
||||
|
||||
private createPlayer(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
}
|
||||
|
||||
private createEnemies(): void {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
}
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('Player died');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. **initialize() called once** - For initial state setup
|
||||
2. **onStart() on scene activation** - May be called multiple times
|
||||
3. **unload() must cleanup resources** - Avoid memory leaks
|
||||
4. **update() managed by framework** - No manual calls needed
|
||||
115
docs/src/content/docs/en/guide/scene/system-management.md
Normal file
115
docs/src/content/docs/en/guide/scene/system-management.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "System Management"
|
||||
description: "System addition, removal and control in scenes"
|
||||
---
|
||||
|
||||
Scene manages system registration, execution order, and lifecycle.
|
||||
|
||||
## Adding Systems
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add system
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// Set system update order (lower runs first)
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// Add more systems
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Getting Systems
|
||||
|
||||
```typescript
|
||||
// Get system of specific type
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
console.log("Found physics system");
|
||||
}
|
||||
```
|
||||
|
||||
## Removing Systems
|
||||
|
||||
```typescript
|
||||
public removeUnnecessarySystems(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Controlling Systems
|
||||
|
||||
### Enable/Disable Systems
|
||||
|
||||
```typescript
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false; // Disable system
|
||||
}
|
||||
}
|
||||
|
||||
public resumePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = true; // Enable system
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get All Systems
|
||||
|
||||
```typescript
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // Get all registered systems
|
||||
}
|
||||
```
|
||||
|
||||
## System Organization Best Practice
|
||||
|
||||
Group systems by function:
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Add systems by function and dependencies
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `addSystem(system)` | `void` | Add system to scene |
|
||||
| `removeSystem(system)` | `void` | Remove system from scene |
|
||||
| `getEntityProcessor(Type)` | `T \| undefined` | Get system by type |
|
||||
| `systems` | `EntitySystem[]` | Get all systems |
|
||||
@@ -1,572 +0,0 @@
|
||||
---
|
||||
title: "Worker System"
|
||||
---
|
||||
|
||||
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
|
||||
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
|
||||
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
|
||||
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
|
||||
- **Type Safety**: Full TypeScript support and type checking
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Simple Physics System Example
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // Enable Worker parallel processing
|
||||
workerCount: 8, // Worker count, auto-limited to hardware capacity
|
||||
entitiesPerWorker: 100, // Entities per Worker
|
||||
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
|
||||
entityDataSize: 7, // Data size per entity
|
||||
maxEntities: 10000, // Maximum entity count
|
||||
systemConfig: { // Configuration passed to Worker
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Data extraction: Convert Entity to serializable data
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker processing function: Pure function executed in Worker
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// Apply friction
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply results: Apply Worker processing results back to Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer optimization support
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The Worker system supports rich configuration options:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** Enable Worker parallel processing */
|
||||
enableWorker?: boolean;
|
||||
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
|
||||
workerCount?: number;
|
||||
/** Entities per Worker for load distribution control */
|
||||
entitiesPerWorker?: number;
|
||||
/** System configuration data passed to Worker */
|
||||
systemConfig?: any;
|
||||
/** Enable SharedArrayBuffer optimization */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** Float32 count per entity in SharedArrayBuffer */
|
||||
entityDataSize?: number;
|
||||
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
|
||||
maxEntities?: number;
|
||||
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Recommendations
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// Decide based on task complexity
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker count: System auto-limits to hardware capacity
|
||||
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
|
||||
|
||||
// Entities per Worker (optional)
|
||||
entitiesPerWorker: 200, // Precise load distribution control
|
||||
|
||||
// Enable SharedArrayBuffer for many simple calculations
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// Set according to actual data structure
|
||||
entityDataSize: 8, // Ensure it matches data structure
|
||||
|
||||
// Estimated maximum entity count
|
||||
maxEntities: 10000,
|
||||
|
||||
// Global configuration passed to Worker
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// Decide based on entity count and complexity
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// Get system info
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
|
||||
console.log(`Current mode: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Modes
|
||||
|
||||
The Worker system supports two processing modes:
|
||||
|
||||
### 1. Traditional Worker Mode
|
||||
|
||||
Data is serialized and passed between main thread and Workers:
|
||||
|
||||
```typescript
|
||||
// Suitable for: Complex computation logic, moderate entity count
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // Use traditional mode
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// Complex algorithm logic
|
||||
return entities.map(entity => {
|
||||
// AI decisions, pathfinding, etc.
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer Mode
|
||||
|
||||
Zero-copy data sharing, suitable for many simple calculations:
|
||||
|
||||
```typescript
|
||||
// Suitable for: Many entities with simple calculations
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // Enable shared memory
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// Read data
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// Physics calculations
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// Write back data
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
The Worker system is particularly suitable for:
|
||||
|
||||
### 1. Physics Simulation
|
||||
- **Gravity systems**: Gravity calculations for many entities
|
||||
- **Collision detection**: Complex collision algorithms
|
||||
- **Fluid simulation**: Particle fluid systems
|
||||
- **Cloth simulation**: Vertex physics calculations
|
||||
|
||||
### 2. AI Computation
|
||||
- **Pathfinding**: A*, Dijkstra algorithms
|
||||
- **Behavior trees**: Complex AI decision logic
|
||||
- **Swarm intelligence**: Boid, fish school algorithms
|
||||
- **Neural networks**: Simple AI inference
|
||||
|
||||
### 3. Data Processing
|
||||
- **Bulk entity updates**: State machines, lifecycle management
|
||||
- **Statistical calculations**: Game data analysis
|
||||
- **Image processing**: Texture generation, effect calculations
|
||||
- **Audio processing**: Sound synthesis, spectrum analysis
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Worker Function Requirements
|
||||
|
||||
```typescript
|
||||
// Recommended: Worker processing function is a pure function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// Only use parameters and standard JavaScript APIs
|
||||
return entities.map(entity => {
|
||||
// Pure computation logic, no external state dependencies
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// Avoid: Using external references in Worker function
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this and external variables are not available in Worker
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // Error
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Design
|
||||
|
||||
```typescript
|
||||
// Recommended: Reasonable data design
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// Keep data structure simple for easy serialization
|
||||
}
|
||||
|
||||
// Avoid: Complex nested objects
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// Complex nested structures increase serialization overhead
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker Count Control
|
||||
|
||||
```typescript
|
||||
// Recommended: Flexible Worker configuration
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// Specify needed Worker count, system auto-limits to hardware capacity
|
||||
workerCount: 8, // Request 8 Workers
|
||||
entitiesPerWorker: 100, // 100 entities per Worker
|
||||
enableWorker: this.shouldUseWorker(), // Conditional enable
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// Decide based on entity count and complexity
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// Get actual Worker info
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Requested Workers: 8`);
|
||||
console.log(`Actual Workers: ${info.workerCount}`);
|
||||
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Performance Monitoring
|
||||
|
||||
```typescript
|
||||
// Recommended: Performance monitoring
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
### 1. Compute Intensity Assessment
|
||||
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
|
||||
|
||||
### 2. Data Transfer Optimization
|
||||
- Use SharedArrayBuffer to reduce serialization overhead
|
||||
- Keep data structures simple and flat
|
||||
- Avoid frequent large data transfers
|
||||
|
||||
### 3. Degradation Strategy
|
||||
Always provide main thread fallback to ensure normal operation in environments without Worker support.
|
||||
|
||||
### 4. Memory Management
|
||||
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
|
||||
|
||||
### 5. Load Balancing
|
||||
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
|
||||
|
||||
## WeChat Mini Game Support
|
||||
|
||||
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
|
||||
|
||||
### WeChat Mini Game Worker Limitations
|
||||
|
||||
| Feature | Browser | WeChat Mini Game |
|
||||
|---------|---------|------------------|
|
||||
| Dynamic scripts (Blob URL) | Supported | Not supported |
|
||||
| Worker count | Multiple | Maximum 1 |
|
||||
| Script source | Any | Must be in code package |
|
||||
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
|
||||
|
||||
### Using Worker Generator CLI
|
||||
|
||||
#### 1. Install the Tool
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### 2. Configure workerScriptPath
|
||||
|
||||
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
// Physics calculation logic
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Generate Worker Files
|
||||
|
||||
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# Full options
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \ # Source directory
|
||||
--wechat \ # Generate WeChat Mini Game compatible code
|
||||
--mapping \ # Generate worker-mapping.json
|
||||
--verbose # Verbose output
|
||||
```
|
||||
|
||||
The CLI tool will:
|
||||
1. Scan source directory for all `WorkerEntitySystem` subclasses
|
||||
2. Read each class's `workerScriptPath` configuration
|
||||
3. Extract `workerProcess` method body
|
||||
4. Convert to ES5 syntax (WeChat Mini Game compatible)
|
||||
5. Generate to configured path
|
||||
|
||||
#### 4. Configure game.json
|
||||
|
||||
Configure workers directory in WeChat Mini Game's `game.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Project Structure
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json # Configure "workers": "workers"
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
|
||||
└── workers/
|
||||
├── physics-worker.js # Auto-generated
|
||||
└── worker-mapping.json # Auto-generated
|
||||
```
|
||||
|
||||
### Temporarily Disabling Workers
|
||||
|
||||
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
|
||||
|
||||
#### Method 1: Configuration Disable
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false, // Disable Worker, use main thread processing
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Method 2: Platform Adapter Disable
|
||||
|
||||
Return Worker not supported in custom platform adapter:
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false; // Return false to disable Worker
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
|
||||
|
||||
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
|
||||
```typescript
|
||||
// Correct: Only use parameters
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * deltaTime;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// Wrong: Using this
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += this.gravity * deltaTime; // Cannot access this in Worker
|
||||
return e;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **Pass configuration data via `systemConfig`**, not class properties
|
||||
|
||||
4. **Developer tool warnings can be ignored**:
|
||||
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
|
||||
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
|
||||
|
||||
## Online Demo
|
||||
|
||||
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
The demo showcases:
|
||||
- Multi-threaded physics computation
|
||||
- Real-time performance comparison
|
||||
- SharedArrayBuffer optimization
|
||||
- Parallel processing of many entities
|
||||
|
||||
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Worker system performance optimization"
|
||||
---
|
||||
|
||||
## Worker Function Requirements
|
||||
|
||||
```typescript
|
||||
// ✅ Pure function using only parameters
|
||||
protected workerProcess(entities: PhysicsData[], dt: number, config: any): PhysicsData[] {
|
||||
return entities.map(e => {
|
||||
e.y += e.velocity * dt;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ Avoid using this or external variables
|
||||
protected workerProcess(entities: PhysicsData[], dt: number): PhysicsData[] {
|
||||
e.y += this.someProperty; // ❌ Can't access this in Worker
|
||||
}
|
||||
```
|
||||
|
||||
## Data Design
|
||||
|
||||
- Use simple, flat data structures
|
||||
- Avoid complex nested objects
|
||||
- Keep serialization overhead minimal
|
||||
|
||||
## When to Use Workers
|
||||
|
||||
| Scenario | Recommendation |
|
||||
|----------|----------------|
|
||||
| Entities < 100 | Don't use Worker |
|
||||
| 100 < Entities < 1000 | Traditional Worker mode |
|
||||
| Entities > 1000 | SharedArrayBuffer mode |
|
||||
| Complex AI | Traditional Worker mode |
|
||||
| Simple physics | SharedArrayBuffer mode |
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. Only use Workers for compute-intensive tasks
|
||||
2. Use SharedArrayBuffer to reduce serialization
|
||||
3. Keep data structures simple and flat
|
||||
4. Use `entitiesPerWorker` for load balancing
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
description: "Worker system configuration and processing modes"
|
||||
---
|
||||
|
||||
## Configuration Interface
|
||||
|
||||
```typescript
|
||||
interface IWorkerSystemConfig {
|
||||
enableWorker?: boolean;
|
||||
workerCount?: number;
|
||||
entitiesPerWorker?: number;
|
||||
systemConfig?: unknown;
|
||||
useSharedArrayBuffer?: boolean;
|
||||
entityDataSize?: number;
|
||||
maxEntities?: number;
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Modes
|
||||
|
||||
### Traditional Worker Mode
|
||||
|
||||
Data serialized between main thread and Workers:
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false,
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Use case**: Complex calculations, moderate entity count
|
||||
|
||||
### SharedArrayBuffer Mode
|
||||
|
||||
Zero-copy data sharing for large-scale simple calculations:
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Use case**: Many entities with simple calculations
|
||||
|
||||
## Get System Info
|
||||
|
||||
```typescript
|
||||
const info = this.getWorkerInfo();
|
||||
// { enabled, workerCount, maxSystemWorkerCount, currentMode, ... }
|
||||
```
|
||||
39
docs/src/content/docs/en/guide/worker-system/examples.md
Normal file
39
docs/src/content/docs/en/guide/worker-system/examples.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "Complete Worker system examples"
|
||||
---
|
||||
|
||||
## Particle Physics System
|
||||
|
||||
A complete particle physics system with collision detection. See the [Chinese version](/guide/worker-system/examples/) for the full code example with:
|
||||
|
||||
- Gravity and velocity updates
|
||||
- Boundary collision
|
||||
- Particle-particle collision with elastic response
|
||||
- SharedArrayBuffer optimization
|
||||
- Performance monitoring
|
||||
|
||||
## Key Implementation Points
|
||||
|
||||
```typescript
|
||||
@ECSSystem('ParticlePhysics')
|
||||
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerCount: 6,
|
||||
entitiesPerWorker: 150,
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 9,
|
||||
maxEntities: 5000,
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Implement required abstract methods...
|
||||
}
|
||||
```
|
||||
104
docs/src/content/docs/en/guide/worker-system/index.md
Normal file
104
docs/src/content/docs/en/guide/worker-system/index.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: "Worker System"
|
||||
description: "Web Worker based multi-threaded parallel processing system"
|
||||
---
|
||||
|
||||
Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers, designed for compute-intensive tasks.
|
||||
|
||||
## Core Features
|
||||
|
||||
- **True Parallel Computing**: Execute tasks in background threads via Web Workers
|
||||
- **Auto Load Balancing**: Distribute workload based on CPU cores
|
||||
- **SharedArrayBuffer Optimization**: Zero-copy data sharing
|
||||
- **Fallback Support**: Auto fallback to main thread when Workers unavailable
|
||||
- **Type Safety**: Full TypeScript support
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 4,
|
||||
systemConfig: { gravity: 100 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
return { id: entity.id, x: pos.x, y: pos.y, vx: vel.x, vy: vel.y };
|
||||
}
|
||||
|
||||
protected workerProcess(entities: PhysicsData[], dt: number, config: any): PhysicsData[] {
|
||||
return entities.map(e => {
|
||||
e.vy += config.gravity * dt;
|
||||
e.x += e.vx * dt;
|
||||
e.y += e.vy * dt;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x = result.x;
|
||||
pos.y = result.y;
|
||||
vel.x = result.vx;
|
||||
vel.y = result.vy;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
this.sharedFloatArray[offset] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.vx;
|
||||
this.sharedFloatArray[offset + 4] = data.vy;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
return {
|
||||
id: this.sharedFloatArray[offset],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Scenario | Examples |
|
||||
|----------|----------|
|
||||
| **Physics Simulation** | Gravity, collision detection, fluid |
|
||||
| **AI Computing** | Pathfinding, behavior trees, flocking |
|
||||
| **Data Processing** | State machines, statistics, image processing |
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Configuration](/en/guide/worker-system/configuration/) - Options and processing modes
|
||||
- [Examples](/en/guide/worker-system/examples/) - Complete particle physics example
|
||||
- [WeChat Mini Game](/en/guide/worker-system/wechat/) - WeChat Worker support
|
||||
- [Best Practices](/en/guide/worker-system/best-practices/) - Performance optimization
|
||||
|
||||
## Live Demo
|
||||
|
||||
[Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
43
docs/src/content/docs/en/guide/worker-system/wechat.md
Normal file
43
docs/src/content/docs/en/guide/worker-system/wechat.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "WeChat Mini Game"
|
||||
description: "WeChat Worker limitations and solutions"
|
||||
---
|
||||
|
||||
WeChat Mini Game has special Worker restrictions. ESEngine provides CLI tools to solve this.
|
||||
|
||||
## Platform Differences
|
||||
|
||||
| Feature | Browser | WeChat |
|
||||
|---------|---------|--------|
|
||||
| Dynamic scripts | ✅ | ❌ |
|
||||
| Worker count | Multiple | Max 1 |
|
||||
| Script source | Any | Package files only |
|
||||
|
||||
## Using Worker Generator CLI
|
||||
|
||||
```bash
|
||||
# Install
|
||||
pnpm add -D @esengine/worker-generator
|
||||
|
||||
# Generate Worker files
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. Re-run CLI after modifying `workerProcess`
|
||||
2. Worker functions must be pure (no `this`)
|
||||
3. Pass config via `systemConfig`
|
||||
121
docs/src/content/docs/en/modules/behavior-tree/advanced-usage.md
Normal file
121
docs/src/content/docs/en/modules/behavior-tree/advanced-usage.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: "Advanced Usage"
|
||||
description: "Performance optimization and debugging"
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Tick Rate Control
|
||||
|
||||
```typescript
|
||||
// Reduce update frequency for distant entities
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime.tickInterval = 100; // Update every 100ms instead of every frame
|
||||
```
|
||||
|
||||
### Conditional Execution
|
||||
|
||||
```typescript
|
||||
// Skip execution based on conditions
|
||||
class OptimizedSystem extends BehaviorTreeExecutionSystem {
|
||||
shouldProcess(entity: Entity): boolean {
|
||||
const distance = getDistanceToPlayer(entity);
|
||||
return distance < 1000; // Only process nearby entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Node Pooling
|
||||
|
||||
The framework automatically pools node execution contexts to reduce GC pressure.
|
||||
|
||||
## Debugging
|
||||
|
||||
### Runtime Inspection
|
||||
|
||||
```typescript
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
// Current state
|
||||
console.log('State:', runtime.state);
|
||||
console.log('Current node:', runtime.currentNodeId);
|
||||
|
||||
// Blackboard
|
||||
console.log('Health:', runtime.getBlackboardValue('health'));
|
||||
|
||||
// Execution history
|
||||
console.log('Last nodes:', runtime.executionHistory);
|
||||
```
|
||||
|
||||
### Event Logging
|
||||
|
||||
```typescript
|
||||
runtime.onNodeEnter = (nodeId) => {
|
||||
console.log(`Entering: ${nodeId}`);
|
||||
};
|
||||
|
||||
runtime.onNodeExit = (nodeId, status) => {
|
||||
console.log(`Exiting: ${nodeId} with ${status}`);
|
||||
};
|
||||
```
|
||||
|
||||
### Visual Debugging
|
||||
|
||||
```typescript
|
||||
// Enable visual debugging
|
||||
runtime.debug = true;
|
||||
|
||||
// Draw current execution path
|
||||
BehaviorTreeDebugger.draw(entity);
|
||||
```
|
||||
|
||||
## State Persistence
|
||||
|
||||
### Save State
|
||||
|
||||
```typescript
|
||||
const state = runtime.serialize();
|
||||
localStorage.setItem('ai-state', JSON.stringify(state));
|
||||
```
|
||||
|
||||
### Restore State
|
||||
|
||||
```typescript
|
||||
const state = JSON.parse(localStorage.getItem('ai-state'));
|
||||
runtime.deserialize(state);
|
||||
```
|
||||
|
||||
## Multi-Tree Entities
|
||||
|
||||
```typescript
|
||||
// An entity can have multiple behavior trees
|
||||
const combatAI = BehaviorTreeBuilder.create('Combat').build();
|
||||
const dialogAI = BehaviorTreeBuilder.create('Dialog').build();
|
||||
|
||||
// Start both
|
||||
BehaviorTreeStarter.start(entity, combatAI, 'combat');
|
||||
BehaviorTreeStarter.start(entity, dialogAI, 'dialog');
|
||||
|
||||
// Control individually
|
||||
const combatRuntime = entity.getComponent(BehaviorTreeRuntimeComponent, 'combat');
|
||||
combatRuntime.pause();
|
||||
```
|
||||
|
||||
## Custom Execution
|
||||
|
||||
Override the default execution system:
|
||||
|
||||
```typescript
|
||||
class CustomExecutionSystem extends BehaviorTreeExecutionSystem {
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
// Custom pre-processing
|
||||
this.updateBlackboard(entity);
|
||||
|
||||
// Standard execution
|
||||
super.processEntity(entity, dt);
|
||||
|
||||
// Custom post-processing
|
||||
this.handleResults(entity);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "Asset Management"
|
||||
description: "Loading, managing, and reusing behavior tree assets"
|
||||
---
|
||||
|
||||
## Loading Trees
|
||||
|
||||
### From JSON
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeLoader } from '@esengine/behavior-tree';
|
||||
|
||||
// Load from URL
|
||||
const tree = await BehaviorTreeLoader.load('assets/enemy-ai.json');
|
||||
|
||||
// Load from object
|
||||
const tree = BehaviorTreeLoader.fromData(jsonData);
|
||||
```
|
||||
|
||||
### Using Asset Manager
|
||||
|
||||
```typescript
|
||||
import { AssetManager } from '@esengine/asset-system';
|
||||
|
||||
// Register loader
|
||||
AssetManager.registerLoader('btree', BehaviorTreeLoader);
|
||||
|
||||
// Load asset
|
||||
const tree = await AssetManager.load<BehaviorTreeData>('enemy-ai.btree');
|
||||
```
|
||||
|
||||
## Subtrees
|
||||
|
||||
Reuse behavior trees as subtrees:
|
||||
|
||||
```typescript
|
||||
// Create a reusable patrol behavior
|
||||
const patrolTree = BehaviorTreeBuilder.create('PatrolBehavior')
|
||||
.sequence('Patrol')
|
||||
.action('moveToWaypoint')
|
||||
.wait(2000)
|
||||
.action('nextWaypoint')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// Use as subtree in main AI
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.selector('Main')
|
||||
.sequence('Combat')
|
||||
.condition('hasTarget')
|
||||
.action('attack')
|
||||
.end()
|
||||
// Include patrol subtree
|
||||
.subtree(patrolTree)
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## Asset References
|
||||
|
||||
Reference external trees by ID:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('MainAI')
|
||||
.selector('Root')
|
||||
.subtreeRef('combat-behavior') // References another tree
|
||||
.subtreeRef('patrol-behavior')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
```typescript
|
||||
// Trees are cached automatically
|
||||
const cache = BehaviorTreeLoader.getCache();
|
||||
|
||||
// Clear specific tree
|
||||
cache.remove('enemy-ai');
|
||||
|
||||
// Clear all
|
||||
cache.clear();
|
||||
```
|
||||
|
||||
## Hot Reloading
|
||||
|
||||
During development:
|
||||
|
||||
```typescript
|
||||
// Enable hot reload
|
||||
BehaviorTreeLoader.enableHotReload();
|
||||
|
||||
// Trees will automatically update when files change
|
||||
```
|
||||
174
docs/src/content/docs/en/modules/behavior-tree/best-practices.md
Normal file
174
docs/src/content/docs/en/modules/behavior-tree/best-practices.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Design patterns and tips for behavior trees"
|
||||
---
|
||||
|
||||
## Tree Structure
|
||||
|
||||
### Keep Trees Shallow
|
||||
|
||||
```typescript
|
||||
// Good - flat structure
|
||||
.selector('Main')
|
||||
.sequence('Combat')
|
||||
.sequence('Patrol')
|
||||
.sequence('Idle')
|
||||
.end()
|
||||
|
||||
// Avoid - deep nesting
|
||||
.selector('Main')
|
||||
.selector('Level1')
|
||||
.selector('Level2')
|
||||
.selector('Level3')
|
||||
// ...
|
||||
```
|
||||
|
||||
### Use Subtrees
|
||||
|
||||
Break complex behaviors into reusable subtrees:
|
||||
|
||||
```typescript
|
||||
// Define reusable behaviors
|
||||
const combatBehavior = createCombatTree();
|
||||
const patrolBehavior = createPatrolTree();
|
||||
|
||||
// Compose main AI
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.selector('Main')
|
||||
.subtree(combatBehavior)
|
||||
.subtree(patrolBehavior)
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## Blackboard Design
|
||||
|
||||
### Use Clear Naming
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
.defineBlackboardVariable('targetEntity', null)
|
||||
.defineBlackboardVariable('lastKnownPosition', null)
|
||||
.defineBlackboardVariable('alertLevel', 0)
|
||||
|
||||
// Avoid
|
||||
.defineBlackboardVariable('t', null)
|
||||
.defineBlackboardVariable('pos', null)
|
||||
.defineBlackboardVariable('a', 0)
|
||||
```
|
||||
|
||||
### Group Related Variables
|
||||
|
||||
```typescript
|
||||
// Combat-related
|
||||
combatTarget: Entity
|
||||
combatRange: number
|
||||
attackCooldown: number
|
||||
|
||||
// Movement-related
|
||||
moveTarget: Vector2
|
||||
moveSpeed: number
|
||||
pathNodes: Vector2[]
|
||||
```
|
||||
|
||||
## Action Design
|
||||
|
||||
### Single Responsibility
|
||||
|
||||
```typescript
|
||||
// Good - focused actions
|
||||
class MoveToTarget implements INodeExecutor { }
|
||||
class AttackTarget implements INodeExecutor { }
|
||||
class PlayAnimation implements INodeExecutor { }
|
||||
|
||||
// Avoid - do-everything actions
|
||||
class CombatAction implements INodeExecutor {
|
||||
// Moves, attacks, plays animation, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Stateless Executors
|
||||
|
||||
```typescript
|
||||
// Good - use context for state
|
||||
class WaitAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const elapsed = context.runtime.getNodeState(context.node.id, 'elapsed') ?? 0;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid - instance state
|
||||
class WaitAction implements INodeExecutor {
|
||||
private elapsed = 0; // Don't do this!
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Add Log Nodes
|
||||
|
||||
```typescript
|
||||
.sequence('AttackSequence')
|
||||
.log('Starting attack sequence', 'Debug')
|
||||
.action('findTarget')
|
||||
.log('Target found', 'Debug')
|
||||
.action('attack')
|
||||
.log('Attack complete', 'Debug')
|
||||
.end()
|
||||
```
|
||||
|
||||
### Use Meaningful Node Names
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
.sequence('ApproachAndAttackEnemy')
|
||||
.condition('IsEnemyInRange')
|
||||
.action('PerformMeleeAttack')
|
||||
|
||||
// Avoid
|
||||
.sequence('Seq1')
|
||||
.condition('Cond1')
|
||||
.action('Action1')
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reduce tick rate** for distant entities
|
||||
2. **Use conditions early** to fail fast
|
||||
3. **Cache expensive calculations** in blackboard
|
||||
4. **Limit subtree depth** to reduce traversal cost
|
||||
5. **Profile** your trees in real gameplay
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Guard Pattern
|
||||
|
||||
```typescript
|
||||
.sequence('GuardedAction')
|
||||
.condition('canPerformAction') // Guard condition
|
||||
.action('performAction') // Actual action
|
||||
.end()
|
||||
```
|
||||
|
||||
### Cooldown Pattern
|
||||
|
||||
```typescript
|
||||
.sequence('CooldownAttack')
|
||||
.condition('isCooldownReady')
|
||||
.action('attack')
|
||||
.action('startCooldown')
|
||||
.end()
|
||||
```
|
||||
|
||||
### Memory Pattern
|
||||
|
||||
```typescript
|
||||
.selector('RememberAndAct')
|
||||
.sequence('UseMemory')
|
||||
.condition('hasLastKnownPosition')
|
||||
.action('moveToLastKnownPosition')
|
||||
.end()
|
||||
.action('search')
|
||||
.end()
|
||||
```
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: "Cocos Creator Integration"
|
||||
description: "Using behavior trees with Cocos Creator"
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/behavior-tree @esengine/ecs-framework
|
||||
```
|
||||
|
||||
### Project Configuration
|
||||
|
||||
Add to your Cocos Creator project's `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Integration
|
||||
|
||||
```typescript
|
||||
import { _decorator, Component } from 'cc';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin, BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends Component {
|
||||
private scene: Scene;
|
||||
private plugin: BehaviorTreePlugin;
|
||||
|
||||
async start() {
|
||||
// Initialize ECS
|
||||
Core.create();
|
||||
this.plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(this.plugin);
|
||||
|
||||
this.scene = new Scene();
|
||||
this.plugin.setupScene(this.scene);
|
||||
Core.setScene(this.scene);
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
// Update ECS
|
||||
this.scene?.update(dt * 1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating AI Components
|
||||
|
||||
```typescript
|
||||
@ccclass('EnemyAI')
|
||||
export class EnemyAI extends Component {
|
||||
@property
|
||||
public aggroRange: number = 200;
|
||||
|
||||
private entity: Entity;
|
||||
|
||||
start() {
|
||||
const tree = this.createBehaviorTree();
|
||||
this.entity = GameManager.instance.scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
|
||||
private createBehaviorTree() {
|
||||
return BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('ccNode', this.node)
|
||||
.selector('Main')
|
||||
.sequence('Attack')
|
||||
.condition('hasTarget')
|
||||
.action('attack')
|
||||
.end()
|
||||
.action('patrol')
|
||||
.end()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Actions for Cocos
|
||||
|
||||
```typescript
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'CCMoveToTarget',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: 'Move To Target',
|
||||
category: 'Cocos'
|
||||
})
|
||||
export class CCMoveToTarget implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const ccNode = context.runtime.getBlackboardValue<Node>('ccNode');
|
||||
const target = context.runtime.getBlackboardValue<Vec3>('targetPosition');
|
||||
|
||||
if (!ccNode || !target) return TaskStatus.Failure;
|
||||
|
||||
const pos = ccNode.position;
|
||||
const direction = new Vec3();
|
||||
Vec3.subtract(direction, target, pos);
|
||||
|
||||
if (direction.length() < 10) {
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
direction.normalize();
|
||||
const speed = BindingHelper.getValue<number>(context, 'speed', 100);
|
||||
const delta = new Vec3();
|
||||
Vec3.multiplyScalar(delta, direction, speed * context.deltaTime / 1000);
|
||||
ccNode.position = pos.add(delta);
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Loading Tree Assets
|
||||
|
||||
```typescript
|
||||
// Load from Cocos resources
|
||||
import { resources, JsonAsset } from 'cc';
|
||||
|
||||
async function loadBehaviorTree(path: string): Promise<BehaviorTreeData> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resources.load(path, JsonAsset, (err, asset) => {
|
||||
if (err) reject(err);
|
||||
else resolve(BehaviorTreeLoader.fromData(asset.json));
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Sync positions** between Cocos nodes and ECS entities
|
||||
2. **Use blackboard** to store Cocos-specific references
|
||||
3. **Update ECS** in Cocos update loop
|
||||
4. **Handle cleanup** when destroying components
|
||||
179
docs/src/content/docs/en/modules/behavior-tree/core-concepts.md
Normal file
179
docs/src/content/docs/en/modules/behavior-tree/core-concepts.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "Core Concepts"
|
||||
description: "Understanding behavior tree fundamentals"
|
||||
---
|
||||
|
||||
## Node Types
|
||||
|
||||
### Composite Nodes
|
||||
|
||||
Control the execution flow of child nodes.
|
||||
|
||||
#### Sequence
|
||||
|
||||
Executes children in order. Fails if any child fails.
|
||||
|
||||
```typescript
|
||||
.sequence('AttackSequence')
|
||||
.condition('hasTarget') // If false, sequence fails
|
||||
.action('moveToTarget') // If fails, sequence fails
|
||||
.action('attack') // If succeeds, sequence succeeds
|
||||
.end()
|
||||
```
|
||||
|
||||
#### Selector
|
||||
|
||||
Tries children until one succeeds. Fails if all children fail.
|
||||
|
||||
```typescript
|
||||
.selector('FindTarget')
|
||||
.action('findNearestEnemy') // Try first
|
||||
.action('findNearestItem') // Try if first fails
|
||||
.action('wander') // Fallback
|
||||
.end()
|
||||
```
|
||||
|
||||
#### Parallel
|
||||
|
||||
Executes all children simultaneously.
|
||||
|
||||
```typescript
|
||||
.parallel('CombatActions', {
|
||||
successPolicy: 'all', // 'all' | 'any'
|
||||
failurePolicy: 'any' // 'all' | 'any'
|
||||
})
|
||||
.action('playAttackAnimation')
|
||||
.action('dealDamage')
|
||||
.action('playSound')
|
||||
.end()
|
||||
```
|
||||
|
||||
### Leaf Nodes
|
||||
|
||||
#### Action
|
||||
|
||||
Performs a specific task.
|
||||
|
||||
```typescript
|
||||
.action('attack', { damage: 10 })
|
||||
```
|
||||
|
||||
#### Condition
|
||||
|
||||
Checks a condition without side effects.
|
||||
|
||||
```typescript
|
||||
.condition('isHealthLow', { threshold: 30 })
|
||||
```
|
||||
|
||||
### Decorator Nodes
|
||||
|
||||
Modify child behavior.
|
||||
|
||||
```typescript
|
||||
// Invert result
|
||||
.inverter()
|
||||
.condition('isEnemy')
|
||||
.end()
|
||||
|
||||
// Repeat until failure
|
||||
.repeatUntilFail()
|
||||
.action('patrol')
|
||||
.end()
|
||||
|
||||
// Timeout
|
||||
.timeout(5000)
|
||||
.action('searchForTarget')
|
||||
.end()
|
||||
```
|
||||
|
||||
## Task Status
|
||||
|
||||
Every node returns one of these statuses:
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `Success` | Task completed successfully |
|
||||
| `Failure` | Task failed |
|
||||
| `Running` | Task still in progress |
|
||||
|
||||
```typescript
|
||||
import { TaskStatus } from '@esengine/behavior-tree';
|
||||
|
||||
class MyAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
if (/* completed */) return TaskStatus.Success;
|
||||
if (/* failed */) return TaskStatus.Failure;
|
||||
return TaskStatus.Running; // Still working
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Blackboard System
|
||||
|
||||
### Local vs Global
|
||||
|
||||
```typescript
|
||||
// Local blackboard - per behavior tree instance
|
||||
runtime.setBlackboardValue('localVar', value);
|
||||
|
||||
// Global blackboard - shared across all trees
|
||||
runtime.setGlobalBlackboardValue('globalVar', value);
|
||||
```
|
||||
|
||||
### Variable Access in Executors
|
||||
|
||||
```typescript
|
||||
class PatrolAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
// Read from blackboard
|
||||
const target = context.runtime.getBlackboardValue('patrolTarget');
|
||||
const speed = context.runtime.getBlackboardValue('moveSpeed');
|
||||
|
||||
// Write to blackboard
|
||||
context.runtime.setBlackboardValue('lastPosition', currentPos);
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Context
|
||||
|
||||
```typescript
|
||||
interface NodeExecutionContext {
|
||||
readonly node: IBehaviorTreeNode; // Current node data
|
||||
readonly runtime: BehaviorTreeRuntimeComponent; // Runtime state
|
||||
readonly entity: Entity; // Owner entity
|
||||
readonly scene: Scene; // Current scene
|
||||
readonly deltaTime: number; // Frame delta time
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
BehaviorTreeData (Pure Data)
|
||||
│
|
||||
├── Serializable JSON structure
|
||||
├── Node definitions
|
||||
└── Blackboard schema
|
||||
|
||||
BehaviorTreeRuntimeComponent (State)
|
||||
│
|
||||
├── Current execution state
|
||||
├── Blackboard values
|
||||
└── Node status cache
|
||||
|
||||
BehaviorTreeExecutionSystem (Logic)
|
||||
│
|
||||
├── Drives tree execution
|
||||
├── Manages node traversal
|
||||
└── Calls INodeExecutor.execute()
|
||||
|
||||
INodeExecutor (Behavior)
|
||||
│
|
||||
├── Stateless design
|
||||
├── Receives context
|
||||
└── Returns TaskStatus
|
||||
```
|
||||
197
docs/src/content/docs/en/modules/behavior-tree/custom-actions.md
Normal file
197
docs/src/content/docs/en/modules/behavior-tree/custom-actions.md
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
title: "Custom Actions"
|
||||
description: "Create custom behavior tree nodes"
|
||||
---
|
||||
|
||||
## Creating a Custom Executor
|
||||
|
||||
```typescript
|
||||
import {
|
||||
INodeExecutor,
|
||||
NodeExecutionContext,
|
||||
NodeExecutorMetadata,
|
||||
TaskStatus,
|
||||
NodeType,
|
||||
BindingHelper
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'AttackAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: 'Attack',
|
||||
description: 'Attack the target',
|
||||
category: 'Combat',
|
||||
configSchema: {
|
||||
damage: {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
supportBinding: true
|
||||
},
|
||||
range: {
|
||||
type: 'number',
|
||||
default: 50
|
||||
}
|
||||
}
|
||||
})
|
||||
export class AttackAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
|
||||
const target = context.runtime.getBlackboardValue('target');
|
||||
|
||||
if (!target) {
|
||||
return TaskStatus.Failure;
|
||||
}
|
||||
|
||||
// Perform attack logic
|
||||
console.log(`Dealing ${damage} damage`);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Decorator Metadata
|
||||
|
||||
```typescript
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'MoveToTarget',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: 'Move To Target',
|
||||
description: 'Move entity towards target position',
|
||||
category: 'Movement',
|
||||
icon: 'move',
|
||||
color: '#4CAF50',
|
||||
configSchema: {
|
||||
speed: {
|
||||
type: 'number',
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 1000,
|
||||
supportBinding: true,
|
||||
description: 'Movement speed'
|
||||
},
|
||||
arrivalDistance: {
|
||||
type: 'number',
|
||||
default: 10,
|
||||
description: 'Distance to consider arrived'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Config Schema Types
|
||||
|
||||
| Type | Properties |
|
||||
|------|------------|
|
||||
| `number` | `min`, `max`, `step`, `default` |
|
||||
| `string` | `default`, `maxLength` |
|
||||
| `boolean` | `default` |
|
||||
| `select` | `options`, `default` |
|
||||
| `vector2` | `default: { x, y }` |
|
||||
|
||||
```typescript
|
||||
configSchema: {
|
||||
mode: {
|
||||
type: 'select',
|
||||
options: ['aggressive', 'defensive', 'passive'],
|
||||
default: 'aggressive'
|
||||
},
|
||||
offset: {
|
||||
type: 'vector2',
|
||||
default: { x: 0, y: 0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using BindingHelper
|
||||
|
||||
```typescript
|
||||
import { BindingHelper } from '@esengine/behavior-tree';
|
||||
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
// Get value with fallback
|
||||
const speed = BindingHelper.getValue<number>(context, 'speed', 100);
|
||||
|
||||
// Get bound value from blackboard
|
||||
const target = BindingHelper.getBoundValue<Entity>(context, 'target');
|
||||
|
||||
// Check if value is bound
|
||||
if (BindingHelper.isBound(context, 'target')) {
|
||||
// Value comes from blackboard
|
||||
}
|
||||
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
```
|
||||
|
||||
## Async Actions
|
||||
|
||||
For actions that span multiple frames:
|
||||
|
||||
```typescript
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'WaitAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: 'Wait',
|
||||
category: 'Timing',
|
||||
configSchema: {
|
||||
duration: { type: 'number', default: 1000 }
|
||||
}
|
||||
})
|
||||
export class WaitAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const duration = BindingHelper.getValue<number>(context, 'duration', 1000);
|
||||
|
||||
// Get or initialize state
|
||||
let elapsed = context.runtime.getNodeState<number>(context.node.id, 'elapsed') ?? 0;
|
||||
elapsed += context.deltaTime;
|
||||
|
||||
if (elapsed >= duration) {
|
||||
// Clear state and complete
|
||||
context.runtime.clearNodeState(context.node.id);
|
||||
return TaskStatus.Success;
|
||||
}
|
||||
|
||||
// Save state and continue
|
||||
context.runtime.setNodeState(context.node.id, 'elapsed', elapsed);
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Condition Nodes
|
||||
|
||||
```typescript
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'IsHealthLow',
|
||||
nodeType: NodeType.Condition,
|
||||
displayName: 'Is Health Low',
|
||||
category: 'Conditions',
|
||||
configSchema: {
|
||||
threshold: { type: 'number', default: 30 }
|
||||
}
|
||||
})
|
||||
export class IsHealthLow implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const threshold = BindingHelper.getValue<number>(context, 'threshold', 30);
|
||||
const health = context.runtime.getBlackboardValue<number>('health') ?? 100;
|
||||
|
||||
return health <= threshold
|
||||
? TaskStatus.Success
|
||||
: TaskStatus.Failure;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registering Custom Executors
|
||||
|
||||
Executors are auto-registered via the decorator. To manually register:
|
||||
|
||||
```typescript
|
||||
import { NodeExecutorRegistry } from '@esengine/behavior-tree';
|
||||
|
||||
// Register
|
||||
NodeExecutorRegistry.register('CustomAction', CustomAction);
|
||||
|
||||
// Get executor
|
||||
const executor = NodeExecutorRegistry.get('CustomAction');
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: "Editor Guide"
|
||||
description: "Visual behavior tree creation"
|
||||
---
|
||||
|
||||
## Opening the Editor
|
||||
|
||||
The behavior tree editor provides a visual interface for creating and managing behavior trees.
|
||||
|
||||
## Creating a New Tree
|
||||
|
||||
1. Open the behavior tree editor
|
||||
2. Click "New Behavior Tree"
|
||||
3. Enter a name for your tree
|
||||
4. Start adding nodes
|
||||
|
||||
## Node Operations
|
||||
|
||||
### Adding Nodes
|
||||
|
||||
- Drag nodes from the palette to the canvas
|
||||
- Right-click on canvas to open context menu
|
||||
- Use keyboard shortcuts for quick access
|
||||
|
||||
### Connecting Nodes
|
||||
|
||||
- Click and drag from a node's output port
|
||||
- Drop on another node's input port
|
||||
- Connections show execution flow
|
||||
|
||||
### Configuring Nodes
|
||||
|
||||
- Select a node to view its properties
|
||||
- Configure parameters in the property panel
|
||||
- Use bindings to connect to blackboard variables
|
||||
|
||||
## Blackboard Panel
|
||||
|
||||
The blackboard panel allows you to:
|
||||
|
||||
- Define variables used by the tree
|
||||
- Set initial values
|
||||
- Specify variable types
|
||||
|
||||
```
|
||||
Variables:
|
||||
- health: number = 100
|
||||
- target: Entity = null
|
||||
- isAlert: boolean = false
|
||||
```
|
||||
|
||||
## Saving and Loading
|
||||
|
||||
- **Save**: Ctrl+S or File → Save
|
||||
- **Export**: Export as JSON for runtime use
|
||||
- **Import**: Load existing tree files
|
||||
|
||||
## Debugging
|
||||
|
||||
- Use the play button to test execution
|
||||
- Watch node status in real-time
|
||||
- View blackboard values during execution
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| Ctrl+S | Save |
|
||||
| Ctrl+Z | Undo |
|
||||
| Ctrl+Y | Redo |
|
||||
| Delete | Remove selected |
|
||||
| Ctrl+C | Copy |
|
||||
| Ctrl+V | Paste |
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "Editor Workflow"
|
||||
description: "Complete development workflow with the editor"
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
1. **Design** - Plan your AI behavior
|
||||
2. **Create** - Build the tree in the editor
|
||||
3. **Configure** - Set up blackboard and properties
|
||||
4. **Test** - Debug in the editor
|
||||
5. **Export** - Generate runtime assets
|
||||
6. **Integrate** - Use in your game
|
||||
|
||||
## Design Phase
|
||||
|
||||
Before opening the editor, plan your AI:
|
||||
|
||||
```
|
||||
Enemy AI Design:
|
||||
├── If health > 50%
|
||||
│ ├── Find target
|
||||
│ ├── Move to target
|
||||
│ └── Attack
|
||||
└── Else
|
||||
└── Flee to safety
|
||||
```
|
||||
|
||||
## Create Phase
|
||||
|
||||
Translate your design to nodes:
|
||||
|
||||
1. Start with a Selector (main decision)
|
||||
2. Add Sequences for each branch
|
||||
3. Add Conditions and Actions
|
||||
4. Configure node properties
|
||||
|
||||
## Configure Phase
|
||||
|
||||
### Blackboard Setup
|
||||
|
||||
Define variables your tree needs:
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| health | number | 100 | Current health |
|
||||
| target | Entity | null | Attack target |
|
||||
| homePosition | Vector2 | (0,0) | Safe position |
|
||||
|
||||
### Property Bindings
|
||||
|
||||
Connect node properties to blackboard:
|
||||
|
||||
```
|
||||
Attack Node:
|
||||
damage: @blackboard.attackPower
|
||||
target: @blackboard.target
|
||||
```
|
||||
|
||||
## Test Phase
|
||||
|
||||
1. Click Play in the editor
|
||||
2. Watch node execution
|
||||
3. Monitor blackboard values
|
||||
4. Step through execution
|
||||
5. Fix issues and repeat
|
||||
|
||||
## Export Phase
|
||||
|
||||
Export your tree for runtime use:
|
||||
|
||||
```typescript
|
||||
// The exported JSON can be loaded at runtime
|
||||
const treeData = await loadBehaviorTree('assets/enemy-ai.json');
|
||||
```
|
||||
|
||||
## Integration Phase
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeLoader, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
|
||||
// Load exported tree
|
||||
const treeData = await BehaviorTreeLoader.load('enemy-ai.json');
|
||||
|
||||
// Attach to entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(enemy, treeData);
|
||||
```
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "Getting Started"
|
||||
description: "Quick start guide for behavior trees"
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/behavior-tree
|
||||
```
|
||||
|
||||
## Basic Setup
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
|
||||
// Initialize Core
|
||||
Core.create();
|
||||
|
||||
// Install behavior tree plugin
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
// Create and setup scene
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
```
|
||||
|
||||
## Creating Your First Behavior Tree
|
||||
|
||||
### Using Builder API
|
||||
|
||||
```typescript
|
||||
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
|
||||
// Build behavior tree
|
||||
const patrolAI = BehaviorTreeBuilder.create('PatrolAI')
|
||||
.defineBlackboardVariable('targetPosition', null)
|
||||
.sequence('PatrolSequence')
|
||||
.log('Start patrol', 'Patrol')
|
||||
.wait(2000)
|
||||
.log('Move to next point', 'Patrol')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// Attach to entity
|
||||
const entity = scene.createEntity('Guard');
|
||||
BehaviorTreeStarter.start(entity, patrolAI);
|
||||
```
|
||||
|
||||
### Node Types
|
||||
|
||||
| Node Type | Description |
|
||||
|-----------|-------------|
|
||||
| **Sequence** | Executes children in order until one fails |
|
||||
| **Selector** | Tries children until one succeeds |
|
||||
| **Parallel** | Executes all children simultaneously |
|
||||
| **Action** | Performs a specific action |
|
||||
| **Condition** | Checks a condition |
|
||||
| **Decorator** | Modifies child behavior |
|
||||
|
||||
## Blackboard Variables
|
||||
|
||||
The blackboard is a shared data store for behavior tree nodes:
|
||||
|
||||
```typescript
|
||||
const tree = BehaviorTreeBuilder.create('EnemyAI')
|
||||
// Define variables
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.defineBlackboardVariable('isAlert', false)
|
||||
|
||||
.selector('Main')
|
||||
// Use blackboard in conditions
|
||||
.sequence('AttackBranch')
|
||||
.blackboardCompare('health', 30, 'greater')
|
||||
.blackboardCondition('target', (t) => t !== null)
|
||||
.log('Attacking', 'Combat')
|
||||
.end()
|
||||
.log('Retreating', 'Combat')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## Running the Behavior Tree
|
||||
|
||||
```typescript
|
||||
// The behavior tree runs automatically via ECS system
|
||||
// Just create the entity and start the tree
|
||||
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(enemy, patrolAI);
|
||||
|
||||
// Access runtime for debugging
|
||||
const runtime = enemy.getComponent(BehaviorTreeRuntimeComponent);
|
||||
console.log('Current state:', runtime.state);
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Core Concepts](./core-concepts/) - Understand nodes and execution
|
||||
- [Custom Actions](./custom-actions/) - Create your own nodes
|
||||
- [Editor Guide](./editor-guide/) - Visual tree creation
|
||||
95
docs/src/content/docs/en/modules/behavior-tree/index.md
Normal file
95
docs/src/content/docs/en/modules/behavior-tree/index.md
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: "Behavior Tree System"
|
||||
description: "AI behavior trees with runtime executor architecture"
|
||||
---
|
||||
|
||||
Behavior Tree is a powerful tool for game AI and automation control. This framework provides a behavior tree system based on Runtime Executor Architecture, featuring high performance, type safety, and easy extensibility.
|
||||
|
||||
## What is a Behavior Tree?
|
||||
|
||||
A behavior tree is a hierarchical task execution structure composed of multiple nodes, each responsible for specific tasks. Behavior trees are especially suitable for:
|
||||
|
||||
- Game AI (enemies, NPC behavior)
|
||||
- Alternative to state machines
|
||||
- Complex decision logic
|
||||
- Visual behavior design
|
||||
|
||||
## Core Features
|
||||
|
||||
### Runtime Executor Architecture
|
||||
- Data and logic separation
|
||||
- Stateless executor design
|
||||
- High-performance execution
|
||||
- Type safety
|
||||
|
||||
### Visual Editor
|
||||
- Graphical node editing
|
||||
- Real-time preview and debugging
|
||||
- Drag-and-drop node creation
|
||||
- Property connections and bindings
|
||||
|
||||
### Flexible Blackboard System
|
||||
- Local blackboard (single behavior tree)
|
||||
- Global blackboard (shared across all trees)
|
||||
- Type-safe variable access
|
||||
- Property binding support
|
||||
|
||||
## Quick Example
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
BehaviorTreeBuilder,
|
||||
BehaviorTreeStarter,
|
||||
BehaviorTreePlugin
|
||||
} from '@esengine/behavior-tree';
|
||||
|
||||
// Initialize
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
// Create behavior tree
|
||||
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
|
||||
.defineBlackboardVariable('health', 100)
|
||||
.defineBlackboardVariable('target', null)
|
||||
.selector('MainBehavior')
|
||||
// If health is high, attack
|
||||
.sequence('AttackBranch')
|
||||
.blackboardCompare('health', 50, 'greater')
|
||||
.log('Attack player', 'Attack')
|
||||
.end()
|
||||
// Otherwise flee
|
||||
.log('Flee from combat', 'Flee')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// Start AI
|
||||
const entity = scene.createEntity('Enemy');
|
||||
BehaviorTreeStarter.start(entity, enemyAI);
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Getting Started
|
||||
- **[Getting Started](./getting-started/)** - 5-minute quickstart
|
||||
- **[Core Concepts](./core-concepts/)** - Understanding behavior tree fundamentals
|
||||
|
||||
### Editor
|
||||
- **[Editor Guide](./editor-guide/)** - Visual behavior tree creation
|
||||
- **[Editor Workflow](./editor-workflow/)** - Complete development workflow
|
||||
|
||||
### Advanced
|
||||
- **[Asset Management](./asset-management/)** - Loading, managing, and reusing assets
|
||||
- **[Custom Actions](./custom-actions/)** - Create custom behavior nodes
|
||||
- **[Advanced Usage](./advanced-usage/)** - Performance optimization
|
||||
- **[Best Practices](./best-practices/)** - Design patterns and tips
|
||||
|
||||
### Engine Integration
|
||||
- **[Cocos Creator](./cocos-integration/)** - Using with Cocos Creator
|
||||
- **[Laya Engine](./laya-integration/)** - Using with Laya
|
||||
- **[Node.js Server](./nodejs-usage/)** - Server-side usage
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "Laya Engine Integration"
|
||||
description: "Using behavior trees with Laya Engine"
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/behavior-tree @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Basic Integration
|
||||
|
||||
```typescript
|
||||
import { Laya, Script } from 'laya/Laya';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin, BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
|
||||
export class GameMain {
|
||||
private scene: Scene;
|
||||
private plugin: BehaviorTreePlugin;
|
||||
|
||||
async initialize() {
|
||||
// Initialize ECS
|
||||
Core.create();
|
||||
this.plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(this.plugin);
|
||||
|
||||
this.scene = new Scene();
|
||||
this.plugin.setupScene(this.scene);
|
||||
Core.setScene(this.scene);
|
||||
|
||||
// Start game loop
|
||||
Laya.timer.frameLoop(1, this, this.update);
|
||||
}
|
||||
|
||||
update() {
|
||||
const dt = Laya.timer.delta;
|
||||
this.scene?.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI Script Component
|
||||
|
||||
```typescript
|
||||
export class EnemyAI extends Script {
|
||||
private entity: Entity;
|
||||
|
||||
onAwake() {
|
||||
const tree = this.createBehaviorTree();
|
||||
this.entity = GameMain.instance.scene.createEntity('Enemy');
|
||||
|
||||
// Store Laya node reference
|
||||
const runtime = this.entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime.setBlackboardValue('layaNode', this.owner);
|
||||
|
||||
BehaviorTreeStarter.start(this.entity, tree);
|
||||
}
|
||||
|
||||
private createBehaviorTree() {
|
||||
return BehaviorTreeBuilder.create('EnemyAI')
|
||||
.selector('Main')
|
||||
.sequence('Chase')
|
||||
.condition('canSeePlayer')
|
||||
.action('moveToPlayer')
|
||||
.end()
|
||||
.action('idle')
|
||||
.end()
|
||||
.build();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.entity?.destroy();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Actions for Laya
|
||||
|
||||
```typescript
|
||||
@NodeExecutorMetadata({
|
||||
implementationType: 'LayaMoveAction',
|
||||
nodeType: NodeType.Action,
|
||||
displayName: 'Laya Move',
|
||||
category: 'Laya'
|
||||
})
|
||||
export class LayaMoveAction implements INodeExecutor {
|
||||
execute(context: NodeExecutionContext): TaskStatus {
|
||||
const layaNode = context.runtime.getBlackboardValue<Sprite>('layaNode');
|
||||
const target = context.runtime.getBlackboardValue<{x: number, y: number}>('target');
|
||||
|
||||
if (!layaNode || !target) return TaskStatus.Failure;
|
||||
|
||||
const dx = target.x - layaNode.x;
|
||||
const dy = target.y - layaNode.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 5) return TaskStatus.Success;
|
||||
|
||||
const speed = BindingHelper.getValue<number>(context, 'speed', 100);
|
||||
const step = speed * context.deltaTime / 1000;
|
||||
|
||||
layaNode.x += (dx / distance) * step;
|
||||
layaNode.y += (dy / distance) * step;
|
||||
|
||||
return TaskStatus.Running;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use `Laya.timer.delta` for consistent timing
|
||||
2. Store Laya nodes in blackboard for access in executors
|
||||
3. Clean up entities when Laya components are destroyed
|
||||
183
docs/src/content/docs/en/modules/behavior-tree/nodejs-usage.md
Normal file
183
docs/src/content/docs/en/modules/behavior-tree/nodejs-usage.md
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: "Node.js Server Usage"
|
||||
description: "Using behavior trees in server-side applications"
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Game server AI (NPCs, enemies)
|
||||
- Chatbots and conversational AI
|
||||
- Task automation and workflows
|
||||
- Decision-making systems
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
npm install @esengine/behavior-tree @esengine/ecs-framework
|
||||
```
|
||||
|
||||
## Basic Server Setup
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreePlugin, BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
|
||||
|
||||
async function initializeAI() {
|
||||
Core.create();
|
||||
const plugin = new BehaviorTreePlugin();
|
||||
await Core.installPlugin(plugin);
|
||||
|
||||
const scene = new Scene();
|
||||
plugin.setupScene(scene);
|
||||
Core.setScene(scene);
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
// Game loop
|
||||
function startGameLoop(scene: Scene) {
|
||||
const TICK_RATE = 20; // 20 ticks per second
|
||||
const TICK_INTERVAL = 1000 / TICK_RATE;
|
||||
|
||||
setInterval(() => {
|
||||
scene.update(TICK_INTERVAL);
|
||||
}, TICK_INTERVAL);
|
||||
}
|
||||
```
|
||||
|
||||
## NPC AI Example
|
||||
|
||||
```typescript
|
||||
const npcAI = BehaviorTreeBuilder.create('NPCAI')
|
||||
.defineBlackboardVariable('playerId', null)
|
||||
.defineBlackboardVariable('questState', 'idle')
|
||||
|
||||
.selector('MainBehavior')
|
||||
// Handle combat
|
||||
.sequence('Combat')
|
||||
.condition('isUnderAttack')
|
||||
.action('defendSelf')
|
||||
.end()
|
||||
|
||||
// Handle quests
|
||||
.sequence('QuestInteraction')
|
||||
.condition('playerNearby')
|
||||
.action('checkQuestState')
|
||||
.selector('QuestActions')
|
||||
.sequence('GiveQuest')
|
||||
.blackboardCompare('questState', 'idle', 'equals')
|
||||
.action('offerQuest')
|
||||
.end()
|
||||
.sequence('CompleteQuest')
|
||||
.blackboardCompare('questState', 'complete', 'equals')
|
||||
.action('giveReward')
|
||||
.end()
|
||||
.end()
|
||||
.end()
|
||||
|
||||
// Default idle
|
||||
.action('idle')
|
||||
.end()
|
||||
.build();
|
||||
```
|
||||
|
||||
## Chatbot Example
|
||||
|
||||
```typescript
|
||||
const chatbotAI = BehaviorTreeBuilder.create('ChatbotAI')
|
||||
.defineBlackboardVariable('userInput', '')
|
||||
.defineBlackboardVariable('context', {})
|
||||
|
||||
.selector('ProcessInput')
|
||||
// Handle greetings
|
||||
.sequence('Greeting')
|
||||
.condition('isGreeting')
|
||||
.action('respondWithGreeting')
|
||||
.end()
|
||||
|
||||
// Handle questions
|
||||
.sequence('Question')
|
||||
.condition('isQuestion')
|
||||
.action('searchKnowledgeBase')
|
||||
.action('generateAnswer')
|
||||
.end()
|
||||
|
||||
// Handle commands
|
||||
.sequence('Command')
|
||||
.condition('isCommand')
|
||||
.action('executeCommand')
|
||||
.end()
|
||||
|
||||
// Fallback
|
||||
.action('respondWithFallback')
|
||||
.end()
|
||||
.build();
|
||||
|
||||
// Process message
|
||||
function handleMessage(userId: string, message: string) {
|
||||
const entity = getOrCreateUserEntity(userId);
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
|
||||
runtime.setBlackboardValue('userInput', message);
|
||||
scene.update(0); // Process immediately
|
||||
|
||||
return runtime.getBlackboardValue('response');
|
||||
}
|
||||
```
|
||||
|
||||
## Server Integration
|
||||
|
||||
### Express.js
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/api/ai/process', (req, res) => {
|
||||
const { entityId, action, data } = req.body;
|
||||
|
||||
const entity = scene.findEntityById(entityId);
|
||||
if (!entity) {
|
||||
return res.status(404).json({ error: 'Entity not found' });
|
||||
}
|
||||
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime.setBlackboardValue('action', action);
|
||||
runtime.setBlackboardValue('data', data);
|
||||
|
||||
scene.update(0);
|
||||
|
||||
const result = runtime.getBlackboardValue('result');
|
||||
res.json({ result });
|
||||
});
|
||||
```
|
||||
|
||||
### WebSocket
|
||||
|
||||
```typescript
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
const entity = scene.createEntity('Player');
|
||||
BehaviorTreeStarter.start(entity, playerAI);
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data.toString());
|
||||
const runtime = entity.getComponent(BehaviorTreeRuntimeComponent);
|
||||
runtime.setBlackboardValue('input', message);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
entity.destroy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Batch updates** - Process multiple entities per tick
|
||||
2. **Adjust tick rate** - Use lower rates for less time-critical AI
|
||||
3. **Pool entities** - Reuse entities instead of creating/destroying
|
||||
4. **Profile** - Monitor CPU usage and optimize hot paths
|
||||
133
docs/src/content/docs/en/modules/blueprint/composition.md
Normal file
133
docs/src/content/docs/en/modules/blueprint/composition.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Blueprint Composition"
|
||||
description: "Fragments, composer, and triggers"
|
||||
---
|
||||
|
||||
## 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: [...]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Composing 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();
|
||||
```
|
||||
|
||||
## Fragment Registry
|
||||
|
||||
```typescript
|
||||
import { FragmentRegistry } from '@esengine/blueprint';
|
||||
|
||||
const registry = FragmentRegistry.instance;
|
||||
|
||||
// Register fragment
|
||||
registry.register('health', healthFragment);
|
||||
|
||||
// Get fragment
|
||||
const fragment = registry.get('health');
|
||||
|
||||
// Get all fragments
|
||||
const allFragments = registry.getAll();
|
||||
|
||||
// Get by category
|
||||
const combatFragments = registry.getByCategory('combat');
|
||||
```
|
||||
|
||||
## Trigger System
|
||||
|
||||
### Defining 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 }
|
||||
};
|
||||
```
|
||||
|
||||
### Using Trigger Dispatcher
|
||||
|
||||
```typescript
|
||||
const dispatcher = new TriggerDispatcher();
|
||||
|
||||
// Register trigger
|
||||
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
|
||||
context.triggerEvent('OnLowHealth');
|
||||
});
|
||||
|
||||
// Evaluate each frame
|
||||
dispatcher.evaluate(context);
|
||||
```
|
||||
|
||||
### Compound Conditions
|
||||
|
||||
```typescript
|
||||
const complexCondition: TriggerCondition = {
|
||||
type: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'comparison',
|
||||
left: { type: 'variable', name: 'health' },
|
||||
operator: '<',
|
||||
right: { type: 'constant', value: 50 }
|
||||
},
|
||||
{
|
||||
type: 'comparison',
|
||||
left: { type: 'variable', name: 'inCombat' },
|
||||
operator: '==',
|
||||
right: { type: 'constant', value: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## Fragment Best Practices
|
||||
|
||||
1. **Single Responsibility** - Each fragment does one thing
|
||||
2. **Clear Interface** - Name input/output pins clearly
|
||||
3. **Documentation** - Add descriptions and usage examples
|
||||
4. **Version Control** - Maintain backward compatibility when updating
|
||||
128
docs/src/content/docs/en/modules/blueprint/custom-nodes.md
Normal file
128
docs/src/content/docs/en/modules/blueprint/custom-nodes.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: "Custom Nodes"
|
||||
description: "Creating custom blueprint nodes"
|
||||
---
|
||||
|
||||
## Defining 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' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## Implementing Node Executor
|
||||
|
||||
```typescript
|
||||
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
|
||||
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
// Get input
|
||||
const value = context.getInput<number>(node.id, 'value');
|
||||
|
||||
// Execute logic
|
||||
const result = value * 2;
|
||||
|
||||
// Return result
|
||||
return {
|
||||
outputs: { result },
|
||||
nextExec: 'exec' // Continue execution
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registration Methods
|
||||
|
||||
```typescript
|
||||
// Method 1: Using 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';
|
||||
|
||||
// Get singleton
|
||||
const registry = NodeRegistry.instance;
|
||||
|
||||
// Get all templates
|
||||
const allTemplates = registry.getAllTemplates();
|
||||
|
||||
// Get by category
|
||||
const mathNodes = registry.getTemplatesByCategory('math');
|
||||
|
||||
// Search nodes
|
||||
const results = registry.searchTemplates('add');
|
||||
|
||||
// Check existence
|
||||
if (registry.has('MyCustomNode')) { ... }
|
||||
```
|
||||
|
||||
## Pure Nodes
|
||||
|
||||
Pure nodes have no side effects and their outputs are cached:
|
||||
|
||||
```typescript
|
||||
const PureNodeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDistance',
|
||||
title: 'Get Distance',
|
||||
category: 'math',
|
||||
isPure: true, // Mark as pure node
|
||||
inputs: [
|
||||
{ name: 'a', type: 'vector2', direction: 'input' },
|
||||
{ name: 'b', type: 'vector2', direction: 'input' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'distance', type: 'number', direction: 'output' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## Example: Input Handler Node
|
||||
|
||||
```typescript
|
||||
const InputMoveTemplate: BlueprintNodeTemplate = {
|
||||
type: 'InputMove',
|
||||
title: 'Get Movement Input',
|
||||
category: 'input',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'direction', type: 'vector2', direction: 'output' }
|
||||
],
|
||||
isPure: true
|
||||
};
|
||||
|
||||
@RegisterNode(InputMoveTemplate)
|
||||
class InputMoveExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const input = context.scene.services.get(InputServiceToken);
|
||||
const direction = {
|
||||
x: input.getAxis('horizontal'),
|
||||
y: input.getAxis('vertical')
|
||||
};
|
||||
return { outputs: { direction } };
|
||||
}
|
||||
}
|
||||
```
|
||||
149
docs/src/content/docs/en/modules/blueprint/examples.md
Normal file
149
docs/src/content/docs/en/modules/blueprint/examples.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "ECS integration and best practices"
|
||||
---
|
||||
|
||||
## Player Control Blueprint
|
||||
|
||||
```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
|
||||
};
|
||||
|
||||
@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 } };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State Switching Logic
|
||||
|
||||
```typescript
|
||||
// Implement state machine logic in blueprint
|
||||
const stateBlueprint = createEmptyBlueprint('PlayerState');
|
||||
|
||||
// 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',
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
|
||||
{ name: 'target', type: 'entity', direction: 'input' },
|
||||
{ name: 'amount', type: 'number', direction: 'input', defaultValue: 10 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
|
||||
{ name: 'killed', type: 'boolean', direction: 'output' }
|
||||
]
|
||||
};
|
||||
|
||||
@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 health = target.getComponent(HealthComponent);
|
||||
if (health) {
|
||||
health.current -= amount;
|
||||
const killed = health.current <= 0;
|
||||
return {
|
||||
outputs: { killed },
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
|
||||
return { outputs: { killed: false }, nextExec: 'exec' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Fragments for Reusable Logic
|
||||
|
||||
```typescript
|
||||
// Encapsulate common logic as fragments
|
||||
const movementFragment = createFragment('Movement', {
|
||||
inputs: [{ name: 'speed', type: 'number', ... }],
|
||||
outputs: [{ name: 'position', type: 'vector2', ... }],
|
||||
graph: { ... }
|
||||
});
|
||||
|
||||
// Build complex blueprints via composer
|
||||
const composer = createComposer('Player');
|
||||
composer.addFragment(movementFragment, 'movement');
|
||||
composer.addFragment(combatFragment, 'combat');
|
||||
```
|
||||
|
||||
### 2. Use Variable Scopes Appropriately
|
||||
|
||||
```typescript
|
||||
// local: Temporary calculation results
|
||||
{ name: 'tempValue', scope: 'local' }
|
||||
|
||||
// instance: Entity state (e.g., health)
|
||||
{ name: 'health', scope: 'instance' }
|
||||
|
||||
// global: Game-wide state
|
||||
{ name: 'score', scope: 'global' }
|
||||
```
|
||||
|
||||
### 3. Avoid Infinite Loops
|
||||
|
||||
```typescript
|
||||
// VM has max steps per frame limit (default 1000)
|
||||
// Use Delay nodes to break long execution chains
|
||||
vm.maxStepsPerFrame = 1000;
|
||||
```
|
||||
|
||||
### 4. Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Enable debug mode for execution logs
|
||||
vm.debug = true;
|
||||
|
||||
// Use Print nodes for intermediate values
|
||||
// Set breakpoints in editor
|
||||
```
|
||||
|
||||
### 5. Performance Optimization
|
||||
|
||||
```typescript
|
||||
// Pure node outputs are cached
|
||||
{ isPure: true }
|
||||
|
||||
// Avoid heavy computation in Tick
|
||||
// Use event-driven instead of polling
|
||||
```
|
||||
@@ -404,3 +404,11 @@ async function loadBlueprint(path: string): Promise<BlueprintAsset> {
|
||||
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
|
||||
|
||||
107
docs/src/content/docs/en/modules/blueprint/nodes.md
Normal file
107
docs/src/content/docs/en/modules/blueprint/nodes.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: "Built-in Nodes"
|
||||
description: "Blueprint built-in node reference"
|
||||
---
|
||||
|
||||
## Event Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `EventBeginPlay` | Triggered when blueprint starts |
|
||||
| `EventTick` | Triggered each frame |
|
||||
| `EventEndPlay` | Triggered when blueprint stops |
|
||||
| `EventCollision` | Triggered on collision |
|
||||
| `EventInput` | Triggered on input event |
|
||||
| `EventTimer` | Triggered by timer |
|
||||
| `EventMessage` | Triggered by custom message |
|
||||
|
||||
## Flow Control Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `Branch` | Conditional branch (if/else) |
|
||||
| `Sequence` | Execute multiple outputs in sequence |
|
||||
| `ForLoop` | Loop execution |
|
||||
| `WhileLoop` | Conditional loop |
|
||||
| `DoOnce` | Execute only once |
|
||||
| `FlipFlop` | Alternate between two branches |
|
||||
| `Gate` | Toggleable execution gate |
|
||||
|
||||
## Time Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `Delay` | Delay execution |
|
||||
| `GetDeltaTime` | Get frame delta time |
|
||||
| `GetTime` | Get runtime |
|
||||
| `SetTimer` | Set timer |
|
||||
| `ClearTimer` | Clear timer |
|
||||
|
||||
## Math Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `Add` | Addition |
|
||||
| `Subtract` | Subtraction |
|
||||
| `Multiply` | Multiplication |
|
||||
| `Divide` | Division |
|
||||
| `Abs` | Absolute value |
|
||||
| `Clamp` | Clamp to range |
|
||||
| `Lerp` | Linear interpolation |
|
||||
| `Min` / `Max` | Minimum/Maximum |
|
||||
| `Sin` / `Cos` | Trigonometric functions |
|
||||
| `Sqrt` | Square root |
|
||||
| `Power` | Power |
|
||||
|
||||
## Logic Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `And` | Logical AND |
|
||||
| `Or` | Logical OR |
|
||||
| `Not` | Logical NOT |
|
||||
| `Equal` | Equality comparison |
|
||||
| `NotEqual` | Inequality comparison |
|
||||
| `Greater` | Greater than comparison |
|
||||
| `Less` | Less than comparison |
|
||||
|
||||
## Vector Nodes
|
||||
|
||||
| Node | Description |
|
||||
|------|-------------|
|
||||
| `MakeVector2` | Create 2D vector |
|
||||
| `BreakVector2` | Break 2D vector |
|
||||
| `VectorAdd` | Vector addition |
|
||||
| `VectorSubtract` | Vector subtraction |
|
||||
| `VectorMultiply` | Vector multiplication |
|
||||
| `VectorLength` | Vector length |
|
||||
| `VectorNormalize` | Vector normalization |
|
||||
| `VectorDistance` | Vector distance |
|
||||
|
||||
## 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 |
|
||||
133
docs/src/content/docs/en/modules/blueprint/vm.md
Normal file
133
docs/src/content/docs/en/modules/blueprint/vm.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Virtual Machine API"
|
||||
description: "BlueprintVM execution and context"
|
||||
---
|
||||
|
||||
## BlueprintVM
|
||||
|
||||
The blueprint virtual machine executes blueprint graphs:
|
||||
|
||||
```typescript
|
||||
import { BlueprintVM } from '@esengine/blueprint';
|
||||
|
||||
// Create VM
|
||||
const vm = new BlueprintVM(blueprintAsset, entity, scene);
|
||||
|
||||
// Start (triggers BeginPlay)
|
||||
vm.start();
|
||||
|
||||
// Update each frame (triggers Tick)
|
||||
vm.tick(deltaTime);
|
||||
|
||||
// Stop (triggers EndPlay)
|
||||
vm.stop();
|
||||
|
||||
// Pause/Resume
|
||||
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; // Blueprint asset
|
||||
entity: Entity; // Current entity
|
||||
scene: IScene; // Current scene
|
||||
deltaTime: number; // Frame delta time
|
||||
time: number; // Total runtime
|
||||
|
||||
// Get input value
|
||||
getInput<T>(nodeId: string, pinName: string): T;
|
||||
|
||||
// Set output value
|
||||
setOutput(nodeId: string, pinName: string, value: unknown): void;
|
||||
|
||||
// Variable access
|
||||
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 execution pin
|
||||
delay?: number; // Delay execution (ms)
|
||||
yield?: boolean; // Pause until next frame
|
||||
error?: string; // Error message
|
||||
}
|
||||
```
|
||||
|
||||
## 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) {
|
||||
// Process all entities with blueprint components
|
||||
this.blueprintSystem.process(this.entities, dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 });
|
||||
```
|
||||
|
||||
## Serialization
|
||||
|
||||
### Saving Blueprints
|
||||
|
||||
```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);
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Blueprints
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
158
docs/src/content/docs/en/modules/fsm/api.md
Normal file
158
docs/src/content/docs/en/modules/fsm/api.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: "API Reference"
|
||||
description: "Complete FSM API documentation"
|
||||
---
|
||||
|
||||
## createStateMachine
|
||||
|
||||
```typescript
|
||||
function createStateMachine<TContext = unknown>(
|
||||
config: StateMachineConfig<TContext>
|
||||
): StateMachine<TContext>
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
interface StateMachineConfig<TContext> {
|
||||
initial: string;
|
||||
context?: TContext;
|
||||
states: Record<string, StateDefinition<TContext>>;
|
||||
onTransition?: (from: string, to: string, event: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## StateMachine Instance
|
||||
|
||||
### Properties
|
||||
|
||||
```typescript
|
||||
// Current state
|
||||
fsm.current: string
|
||||
|
||||
// Context object
|
||||
fsm.context: TContext
|
||||
|
||||
// Get state definition
|
||||
fsm.getState(name: string): StateDefinition | undefined
|
||||
|
||||
// Check if in specific state
|
||||
fsm.is(state: string): boolean
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
```typescript
|
||||
// Send event to trigger transition
|
||||
fsm.send(event: string): boolean
|
||||
|
||||
// Force transition (no guards, no event matching)
|
||||
fsm.transitionTo(state: string): void
|
||||
|
||||
// Update current state (call onUpdate if exists)
|
||||
fsm.update(dt: number): void
|
||||
|
||||
// Reset to initial state
|
||||
fsm.reset(): void
|
||||
```
|
||||
|
||||
## State Definition
|
||||
|
||||
```typescript
|
||||
interface StateDefinition<TContext> {
|
||||
// Called on entering state
|
||||
onEnter?: (context: TContext) => void;
|
||||
|
||||
// Called on exiting state
|
||||
onExit?: (context: TContext) => void;
|
||||
|
||||
// Called on each update
|
||||
onUpdate?: (dt: number, context: TContext) => void;
|
||||
|
||||
// Valid transitions from this state
|
||||
transitions: Record<string, string | TransitionConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
## Transition Configuration
|
||||
|
||||
```typescript
|
||||
interface TransitionConfig {
|
||||
target: string;
|
||||
guard?: (context: TContext) => boolean;
|
||||
action?: (context: TContext) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
interface PlayerContext {
|
||||
health: number;
|
||||
stamina: number;
|
||||
isGrounded: boolean;
|
||||
}
|
||||
|
||||
const playerFSM = createStateMachine<PlayerContext>({
|
||||
initial: 'idle',
|
||||
context: {
|
||||
health: 100,
|
||||
stamina: 100,
|
||||
isGrounded: true
|
||||
},
|
||||
states: {
|
||||
idle: {
|
||||
onEnter: (ctx) => console.log('Idle'),
|
||||
onUpdate: (dt, ctx) => {
|
||||
ctx.stamina = Math.min(100, ctx.stamina + dt * 10);
|
||||
},
|
||||
transitions: {
|
||||
move: 'walking',
|
||||
jump: {
|
||||
target: 'jumping',
|
||||
guard: (ctx) => ctx.isGrounded && ctx.stamina >= 20
|
||||
},
|
||||
attack: {
|
||||
target: 'attacking',
|
||||
guard: (ctx) => ctx.stamina >= 30
|
||||
}
|
||||
}
|
||||
},
|
||||
walking: {
|
||||
onUpdate: (dt, ctx) => {
|
||||
ctx.stamina = Math.max(0, ctx.stamina - dt * 5);
|
||||
},
|
||||
transitions: {
|
||||
stop: 'idle',
|
||||
jump: {
|
||||
target: 'jumping',
|
||||
guard: (ctx) => ctx.isGrounded
|
||||
}
|
||||
}
|
||||
},
|
||||
jumping: {
|
||||
onEnter: (ctx) => {
|
||||
ctx.isGrounded = false;
|
||||
ctx.stamina -= 20;
|
||||
},
|
||||
transitions: {
|
||||
land: {
|
||||
target: 'idle',
|
||||
action: (ctx) => { ctx.isGrounded = true; }
|
||||
}
|
||||
}
|
||||
},
|
||||
attacking: {
|
||||
onEnter: (ctx) => {
|
||||
ctx.stamina -= 30;
|
||||
},
|
||||
transitions: {
|
||||
finish: 'idle'
|
||||
}
|
||||
}
|
||||
},
|
||||
onTransition: (from, to, event) => {
|
||||
console.log(`${from} -> ${to} (${event})`);
|
||||
}
|
||||
});
|
||||
```
|
||||
253
docs/src/content/docs/en/modules/fsm/examples.md
Normal file
253
docs/src/content/docs/en/modules/fsm/examples.md
Normal file
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "Character FSM, AI behavior, ECS integration"
|
||||
---
|
||||
|
||||
## Character State Machine
|
||||
|
||||
```typescript
|
||||
interface CharacterContext {
|
||||
health: number;
|
||||
stamina: number;
|
||||
isGrounded: boolean;
|
||||
velocity: { x: number; y: number };
|
||||
}
|
||||
|
||||
const characterFSM = createStateMachine<CharacterContext>({
|
||||
initial: 'idle',
|
||||
context: {
|
||||
health: 100,
|
||||
stamina: 100,
|
||||
isGrounded: true,
|
||||
velocity: { x: 0, y: 0 }
|
||||
},
|
||||
states: {
|
||||
idle: {
|
||||
onEnter: (ctx) => {
|
||||
ctx.velocity.x = 0;
|
||||
},
|
||||
onUpdate: (dt, ctx) => {
|
||||
// Recover stamina
|
||||
ctx.stamina = Math.min(100, ctx.stamina + dt * 10);
|
||||
},
|
||||
transitions: {
|
||||
move: 'running',
|
||||
jump: { target: 'jumping', guard: ctx => ctx.isGrounded },
|
||||
hurt: 'hit',
|
||||
die: { target: 'dead', guard: ctx => ctx.health <= 0 }
|
||||
}
|
||||
},
|
||||
running: {
|
||||
onUpdate: (dt, ctx) => {
|
||||
ctx.stamina = Math.max(0, ctx.stamina - dt * 5);
|
||||
},
|
||||
transitions: {
|
||||
stop: 'idle',
|
||||
jump: { target: 'jumping', guard: ctx => ctx.isGrounded },
|
||||
hurt: 'hit',
|
||||
exhaust: { target: 'idle', guard: ctx => ctx.stamina <= 0 }
|
||||
}
|
||||
},
|
||||
jumping: {
|
||||
onEnter: (ctx) => {
|
||||
ctx.velocity.y = -10;
|
||||
ctx.isGrounded = false;
|
||||
ctx.stamina -= 20;
|
||||
},
|
||||
transitions: {
|
||||
land: { target: 'idle', action: ctx => { ctx.isGrounded = true; } },
|
||||
hurt: 'hit'
|
||||
}
|
||||
},
|
||||
hit: {
|
||||
onEnter: (ctx) => {
|
||||
// Play hit animation
|
||||
},
|
||||
transitions: {
|
||||
recover: 'idle',
|
||||
die: { target: 'dead', guard: ctx => ctx.health <= 0 }
|
||||
}
|
||||
},
|
||||
dead: {
|
||||
onEnter: (ctx) => {
|
||||
ctx.velocity = { x: 0, y: 0 };
|
||||
// Play death animation
|
||||
},
|
||||
transitions: {
|
||||
respawn: {
|
||||
target: 'idle',
|
||||
action: (ctx) => {
|
||||
ctx.health = 100;
|
||||
ctx.stamina = 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## ECS Integration
|
||||
|
||||
```typescript
|
||||
import { System } from '@esengine/ecs-framework';
|
||||
import { createStateMachine } from '@esengine/fsm';
|
||||
|
||||
// FSM Component
|
||||
class FSMComponent extends Component {
|
||||
fsm: StateMachine<any>;
|
||||
}
|
||||
|
||||
// FSM System
|
||||
class FSMSystem extends System {
|
||||
query = this.world.query([FSMComponent]);
|
||||
|
||||
update(dt: number): void {
|
||||
for (const entity of this.query.entities) {
|
||||
const fsm = entity.getComponent(FSMComponent).fsm;
|
||||
fsm.update(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const entity = world.createEntity();
|
||||
const fsmComp = entity.addComponent(FSMComponent);
|
||||
fsmComp.fsm = createStateMachine({
|
||||
initial: 'patrol',
|
||||
states: {
|
||||
patrol: { /* ... */ },
|
||||
chase: { /* ... */ },
|
||||
attack: { /* ... */ }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## AI Behavior Example
|
||||
|
||||
```typescript
|
||||
interface EnemyAIContext {
|
||||
entity: Entity;
|
||||
target: Entity | null;
|
||||
alertLevel: number;
|
||||
lastKnownPosition: { x: number; y: number } | null;
|
||||
}
|
||||
|
||||
const enemyAI = createStateMachine<EnemyAIContext>({
|
||||
initial: 'patrol',
|
||||
context: {
|
||||
entity: enemyEntity,
|
||||
target: null,
|
||||
alertLevel: 0,
|
||||
lastKnownPosition: null
|
||||
},
|
||||
states: {
|
||||
patrol: {
|
||||
onUpdate: (dt, ctx) => {
|
||||
// Patrol logic
|
||||
if (detectPlayer(ctx.entity)) {
|
||||
ctx.target = player;
|
||||
ctx.alertLevel = 100;
|
||||
}
|
||||
},
|
||||
transitions: {
|
||||
detect: { target: 'chase', guard: ctx => ctx.target !== null }
|
||||
}
|
||||
},
|
||||
chase: {
|
||||
onUpdate: (dt, ctx) => {
|
||||
if (ctx.target) {
|
||||
moveToward(ctx.entity, ctx.target.position);
|
||||
ctx.lastKnownPosition = { ...ctx.target.position };
|
||||
|
||||
if (distanceTo(ctx.entity, ctx.target) < 2) {
|
||||
ctx.entity.fsm.send('inRange');
|
||||
}
|
||||
}
|
||||
ctx.alertLevel -= dt * 10;
|
||||
},
|
||||
transitions: {
|
||||
inRange: 'attack',
|
||||
lostTarget: {
|
||||
target: 'search',
|
||||
guard: ctx => !canSee(ctx.entity, ctx.target)
|
||||
},
|
||||
giveUp: {
|
||||
target: 'patrol',
|
||||
guard: ctx => ctx.alertLevel <= 0
|
||||
}
|
||||
}
|
||||
},
|
||||
attack: {
|
||||
onEnter: (ctx) => {
|
||||
performAttack(ctx.entity);
|
||||
},
|
||||
transitions: {
|
||||
cooldown: 'chase',
|
||||
targetDead: {
|
||||
target: 'patrol',
|
||||
guard: ctx => ctx.target?.getComponent(Health)?.value <= 0
|
||||
}
|
||||
}
|
||||
},
|
||||
search: {
|
||||
onUpdate: (dt, ctx) => {
|
||||
if (ctx.lastKnownPosition) {
|
||||
moveToward(ctx.entity, ctx.lastKnownPosition);
|
||||
}
|
||||
ctx.alertLevel -= dt * 5;
|
||||
},
|
||||
transitions: {
|
||||
found: { target: 'chase', guard: ctx => canSee(ctx.entity, ctx.target) },
|
||||
giveUp: { target: 'patrol', guard: ctx => ctx.alertLevel <= 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Animation State Machine
|
||||
|
||||
```typescript
|
||||
const animationFSM = createStateMachine({
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
onEnter: () => playAnimation('idle', { loop: true }),
|
||||
transitions: {
|
||||
walk: 'walking',
|
||||
run: 'running',
|
||||
jump: 'jumping'
|
||||
}
|
||||
},
|
||||
walking: {
|
||||
onEnter: () => playAnimation('walk', { loop: true }),
|
||||
transitions: {
|
||||
stop: 'idle',
|
||||
run: 'running',
|
||||
jump: 'jumping'
|
||||
}
|
||||
},
|
||||
running: {
|
||||
onEnter: () => playAnimation('run', { loop: true }),
|
||||
transitions: {
|
||||
stop: 'idle',
|
||||
walk: 'walking',
|
||||
jump: 'jumping'
|
||||
}
|
||||
},
|
||||
jumping: {
|
||||
onEnter: () => playAnimation('jump', { loop: false }),
|
||||
transitions: {
|
||||
land: 'landing'
|
||||
}
|
||||
},
|
||||
landing: {
|
||||
onEnter: () => playAnimation('land', { loop: false }),
|
||||
transitions: {
|
||||
complete: 'idle'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -316,3 +316,8 @@ The FSM module provides blueprint nodes for visual scripting:
|
||||
- `GetStateDuration` - Get state duration
|
||||
- `EvaluateTransitions` - Evaluate transition conditions
|
||||
- `ResetStateMachine` - Reset state machine
|
||||
|
||||
## Documentation
|
||||
|
||||
- [API Reference](./api) - Complete API documentation
|
||||
- [Examples](./examples) - Character FSM, AI behavior, ECS integration
|
||||
|
||||
164
docs/src/content/docs/en/modules/pathfinding/examples.md
Normal file
164
docs/src/content/docs/en/modules/pathfinding/examples.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "Game movement, dynamic obstacles, hierarchical pathfinding"
|
||||
---
|
||||
|
||||
## Game Character Movement
|
||||
|
||||
```typescript
|
||||
class MovementSystem {
|
||||
private grid: GridMap;
|
||||
private pathfinder: AStarPathfinder;
|
||||
private smoother: CombinedSmoother;
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
this.grid = createGridMap(width, height);
|
||||
this.pathfinder = createAStarPathfinder(this.grid);
|
||||
this.smoother = createCombinedSmoother();
|
||||
}
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPoint[] | null {
|
||||
const result = this.pathfinder.findPath(
|
||||
from.x, from.y,
|
||||
to.x, to.y
|
||||
);
|
||||
|
||||
if (!result.found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Smooth path
|
||||
return this.smoother.smooth(result.path, this.grid);
|
||||
}
|
||||
|
||||
setObstacle(x: number, y: number): void {
|
||||
this.grid.setWalkable(x, y, false);
|
||||
}
|
||||
|
||||
setTerrain(x: number, y: number, cost: number): void {
|
||||
this.grid.setCost(x, y, cost);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Obstacles
|
||||
|
||||
```typescript
|
||||
class DynamicPathfinding {
|
||||
private grid: GridMap;
|
||||
private pathfinder: AStarPathfinder;
|
||||
private dynamicObstacles: Set<string> = new Set();
|
||||
|
||||
addDynamicObstacle(x: number, y: number): void {
|
||||
const key = `${x},${y}`;
|
||||
if (!this.dynamicObstacles.has(key)) {
|
||||
this.dynamicObstacles.add(key);
|
||||
this.grid.setWalkable(x, y, false);
|
||||
}
|
||||
}
|
||||
|
||||
removeDynamicObstacle(x: number, y: number): void {
|
||||
const key = `${x},${y}`;
|
||||
if (this.dynamicObstacles.has(key)) {
|
||||
this.dynamicObstacles.delete(key);
|
||||
this.grid.setWalkable(x, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPathResult {
|
||||
return this.pathfinder.findPath(from.x, from.y, to.x, to.y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Different Terrain Costs
|
||||
|
||||
```typescript
|
||||
// Set different terrain movement costs
|
||||
const grid = createGridMap(50, 50);
|
||||
|
||||
// Normal ground - cost 1 (default)
|
||||
// Sand - cost 2
|
||||
for (let y = 10; y < 20; y++) {
|
||||
for (let x = 0; x < 50; x++) {
|
||||
grid.setCost(x, y, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Swamp - cost 4
|
||||
for (let y = 30; y < 35; y++) {
|
||||
for (let x = 20; x < 30; x++) {
|
||||
grid.setCost(x, y, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Pathfinding automatically considers terrain costs
|
||||
const result = pathfinder.findPath(0, 0, 49, 49);
|
||||
```
|
||||
|
||||
## Hierarchical Pathfinding
|
||||
|
||||
For large maps, use hierarchical pathfinding:
|
||||
|
||||
```typescript
|
||||
class HierarchicalPathfinding {
|
||||
private coarseGrid: GridMap; // Coarse grid
|
||||
private fineGrid: GridMap; // Fine grid
|
||||
private coarsePathfinder: AStarPathfinder;
|
||||
private finePathfinder: AStarPathfinder;
|
||||
private cellSize = 10;
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPoint[] {
|
||||
// 1. Pathfind on coarse grid
|
||||
const coarseFrom = this.toCoarse(from);
|
||||
const coarseTo = this.toCoarse(to);
|
||||
const coarseResult = this.coarsePathfinder.findPath(
|
||||
coarseFrom.x, coarseFrom.y,
|
||||
coarseTo.x, coarseTo.y
|
||||
);
|
||||
|
||||
if (!coarseResult.found) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Fine pathfind within each coarse cell
|
||||
const finePath: IPoint[] = [];
|
||||
// ... detailed implementation
|
||||
return finePath;
|
||||
}
|
||||
|
||||
private toCoarse(p: IPoint): IPoint {
|
||||
return {
|
||||
x: Math.floor(p.x / this.cellSize),
|
||||
y: Math.floor(p.y / this.cellSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
1. **Limit Search Range**
|
||||
```typescript
|
||||
pathfinder.findPath(x1, y1, x2, y2, { maxNodes: 1000 });
|
||||
```
|
||||
|
||||
2. **Use Heuristic Weight**
|
||||
```typescript
|
||||
// Weight > 1 is faster but may not be optimal
|
||||
pathfinder.findPath(x1, y1, x2, y2, { heuristicWeight: 1.5 });
|
||||
```
|
||||
|
||||
3. **Reuse Pathfinder Instance**
|
||||
```typescript
|
||||
// Create once, use many times
|
||||
const pathfinder = createAStarPathfinder(grid);
|
||||
```
|
||||
|
||||
4. **Use Navigation Mesh**
|
||||
- For complex terrain, NavMesh is more efficient than grid pathfinding
|
||||
- Polygon count is much less than grid cell count
|
||||
|
||||
5. **Choose Appropriate Heuristic**
|
||||
- 4-direction movement: use `manhattanDistance`
|
||||
- 8-direction movement: use `octileDistance` (default)
|
||||
112
docs/src/content/docs/en/modules/pathfinding/grid-map.md
Normal file
112
docs/src/content/docs/en/modules/pathfinding/grid-map.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Grid Map API"
|
||||
description: "Grid operations and A* pathfinder"
|
||||
---
|
||||
|
||||
## createGridMap
|
||||
|
||||
```typescript
|
||||
function createGridMap(
|
||||
width: number,
|
||||
height: number,
|
||||
options?: IGridMapOptions
|
||||
): GridMap
|
||||
```
|
||||
|
||||
**Configuration Options:**
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `allowDiagonal` | `boolean` | `true` | Allow diagonal movement |
|
||||
| `diagonalCost` | `number` | `√2` | Diagonal movement cost |
|
||||
| `avoidCorners` | `boolean` | `true` | Avoid corner cutting |
|
||||
| `heuristic` | `HeuristicFunction` | `octileDistance` | Heuristic function |
|
||||
|
||||
## Map Operations
|
||||
|
||||
```typescript
|
||||
// Check/set walkability
|
||||
grid.isWalkable(x, y);
|
||||
grid.setWalkable(x, y, false);
|
||||
|
||||
// Set movement cost (e.g., swamp, sand)
|
||||
grid.setCost(x, y, 2); // Cost of 2 (default 1)
|
||||
|
||||
// Set rectangular area
|
||||
grid.setRectWalkable(0, 0, 5, 5, false);
|
||||
|
||||
// Load from array (0=walkable, non-0=obstacle)
|
||||
grid.loadFromArray([
|
||||
[0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0]
|
||||
]);
|
||||
|
||||
// Load from string (.=walkable, #=obstacle)
|
||||
grid.loadFromString(`
|
||||
.....
|
||||
.#.#.
|
||||
.#...
|
||||
`);
|
||||
|
||||
// Export as string
|
||||
console.log(grid.toString());
|
||||
|
||||
// Reset all nodes to walkable
|
||||
grid.reset();
|
||||
```
|
||||
|
||||
## A* Pathfinder
|
||||
|
||||
### createAStarPathfinder
|
||||
|
||||
```typescript
|
||||
function createAStarPathfinder(map: IPathfindingMap): AStarPathfinder
|
||||
```
|
||||
|
||||
### findPath
|
||||
|
||||
```typescript
|
||||
const result = pathfinder.findPath(
|
||||
startX, startY,
|
||||
endX, endY,
|
||||
{
|
||||
maxNodes: 5000, // Limit search nodes
|
||||
heuristicWeight: 1.5 // Faster but may not be optimal
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Reusing Pathfinder
|
||||
|
||||
```typescript
|
||||
// Pathfinder is reusable, clears state automatically
|
||||
pathfinder.findPath(0, 0, 10, 10);
|
||||
pathfinder.findPath(5, 5, 15, 15);
|
||||
|
||||
// Manual clear (optional)
|
||||
pathfinder.clear();
|
||||
```
|
||||
|
||||
## Direction Constants
|
||||
|
||||
```typescript
|
||||
import { DIRECTIONS_4, DIRECTIONS_8 } from '@esengine/pathfinding';
|
||||
|
||||
// 4 directions (up, down, left, right)
|
||||
DIRECTIONS_4 // [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, ...]
|
||||
|
||||
// 8 directions (includes diagonals)
|
||||
DIRECTIONS_8 // [{ dx: 0, dy: -1 }, { dx: 1, dy: -1 }, ...]
|
||||
```
|
||||
|
||||
## Heuristic Functions
|
||||
|
||||
```typescript
|
||||
import { manhattanDistance, octileDistance } from '@esengine/pathfinding';
|
||||
|
||||
// Custom heuristic
|
||||
const grid = createGridMap(20, 20, {
|
||||
heuristic: manhattanDistance // Use Manhattan distance
|
||||
});
|
||||
```
|
||||
@@ -299,3 +299,10 @@ for (let y = 30; y < 35; y++) {
|
||||
| Precision | Grid-aligned | Continuous coordinates |
|
||||
| Dynamic Updates | Easy | Requires rebuild |
|
||||
| Setup Complexity | Simple | More complex |
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Grid Map API](./grid-map) - Grid operations and A* pathfinder
|
||||
- [Navigation Mesh API](./navmesh) - NavMesh building and querying
|
||||
- [Path Smoothing](./smoothing) - Line of sight and curve smoothing
|
||||
- [Examples](./examples) - Game movement, dynamic obstacles, hierarchical pathfinding
|
||||
|
||||
67
docs/src/content/docs/en/modules/pathfinding/navmesh.md
Normal file
67
docs/src/content/docs/en/modules/pathfinding/navmesh.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Navigation Mesh API"
|
||||
description: "NavMesh building and querying"
|
||||
---
|
||||
|
||||
## createNavMesh
|
||||
|
||||
```typescript
|
||||
function createNavMesh(): NavMesh
|
||||
```
|
||||
|
||||
## Building Navigation Mesh
|
||||
|
||||
```typescript
|
||||
const navmesh = createNavMesh();
|
||||
|
||||
// Add convex polygons
|
||||
const id1 = navmesh.addPolygon([
|
||||
{ x: 0, y: 0 }, { x: 10, y: 0 },
|
||||
{ x: 10, y: 10 }, { x: 0, y: 10 }
|
||||
]);
|
||||
|
||||
const id2 = navmesh.addPolygon([
|
||||
{ x: 10, y: 0 }, { x: 20, y: 0 },
|
||||
{ x: 20, y: 10 }, { x: 10, y: 10 }
|
||||
]);
|
||||
|
||||
// Method 1: Auto-detect shared edges and build connections
|
||||
navmesh.build();
|
||||
|
||||
// Method 2: Manually set connections
|
||||
navmesh.setConnection(id1, id2, {
|
||||
left: { x: 10, y: 0 },
|
||||
right: { x: 10, y: 10 }
|
||||
});
|
||||
```
|
||||
|
||||
## Querying and Pathfinding
|
||||
|
||||
```typescript
|
||||
// Find polygon containing point
|
||||
const polygon = navmesh.findPolygonAt(5, 5);
|
||||
|
||||
// Check if position is walkable
|
||||
navmesh.isWalkable(5, 5);
|
||||
|
||||
// Pathfinding (uses funnel algorithm internally)
|
||||
const result = navmesh.findPath(1, 1, 18, 8);
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
Navigation mesh is suitable for:
|
||||
- Complex irregular terrain
|
||||
- Scenarios requiring precise paths
|
||||
- Large maps where polygon count is much less than grid cells
|
||||
|
||||
```typescript
|
||||
// Load navigation mesh data from editor
|
||||
const navData = await loadNavMeshData('level1.navmesh');
|
||||
|
||||
const navmesh = createNavMesh();
|
||||
for (const poly of navData.polygons) {
|
||||
navmesh.addPolygon(poly.vertices);
|
||||
}
|
||||
navmesh.build();
|
||||
```
|
||||
67
docs/src/content/docs/en/modules/pathfinding/smoothing.md
Normal file
67
docs/src/content/docs/en/modules/pathfinding/smoothing.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Path Smoothing"
|
||||
description: "Line of sight simplification and curve smoothing"
|
||||
---
|
||||
|
||||
## Line of Sight Simplification
|
||||
|
||||
Remove unnecessary intermediate points:
|
||||
|
||||
```typescript
|
||||
import { createLineOfSightSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createLineOfSightSmoother();
|
||||
const smoothedPath = smoother.smooth(result.path, grid);
|
||||
|
||||
// Original path: [(0,0), (1,1), (2,2), (3,3), (4,4)]
|
||||
// Simplified: [(0,0), (4,4)]
|
||||
```
|
||||
|
||||
## Curve Smoothing
|
||||
|
||||
Using Catmull-Rom splines:
|
||||
|
||||
```typescript
|
||||
import { createCatmullRomSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createCatmullRomSmoother(
|
||||
5, // segments - interpolation points per segment
|
||||
0.5 // tension - (0-1)
|
||||
);
|
||||
|
||||
const curvedPath = smoother.smooth(result.path, grid);
|
||||
```
|
||||
|
||||
## Combined Smoothing
|
||||
|
||||
Simplify first, then curve smooth:
|
||||
|
||||
```typescript
|
||||
import { createCombinedSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createCombinedSmoother(5, 0.5);
|
||||
const finalPath = smoother.smooth(result.path, grid);
|
||||
```
|
||||
|
||||
## Line of Sight Functions
|
||||
|
||||
```typescript
|
||||
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
|
||||
|
||||
// Bresenham algorithm (fast, grid-aligned)
|
||||
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
|
||||
|
||||
// Raycast (precise, supports floating point coordinates)
|
||||
const hasLOS = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
- `FindPath` - Find path
|
||||
- `FindPathSmooth` - Find and smooth path
|
||||
- `IsWalkable` - Check if position is walkable
|
||||
- `GetPathLength` - Get number of path points
|
||||
- `GetPathDistance` - Get total path distance
|
||||
- `GetPathPoint` - Get point at index
|
||||
- `MoveAlongPath` - Move along path
|
||||
- `HasLineOfSight` - Check line of sight
|
||||
230
docs/src/content/docs/en/modules/procgen/examples.md
Normal file
230
docs/src/content/docs/en/modules/procgen/examples.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "Terrain, loot, enemies, and level generation"
|
||||
---
|
||||
|
||||
## Procedural Terrain Generation
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||||
|
||||
class TerrainGenerator {
|
||||
private fbm: FBM;
|
||||
private moistureFbm: FBM;
|
||||
|
||||
constructor(seed: number) {
|
||||
const heightNoise = createPerlinNoise(seed);
|
||||
const moistureNoise = createPerlinNoise(seed + 1000);
|
||||
|
||||
this.fbm = createFBM(heightNoise, {
|
||||
octaves: 8,
|
||||
persistence: 0.5,
|
||||
frequency: 0.01
|
||||
});
|
||||
|
||||
this.moistureFbm = createFBM(moistureNoise, {
|
||||
octaves: 4,
|
||||
persistence: 0.6,
|
||||
frequency: 0.02
|
||||
});
|
||||
}
|
||||
|
||||
getHeight(x: number, y: number): number {
|
||||
// Base height
|
||||
let height = this.fbm.noise2D(x, y);
|
||||
|
||||
// Add mountains
|
||||
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
|
||||
|
||||
return (height + 1) * 0.5; // Normalize to [0, 1]
|
||||
}
|
||||
|
||||
getBiome(x: number, y: number): string {
|
||||
const height = this.getHeight(x, y);
|
||||
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
|
||||
|
||||
if (height < 0.3) return 'water';
|
||||
if (height < 0.4) return 'beach';
|
||||
if (height > 0.8) return 'mountain';
|
||||
|
||||
if (moisture < 0.3) return 'desert';
|
||||
if (moisture > 0.7) return 'forest';
|
||||
return 'grassland';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Loot System
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom, createWeightedRandom, pickOne } from '@esengine/procgen';
|
||||
|
||||
interface LootItem {
|
||||
id: string;
|
||||
rarity: string;
|
||||
}
|
||||
|
||||
class LootSystem {
|
||||
private rng: SeededRandom;
|
||||
private raritySelector: WeightedRandom<string>;
|
||||
private lootTables: Map<string, LootItem[]> = new Map();
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
|
||||
this.raritySelector = createWeightedRandom([
|
||||
{ value: 'common', weight: 60 },
|
||||
{ value: 'uncommon', weight: 25 },
|
||||
{ value: 'rare', weight: 10 },
|
||||
{ value: 'legendary', weight: 5 }
|
||||
]);
|
||||
|
||||
// Initialize loot tables
|
||||
this.lootTables.set('common', [/* ... */]);
|
||||
this.lootTables.set('rare', [/* ... */]);
|
||||
}
|
||||
|
||||
generateLoot(count: number): LootItem[] {
|
||||
const loot: LootItem[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const rarity = this.raritySelector.pick(this.rng);
|
||||
const table = this.lootTables.get(rarity)!;
|
||||
const item = pickOne(table, this.rng);
|
||||
loot.push(item);
|
||||
}
|
||||
|
||||
return loot;
|
||||
}
|
||||
|
||||
// Ensure reproducibility
|
||||
setSeed(seed: number): void {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Procedural Enemy Placement
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom } from '@esengine/procgen';
|
||||
|
||||
class EnemySpawner {
|
||||
private rng: SeededRandom;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
|
||||
spawnEnemiesInArea(
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
radius: number,
|
||||
count: number
|
||||
): Array<{ x: number; y: number; type: string }> {
|
||||
const enemies: Array<{ x: number; y: number; type: string }> = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Generate position in circle
|
||||
const pos = this.rng.nextPointInCircle(radius);
|
||||
|
||||
// Randomly select enemy type
|
||||
const type = this.rng.nextBool(0.2) ? 'elite' : 'normal';
|
||||
|
||||
enemies.push({
|
||||
x: centerX + pos.x,
|
||||
y: centerY + pos.y,
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
return enemies;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Procedural Level Layout
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom, shuffle } from '@esengine/procgen';
|
||||
|
||||
interface Room {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
type: 'start' | 'combat' | 'treasure' | 'boss';
|
||||
}
|
||||
|
||||
class DungeonGenerator {
|
||||
private rng: SeededRandom;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
|
||||
generate(roomCount: number): Room[] {
|
||||
const rooms: Room[] = [];
|
||||
|
||||
// Generate rooms
|
||||
for (let i = 0; i < roomCount; i++) {
|
||||
rooms.push({
|
||||
x: this.rng.nextInt(0, 100),
|
||||
y: this.rng.nextInt(0, 100),
|
||||
width: this.rng.nextInt(5, 15),
|
||||
height: this.rng.nextInt(5, 15),
|
||||
type: 'combat'
|
||||
});
|
||||
}
|
||||
|
||||
// Randomly assign special rooms
|
||||
shuffle(rooms, this.rng);
|
||||
rooms[0].type = 'start';
|
||||
rooms[1].type = 'treasure';
|
||||
rooms[rooms.length - 1].type = 'boss';
|
||||
|
||||
return rooms;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
Procgen module provides blueprint nodes for visual scripting:
|
||||
|
||||
### Noise Nodes
|
||||
|
||||
- `SampleNoise2D` - Sample 2D noise
|
||||
- `SampleFBM` - Sample FBM noise
|
||||
|
||||
### Random Nodes
|
||||
|
||||
- `SeededRandom` - Generate random float
|
||||
- `SeededRandomInt` - Generate random integer
|
||||
- `WeightedPick` - Weighted random selection
|
||||
- `ShuffleArray` - Shuffle array
|
||||
- `PickRandom` - Pick random element
|
||||
- `SampleArray` - Sample from array
|
||||
- `RandomPointInCircle` - Random point in circle
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use seeds for reproducibility**
|
||||
```typescript
|
||||
const seed = Date.now();
|
||||
const rng = createSeededRandom(seed);
|
||||
saveSeed(seed);
|
||||
```
|
||||
|
||||
2. **Precompute weighted selectors** - Avoid repeated creation
|
||||
|
||||
3. **Choose appropriate noise**
|
||||
- Perlin: Smooth terrain, clouds
|
||||
- Simplex: Performance-critical
|
||||
- Worley: Cells, stone textures
|
||||
- FBM: Multi-layer natural effects
|
||||
|
||||
4. **Tune FBM parameters**
|
||||
- `octaves`: More = richer detail, higher cost
|
||||
- `persistence`: 0.5 is common, higher = more high-frequency detail
|
||||
- `lacunarity`: Usually 2, controls frequency growth
|
||||
@@ -396,3 +396,10 @@ class LootSystem {
|
||||
- `octaves`: More = richer detail, higher cost
|
||||
- `persistence`: 0.5 is common, higher = more high-frequency detail
|
||||
- `lacunarity`: Usually 2, controls frequency growth
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Noise Functions](./noise) - Perlin, Simplex, Worley, FBM
|
||||
- [Seeded Random](./random) - SeededRandom API and distribution methods
|
||||
- [Sampling Utilities](./sampling) - Weighted random, shuffle, sampling
|
||||
- [Examples](./examples) - Terrain, loot, level generation
|
||||
|
||||
97
docs/src/content/docs/en/modules/procgen/noise.md
Normal file
97
docs/src/content/docs/en/modules/procgen/noise.md
Normal file
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: "Noise Functions"
|
||||
description: "Perlin, Simplex, Worley, and FBM"
|
||||
---
|
||||
|
||||
## Perlin Noise
|
||||
|
||||
Classic gradient noise, output range [-1, 1]:
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise } from '@esengine/procgen';
|
||||
|
||||
const perlin = createPerlinNoise(seed);
|
||||
|
||||
// 2D noise
|
||||
const value2D = perlin.noise2D(x, y);
|
||||
|
||||
// 3D noise
|
||||
const value3D = perlin.noise3D(x, y, z);
|
||||
```
|
||||
|
||||
## Simplex Noise
|
||||
|
||||
Faster than Perlin with less directional bias:
|
||||
|
||||
```typescript
|
||||
import { createSimplexNoise } from '@esengine/procgen';
|
||||
|
||||
const simplex = createSimplexNoise(seed);
|
||||
|
||||
const value = simplex.noise2D(x, y);
|
||||
```
|
||||
|
||||
## Worley Noise
|
||||
|
||||
Cell-based noise, suitable for stone, cell textures:
|
||||
|
||||
```typescript
|
||||
import { createWorleyNoise } from '@esengine/procgen';
|
||||
|
||||
const worley = createWorleyNoise(seed);
|
||||
|
||||
// Returns distance to nearest point
|
||||
const distance = worley.noise2D(x, y);
|
||||
```
|
||||
|
||||
## FBM (Fractal Brownian Motion)
|
||||
|
||||
Layer multiple noise octaves for richer detail:
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||||
|
||||
const baseNoise = createPerlinNoise(seed);
|
||||
|
||||
const fbm = createFBM(baseNoise, {
|
||||
octaves: 6, // Layer count (more = richer detail)
|
||||
lacunarity: 2.0, // Frequency multiplier
|
||||
persistence: 0.5, // Amplitude decay factor
|
||||
frequency: 1.0, // Initial frequency
|
||||
amplitude: 1.0 // Initial amplitude
|
||||
});
|
||||
|
||||
// Standard FBM
|
||||
const value = fbm.noise2D(x, y);
|
||||
|
||||
// Ridged FBM (for mountains)
|
||||
const ridged = fbm.ridged2D(x, y);
|
||||
|
||||
// Turbulence
|
||||
const turb = fbm.turbulence2D(x, y);
|
||||
|
||||
// Billowed (for clouds)
|
||||
const cloud = fbm.billowed2D(x, y);
|
||||
```
|
||||
|
||||
## FBM Parameter Guide
|
||||
|
||||
| Parameter | Description | Recommended |
|
||||
|-----------|-------------|-------------|
|
||||
| `octaves` | Layer count, more = richer detail | 4-8 |
|
||||
| `lacunarity` | Frequency multiplier | 2.0 |
|
||||
| `persistence` | Amplitude decay factor | 0.5 |
|
||||
| `frequency` | Initial frequency | 0.01-0.1 |
|
||||
| `amplitude` | Initial amplitude | 1.0 |
|
||||
|
||||
## Choosing the Right Noise
|
||||
|
||||
| Noise Type | Use Case |
|
||||
|------------|----------|
|
||||
| Perlin | Smooth terrain transitions, clouds |
|
||||
| Simplex | Performance-critical scenarios |
|
||||
| Worley | Cells, stone, crack textures |
|
||||
| FBM | Multi-layer natural detail effects |
|
||||
| Ridged FBM | Mountains, ridged terrain |
|
||||
| Turbulence | Flame, smoke effects |
|
||||
| Billowed FBM | Clouds, soft puffy effects |
|
||||
92
docs/src/content/docs/en/modules/procgen/random.md
Normal file
92
docs/src/content/docs/en/modules/procgen/random.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: "Seeded Random"
|
||||
description: "SeededRandom API and distribution methods"
|
||||
---
|
||||
|
||||
## SeededRandom
|
||||
|
||||
Deterministic PRNG based on xorshift128+ algorithm:
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom } from '@esengine/procgen';
|
||||
|
||||
const rng = createSeededRandom(42);
|
||||
```
|
||||
|
||||
## Basic Methods
|
||||
|
||||
```typescript
|
||||
// [0, 1) float
|
||||
rng.next();
|
||||
|
||||
// [min, max] integer
|
||||
rng.nextInt(1, 10);
|
||||
|
||||
// [min, max) float
|
||||
rng.nextFloat(0, 100);
|
||||
|
||||
// Boolean (with optional probability)
|
||||
rng.nextBool(); // 50%
|
||||
rng.nextBool(0.3); // 30%
|
||||
|
||||
// Reset to initial state
|
||||
rng.reset();
|
||||
```
|
||||
|
||||
## Distribution Methods
|
||||
|
||||
```typescript
|
||||
// Normal distribution (Gaussian)
|
||||
rng.nextGaussian(); // mean 0, stdDev 1
|
||||
rng.nextGaussian(100, 15); // mean 100, stdDev 15
|
||||
|
||||
// Exponential distribution
|
||||
rng.nextExponential(); // λ = 1
|
||||
rng.nextExponential(0.5); // λ = 0.5
|
||||
```
|
||||
|
||||
## Geometry Methods
|
||||
|
||||
```typescript
|
||||
// Uniform point in circle
|
||||
const point = rng.nextPointInCircle(50); // { x, y }
|
||||
|
||||
// Point on circle edge
|
||||
const edge = rng.nextPointOnCircle(50); // { x, y }
|
||||
|
||||
// Uniform point in sphere
|
||||
const point3D = rng.nextPointInSphere(50); // { x, y, z }
|
||||
|
||||
// Random direction vector
|
||||
const dir = rng.nextDirection2D(); // { x, y }, length 1
|
||||
```
|
||||
|
||||
## Determinism Guarantee
|
||||
|
||||
Same seed always produces same sequence:
|
||||
|
||||
```typescript
|
||||
const rng1 = createSeededRandom(12345);
|
||||
const rng2 = createSeededRandom(12345);
|
||||
|
||||
// These sequences are identical
|
||||
console.log(rng1.next()); // 0.xxx
|
||||
console.log(rng2.next()); // 0.xxx (same)
|
||||
|
||||
console.log(rng1.nextInt(1, 100)); // N
|
||||
console.log(rng2.nextInt(1, 100)); // N (same)
|
||||
```
|
||||
|
||||
## Saving and Restoring State
|
||||
|
||||
```typescript
|
||||
// Save seed for reproducibility
|
||||
const seed = Date.now();
|
||||
const rng = createSeededRandom(seed);
|
||||
saveSeed(seed);
|
||||
|
||||
// Later, restore with same seed
|
||||
const savedSeed = loadSeed();
|
||||
const rng2 = createSeededRandom(savedSeed);
|
||||
// Will produce identical sequence
|
||||
```
|
||||
135
docs/src/content/docs/en/modules/procgen/sampling.md
Normal file
135
docs/src/content/docs/en/modules/procgen/sampling.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: "Sampling Utilities"
|
||||
description: "Weighted random, shuffle, and sampling functions"
|
||||
---
|
||||
|
||||
## Weighted Random API
|
||||
|
||||
### WeightedRandom
|
||||
|
||||
Precomputed cumulative weights for efficient selection:
|
||||
|
||||
```typescript
|
||||
import { createWeightedRandom } from '@esengine/procgen';
|
||||
|
||||
const selector = createWeightedRandom([
|
||||
{ value: 'apple', weight: 5 },
|
||||
{ value: 'banana', weight: 3 },
|
||||
{ value: 'cherry', weight: 2 }
|
||||
]);
|
||||
|
||||
// Use with seeded random
|
||||
const result = selector.pick(rng);
|
||||
|
||||
// Use with Math.random
|
||||
const result2 = selector.pickRandom();
|
||||
|
||||
// Get probability
|
||||
console.log(selector.getProbability(0)); // 0.5 (5/10)
|
||||
console.log(selector.size); // 3
|
||||
console.log(selector.totalWeight); // 10
|
||||
```
|
||||
|
||||
### Convenience Functions
|
||||
|
||||
```typescript
|
||||
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
|
||||
|
||||
// Pick from array
|
||||
const item = weightedPick([
|
||||
{ value: 'a', weight: 1 },
|
||||
{ value: 'b', weight: 2 }
|
||||
], rng);
|
||||
|
||||
// Pick from object
|
||||
const item2 = weightedPickFromMap({
|
||||
'common': 60,
|
||||
'rare': 30,
|
||||
'epic': 10
|
||||
}, rng);
|
||||
```
|
||||
|
||||
## Shuffle API
|
||||
|
||||
### shuffle / shuffleCopy
|
||||
|
||||
Fisher-Yates shuffle algorithm:
|
||||
|
||||
```typescript
|
||||
import { shuffle, shuffleCopy } from '@esengine/procgen';
|
||||
|
||||
const arr = [1, 2, 3, 4, 5];
|
||||
|
||||
// In-place shuffle
|
||||
shuffle(arr, rng);
|
||||
|
||||
// Create shuffled copy (original unchanged)
|
||||
const shuffled = shuffleCopy(arr, rng);
|
||||
```
|
||||
|
||||
### pickOne
|
||||
|
||||
Randomly select one element:
|
||||
|
||||
```typescript
|
||||
import { pickOne } from '@esengine/procgen';
|
||||
|
||||
const items = ['a', 'b', 'c', 'd'];
|
||||
const item = pickOne(items, rng);
|
||||
```
|
||||
|
||||
## Sampling API
|
||||
|
||||
### sample / sampleWithReplacement
|
||||
|
||||
```typescript
|
||||
import { sample, sampleWithReplacement } from '@esengine/procgen';
|
||||
|
||||
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
// Sample 3 unique elements
|
||||
const unique = sample(arr, 3, rng);
|
||||
|
||||
// Sample 5 (with possible repeats)
|
||||
const withRep = sampleWithReplacement(arr, 5, rng);
|
||||
```
|
||||
|
||||
### randomIntegers
|
||||
|
||||
Generate random integer array within range:
|
||||
|
||||
```typescript
|
||||
import { randomIntegers } from '@esengine/procgen';
|
||||
|
||||
// 5 unique random integers from 1-100
|
||||
const nums = randomIntegers(1, 100, 5, rng);
|
||||
```
|
||||
|
||||
### weightedSample
|
||||
|
||||
Sample by weight (no replacement):
|
||||
|
||||
```typescript
|
||||
import { weightedSample } from '@esengine/procgen';
|
||||
|
||||
const items = ['A', 'B', 'C', 'D', 'E'];
|
||||
const weights = [10, 8, 6, 4, 2];
|
||||
|
||||
// Select 3 by weight
|
||||
const selected = weightedSample(items, weights, 3, rng);
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
```typescript
|
||||
// Good: Create once, use many times
|
||||
const selector = createWeightedRandom(items);
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
selector.pick(rng);
|
||||
}
|
||||
|
||||
// Bad: Create every time
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
weightedPick(items, rng);
|
||||
}
|
||||
```
|
||||
166
docs/src/content/docs/en/modules/spatial/aoi.md
Normal file
166
docs/src/content/docs/en/modules/spatial/aoi.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: "AOI (Area of Interest)"
|
||||
description: "View management and enter/exit events"
|
||||
---
|
||||
|
||||
AOI (Area of Interest) tracks visibility relationships between entities, commonly used for MMO synchronization and NPC AI perception.
|
||||
|
||||
## createGridAOI
|
||||
|
||||
```typescript
|
||||
function createGridAOI<T>(cellSize?: number): GridAOI<T>
|
||||
```
|
||||
|
||||
Creates a grid-based AOI manager.
|
||||
|
||||
**Parameters:**
|
||||
- `cellSize` - Grid cell size (recommended: 1-2x average view range)
|
||||
|
||||
## Observer Management
|
||||
|
||||
### addObserver
|
||||
|
||||
Add an observer:
|
||||
|
||||
```typescript
|
||||
aoi.addObserver(player, position, {
|
||||
viewRange: 200, // View range
|
||||
observable: true // Can be seen by other observers (default true)
|
||||
});
|
||||
|
||||
// NPC that only observes but cannot be observed
|
||||
aoi.addObserver(camera, position, {
|
||||
viewRange: 500,
|
||||
observable: false
|
||||
});
|
||||
```
|
||||
|
||||
### removeObserver
|
||||
|
||||
Remove an observer:
|
||||
|
||||
```typescript
|
||||
aoi.removeObserver(player);
|
||||
```
|
||||
|
||||
### updatePosition
|
||||
|
||||
Update position (automatically triggers enter/exit events):
|
||||
|
||||
```typescript
|
||||
aoi.updatePosition(player, newPosition);
|
||||
```
|
||||
|
||||
### updateViewRange
|
||||
|
||||
Update view range:
|
||||
|
||||
```typescript
|
||||
// View range expanded after buff
|
||||
aoi.updateViewRange(player, 300);
|
||||
```
|
||||
|
||||
## Query Methods
|
||||
|
||||
### getEntitiesInView
|
||||
|
||||
Get all entities within an observer's view:
|
||||
|
||||
```typescript
|
||||
const visible = aoi.getEntitiesInView(player);
|
||||
for (const entity of visible) {
|
||||
updateEntityForPlayer(player, entity);
|
||||
}
|
||||
```
|
||||
|
||||
### getObserversOf
|
||||
|
||||
Get all observers who can see a specific entity:
|
||||
|
||||
```typescript
|
||||
const observers = aoi.getObserversOf(monster);
|
||||
for (const observer of observers) {
|
||||
notifyMonsterMoved(observer, monster);
|
||||
}
|
||||
```
|
||||
|
||||
### canSee
|
||||
|
||||
Check visibility:
|
||||
|
||||
```typescript
|
||||
if (aoi.canSee(player, enemy)) {
|
||||
enemy.showHealthBar();
|
||||
}
|
||||
```
|
||||
|
||||
## Event System
|
||||
|
||||
### Global Event Listener
|
||||
|
||||
```typescript
|
||||
aoi.addListener((event) => {
|
||||
switch (event.type) {
|
||||
case 'enter':
|
||||
console.log(`${event.observer} sees ${event.target}`);
|
||||
break;
|
||||
case 'exit':
|
||||
console.log(`${event.target} left ${event.observer}'s view`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Entity-Specific Event Listener
|
||||
|
||||
```typescript
|
||||
// Only listen to a specific player's view events
|
||||
aoi.addEntityListener(player, (event) => {
|
||||
if (event.type === 'enter') {
|
||||
sendToClient(player, 'entity_enter', event.target);
|
||||
} else if (event.type === 'exit') {
|
||||
sendToClient(player, 'entity_exit', event.target);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Event Types
|
||||
|
||||
```typescript
|
||||
interface IAOIEvent<T> {
|
||||
type: 'enter' | 'exit' | 'update';
|
||||
observer: T; // Observer (who saw the change)
|
||||
target: T; // Target (object that changed)
|
||||
position: IVector2; // Target position
|
||||
}
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
- `GetEntitiesInView` - Get entities in view
|
||||
- `GetObserversOf` - Get observers
|
||||
- `CanSee` - Check visibility
|
||||
- `OnEntityEnterView` - Enter view event
|
||||
- `OnEntityExitView` - Exit view event
|
||||
|
||||
## Service Tokens
|
||||
|
||||
For dependency injection scenarios:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
SpatialIndexToken,
|
||||
SpatialQueryToken,
|
||||
AOIManagerToken,
|
||||
createGridSpatialIndex,
|
||||
createGridAOI
|
||||
} from '@esengine/spatial';
|
||||
|
||||
// Register services
|
||||
services.register(SpatialIndexToken, createGridSpatialIndex(100));
|
||||
services.register(AOIManagerToken, createGridAOI(100));
|
||||
|
||||
// Get services
|
||||
const spatialIndex = services.get(SpatialIndexToken);
|
||||
const aoiManager = services.get(AOIManagerToken);
|
||||
```
|
||||
184
docs/src/content/docs/en/modules/spatial/examples.md
Normal file
184
docs/src/content/docs/en/modules/spatial/examples.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "Area attacks, MMO sync, AI perception and more"
|
||||
---
|
||||
|
||||
## Area Attack Detection
|
||||
|
||||
```typescript
|
||||
class CombatSystem {
|
||||
private spatialIndex: ISpatialIndex<Entity>;
|
||||
|
||||
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
|
||||
const targets = this.spatialIndex.findInRadius(
|
||||
center,
|
||||
radius,
|
||||
(entity) => entity.hasComponent(HealthComponent)
|
||||
);
|
||||
|
||||
for (const target of targets) {
|
||||
const health = target.getComponent(HealthComponent);
|
||||
health.takeDamage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
findNearestEnemy(position: IVector2, team: string): Entity | null {
|
||||
return this.spatialIndex.findNearest(
|
||||
position,
|
||||
undefined, // No distance limit
|
||||
(entity) => {
|
||||
const teamComp = entity.getComponent(TeamComponent);
|
||||
return teamComp && teamComp.team !== team;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## MMO Sync System
|
||||
|
||||
```typescript
|
||||
class SyncSystem {
|
||||
private aoi: IAOIManager<Player>;
|
||||
|
||||
constructor() {
|
||||
this.aoi = createGridAOI<Player>(100);
|
||||
|
||||
// Listen for enter/exit events
|
||||
this.aoi.addListener((event) => {
|
||||
const packet = this.createSyncPacket(event);
|
||||
this.sendToPlayer(event.observer, packet);
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerJoin(player: Player): void {
|
||||
this.aoi.addObserver(player, player.position, {
|
||||
viewRange: player.viewRange
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerMove(player: Player, newPosition: IVector2): void {
|
||||
this.aoi.updatePosition(player, newPosition);
|
||||
}
|
||||
|
||||
onPlayerLeave(player: Player): void {
|
||||
this.aoi.removeObserver(player);
|
||||
}
|
||||
|
||||
// Broadcast to all players who can see a specific player
|
||||
broadcastToObservers(player: Player, packet: Packet): void {
|
||||
const observers = this.aoi.getObserversOf(player);
|
||||
for (const observer of observers) {
|
||||
this.sendToPlayer(observer, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## NPC AI Perception
|
||||
|
||||
```typescript
|
||||
class AIPerceptionSystem {
|
||||
private aoi: IAOIManager<Entity>;
|
||||
|
||||
constructor() {
|
||||
this.aoi = createGridAOI<Entity>(50);
|
||||
}
|
||||
|
||||
setupNPC(npc: Entity): void {
|
||||
const perception = npc.getComponent(PerceptionComponent);
|
||||
|
||||
this.aoi.addObserver(npc, npc.position, {
|
||||
viewRange: perception.range
|
||||
});
|
||||
|
||||
// Listen to this NPC's perception events
|
||||
this.aoi.addEntityListener(npc, (event) => {
|
||||
const ai = npc.getComponent(AIComponent);
|
||||
|
||||
if (event.type === 'enter') {
|
||||
ai.onTargetDetected(event.target);
|
||||
} else if (event.type === 'exit') {
|
||||
ai.onTargetLost(event.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(): void {
|
||||
// Update all NPC positions
|
||||
for (const npc of this.npcs) {
|
||||
this.aoi.updatePosition(npc, npc.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Skill Target Selection
|
||||
|
||||
```typescript
|
||||
class TargetingSystem {
|
||||
private spatialIndex: ISpatialIndex<Entity>;
|
||||
|
||||
// Cone-shaped skill
|
||||
findTargetsInCone(
|
||||
origin: IVector2,
|
||||
direction: IVector2,
|
||||
range: number,
|
||||
angle: number
|
||||
): Entity[] {
|
||||
// First use circular range for rough filtering
|
||||
const candidates = this.spatialIndex.findInRadius(origin, range);
|
||||
|
||||
// Then precisely filter targets within the cone
|
||||
return candidates.filter(entity => {
|
||||
const toEntity = normalize(subtract(entity.position, origin));
|
||||
const dot = dotProduct(direction, toEntity);
|
||||
const entityAngle = Math.acos(dot);
|
||||
return entityAngle <= angle / 2;
|
||||
});
|
||||
}
|
||||
|
||||
// Piercing ray skill
|
||||
findTargetsOnLine(
|
||||
origin: IVector2,
|
||||
direction: IVector2,
|
||||
maxDistance: number,
|
||||
maxTargets: number
|
||||
): Entity[] {
|
||||
const hits = this.spatialIndex.raycast(origin, direction, maxDistance);
|
||||
return hits.slice(0, maxTargets).map(hit => hit.target);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Obstacle Avoidance
|
||||
|
||||
```typescript
|
||||
class ObstacleAvoidanceSystem {
|
||||
private spatialIndex: ISpatialIndex<Entity>;
|
||||
|
||||
calculateAvoidanceForce(entity: Entity, velocity: IVector2): IVector2 {
|
||||
const position = entity.position;
|
||||
const lookAhead = 50; // Forward detection distance
|
||||
|
||||
// Detect obstacles ahead
|
||||
const hit = this.spatialIndex.raycastFirst(
|
||||
position,
|
||||
normalize(velocity),
|
||||
lookAhead,
|
||||
(e) => e.hasComponent(ObstacleComponent)
|
||||
);
|
||||
|
||||
if (!hit) return { x: 0, y: 0 };
|
||||
|
||||
// Calculate avoidance force
|
||||
const avoidDirection = normalize({
|
||||
x: hit.normal.y,
|
||||
y: -hit.normal.x
|
||||
});
|
||||
|
||||
const urgency = 1 - (hit.distance / lookAhead);
|
||||
return scale(avoidDirection, urgency * 100);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "Spatial Index System"
|
||||
description: "Efficient spatial queries and AOI management"
|
||||
---
|
||||
|
||||
`@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management.
|
||||
@@ -76,7 +77,9 @@ const visible = aoi.getEntitiesInView(player);
|
||||
| Direction | One-way query | Two-way tracking |
|
||||
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
|
||||
|
||||
### IBounds
|
||||
### Core Interfaces
|
||||
|
||||
#### IBounds
|
||||
|
||||
```typescript
|
||||
interface IBounds {
|
||||
@@ -87,7 +90,7 @@ interface IBounds {
|
||||
}
|
||||
```
|
||||
|
||||
### IRaycastHit
|
||||
#### IRaycastHit
|
||||
|
||||
```typescript
|
||||
interface IRaycastHit<T> {
|
||||
@@ -98,227 +101,9 @@ interface IRaycastHit<T> {
|
||||
}
|
||||
```
|
||||
|
||||
## Spatial Index API
|
||||
## Documentation
|
||||
|
||||
### createGridSpatialIndex
|
||||
|
||||
```typescript
|
||||
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
|
||||
```
|
||||
|
||||
**Choosing cellSize:**
|
||||
- Too small: High memory, reduced query efficiency
|
||||
- Too large: Many objects per cell, slow iteration
|
||||
- Recommended: 1-2x average object spacing
|
||||
|
||||
### Management Methods
|
||||
|
||||
```typescript
|
||||
spatialIndex.insert(entity, position);
|
||||
spatialIndex.remove(entity);
|
||||
spatialIndex.update(entity, newPosition);
|
||||
spatialIndex.clear();
|
||||
```
|
||||
|
||||
### Query Methods
|
||||
|
||||
#### findInRadius
|
||||
|
||||
```typescript
|
||||
const enemies = spatialIndex.findInRadius(
|
||||
{ x: 100, y: 200 },
|
||||
50,
|
||||
(entity) => entity.type === 'enemy' // Optional filter
|
||||
);
|
||||
```
|
||||
|
||||
#### findInRect
|
||||
|
||||
```typescript
|
||||
import { createBounds } from '@esengine/spatial';
|
||||
|
||||
const bounds = createBounds(0, 0, 200, 200);
|
||||
const entities = spatialIndex.findInRect(bounds);
|
||||
```
|
||||
|
||||
#### findNearest
|
||||
|
||||
```typescript
|
||||
const nearest = spatialIndex.findNearest(
|
||||
playerPosition,
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
```
|
||||
|
||||
#### findKNearest
|
||||
|
||||
```typescript
|
||||
const nearestEnemies = spatialIndex.findKNearest(
|
||||
playerPosition,
|
||||
5, // k
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
```
|
||||
|
||||
#### raycast / raycastFirst
|
||||
|
||||
```typescript
|
||||
const hits = spatialIndex.raycast(origin, direction, maxDistance);
|
||||
const firstHit = spatialIndex.raycastFirst(origin, direction, maxDistance);
|
||||
```
|
||||
|
||||
## AOI API
|
||||
|
||||
### createGridAOI
|
||||
|
||||
```typescript
|
||||
function createGridAOI<T>(cellSize?: number): GridAOI<T>
|
||||
```
|
||||
|
||||
### Observer Management
|
||||
|
||||
```typescript
|
||||
// Add observer
|
||||
aoi.addObserver(player, position, {
|
||||
viewRange: 200,
|
||||
observable: true // Can be seen by others
|
||||
});
|
||||
|
||||
// Remove observer
|
||||
aoi.removeObserver(player);
|
||||
|
||||
// Update position
|
||||
aoi.updatePosition(player, newPosition);
|
||||
|
||||
// Update view range
|
||||
aoi.updateViewRange(player, 300);
|
||||
```
|
||||
|
||||
### Query Methods
|
||||
|
||||
```typescript
|
||||
// Get entities in observer's view
|
||||
const visible = aoi.getEntitiesInView(player);
|
||||
|
||||
// Get observers who can see entity
|
||||
const observers = aoi.getObserversOf(monster);
|
||||
|
||||
// Check visibility
|
||||
if (aoi.canSee(player, enemy)) { ... }
|
||||
```
|
||||
|
||||
### Event System
|
||||
|
||||
```typescript
|
||||
// Global event listener
|
||||
aoi.addListener((event) => {
|
||||
switch (event.type) {
|
||||
case 'enter': /* entered view */ break;
|
||||
case 'exit': /* left view */ break;
|
||||
}
|
||||
});
|
||||
|
||||
// Entity-specific listener
|
||||
aoi.addEntityListener(player, (event) => {
|
||||
if (event.type === 'enter') {
|
||||
sendToClient(player, 'entity_enter', event.target);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### Bounds Creation
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createBounds,
|
||||
createBoundsFromCenter,
|
||||
createBoundsFromCircle
|
||||
} from '@esengine/spatial';
|
||||
|
||||
const bounds1 = createBounds(0, 0, 100, 100);
|
||||
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
|
||||
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
|
||||
```
|
||||
|
||||
### Geometry Checks
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isPointInBounds,
|
||||
boundsIntersect,
|
||||
boundsIntersectsCircle,
|
||||
distance,
|
||||
distanceSquared
|
||||
} from '@esengine/spatial';
|
||||
|
||||
if (isPointInBounds(point, bounds)) { ... }
|
||||
if (boundsIntersect(boundsA, boundsB)) { ... }
|
||||
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
|
||||
const dist = distance(pointA, pointB);
|
||||
const distSq = distanceSquared(pointA, pointB); // Faster
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Range Attack Detection
|
||||
|
||||
```typescript
|
||||
class CombatSystem {
|
||||
private spatialIndex: ISpatialIndex<Entity>;
|
||||
|
||||
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
|
||||
const targets = this.spatialIndex.findInRadius(
|
||||
center, radius,
|
||||
(entity) => entity.hasComponent(HealthComponent)
|
||||
);
|
||||
|
||||
for (const target of targets) {
|
||||
target.getComponent(HealthComponent).takeDamage(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MMO Sync System
|
||||
|
||||
```typescript
|
||||
class SyncSystem {
|
||||
private aoi: IAOIManager<Player>;
|
||||
|
||||
constructor() {
|
||||
this.aoi = createGridAOI<Player>(100);
|
||||
|
||||
this.aoi.addListener((event) => {
|
||||
const packet = this.createSyncPacket(event);
|
||||
this.sendToPlayer(event.observer, packet);
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerMove(player: Player, newPosition: IVector2): void {
|
||||
this.aoi.updatePosition(player, newPosition);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
### Spatial Query Nodes
|
||||
- `FindInRadius`, `FindInRect`, `FindNearest`, `FindKNearest`
|
||||
- `Raycast`, `RaycastFirst`
|
||||
|
||||
### AOI Nodes
|
||||
- `GetEntitiesInView`, `GetObserversOf`, `CanSee`
|
||||
- `OnEntityEnterView`, `OnEntityExitView`
|
||||
|
||||
## Service Tokens
|
||||
|
||||
```typescript
|
||||
import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';
|
||||
|
||||
services.register(SpatialIndexToken, createGridSpatialIndex(100));
|
||||
services.register(AOIManagerToken, createGridAOI(100));
|
||||
```
|
||||
- [Spatial Index API](./spatial-index) - Grid index, range queries, raycasting
|
||||
- [AOI (Area of Interest)](./aoi) - View management, enter/exit events
|
||||
- [Examples](./examples) - Area attacks, MMO sync, AI perception
|
||||
- [Utilities & Optimization](./utilities) - Geometry detection, performance tips
|
||||
|
||||
159
docs/src/content/docs/en/modules/spatial/spatial-index.md
Normal file
159
docs/src/content/docs/en/modules/spatial/spatial-index.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: "Spatial Index API"
|
||||
description: "Grid index, range queries, raycasting"
|
||||
---
|
||||
|
||||
## createGridSpatialIndex
|
||||
|
||||
```typescript
|
||||
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
|
||||
```
|
||||
|
||||
Creates a uniform grid-based spatial index.
|
||||
|
||||
**Parameters:**
|
||||
- `cellSize` - Grid cell size (default 100)
|
||||
|
||||
**Choosing cellSize:**
|
||||
- Too small: High memory usage, reduced query efficiency
|
||||
- Too large: Too many objects per cell, slow iteration
|
||||
- Recommended: 1-2x average object spacing
|
||||
|
||||
## Management Methods
|
||||
|
||||
### insert
|
||||
|
||||
Insert an object into the index:
|
||||
|
||||
```typescript
|
||||
spatialIndex.insert(enemy, { x: 100, y: 200 });
|
||||
```
|
||||
|
||||
### remove
|
||||
|
||||
Remove an object:
|
||||
|
||||
```typescript
|
||||
spatialIndex.remove(enemy);
|
||||
```
|
||||
|
||||
### update
|
||||
|
||||
Update object position:
|
||||
|
||||
```typescript
|
||||
spatialIndex.update(enemy, { x: 150, y: 250 });
|
||||
```
|
||||
|
||||
### clear
|
||||
|
||||
Clear the index:
|
||||
|
||||
```typescript
|
||||
spatialIndex.clear();
|
||||
```
|
||||
|
||||
## Query Methods
|
||||
|
||||
### findInRadius
|
||||
|
||||
Find all objects within a circular area:
|
||||
|
||||
```typescript
|
||||
// Find all enemies within radius 50 of point (100, 200)
|
||||
const enemies = spatialIndex.findInRadius(
|
||||
{ x: 100, y: 200 },
|
||||
50,
|
||||
(entity) => entity.type === 'enemy' // Optional filter
|
||||
);
|
||||
```
|
||||
|
||||
### findInRect
|
||||
|
||||
Find all objects within a rectangular area:
|
||||
|
||||
```typescript
|
||||
import { createBounds } from '@esengine/spatial';
|
||||
|
||||
const bounds = createBounds(0, 0, 200, 200);
|
||||
const entities = spatialIndex.findInRect(bounds);
|
||||
```
|
||||
|
||||
### findNearest
|
||||
|
||||
Find the nearest object:
|
||||
|
||||
```typescript
|
||||
// Find nearest enemy (max search distance 500)
|
||||
const nearest = spatialIndex.findNearest(
|
||||
playerPosition,
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
|
||||
if (nearest) {
|
||||
attackTarget(nearest);
|
||||
}
|
||||
```
|
||||
|
||||
### findKNearest
|
||||
|
||||
Find the K nearest objects:
|
||||
|
||||
```typescript
|
||||
// Find 5 nearest enemies
|
||||
const nearestEnemies = spatialIndex.findKNearest(
|
||||
playerPosition,
|
||||
5, // k
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
```
|
||||
|
||||
### raycast
|
||||
|
||||
Raycast (returns all hits):
|
||||
|
||||
```typescript
|
||||
const hits = spatialIndex.raycast(
|
||||
origin, // Ray origin
|
||||
direction, // Ray direction (should be normalized)
|
||||
maxDistance, // Maximum detection distance
|
||||
filter // Optional filter
|
||||
);
|
||||
|
||||
// hits are sorted by distance
|
||||
for (const hit of hits) {
|
||||
console.log(`Hit ${hit.target} at ${hit.point}, distance ${hit.distance}`);
|
||||
}
|
||||
```
|
||||
|
||||
### raycastFirst
|
||||
|
||||
Raycast (returns only the first hit):
|
||||
|
||||
```typescript
|
||||
const hit = spatialIndex.raycastFirst(origin, direction, 1000);
|
||||
if (hit) {
|
||||
dealDamage(hit.target, calculateDamage(hit.distance));
|
||||
}
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
```typescript
|
||||
// Get number of objects in index
|
||||
console.log(spatialIndex.count);
|
||||
|
||||
// Get all objects
|
||||
const all = spatialIndex.getAll();
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
- `FindInRadius` - Find objects within radius
|
||||
- `FindInRect` - Find objects within rectangle
|
||||
- `FindNearest` - Find nearest object
|
||||
- `FindKNearest` - Find K nearest objects
|
||||
- `Raycast` - Raycast (all hits)
|
||||
- `RaycastFirst` - Raycast (first hit only)
|
||||
149
docs/src/content/docs/en/modules/spatial/utilities.md
Normal file
149
docs/src/content/docs/en/modules/spatial/utilities.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: "Utilities & Optimization"
|
||||
description: "Geometry detection functions and performance tips"
|
||||
---
|
||||
|
||||
## Bounds Creation
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createBounds,
|
||||
createBoundsFromCenter,
|
||||
createBoundsFromCircle
|
||||
} from '@esengine/spatial';
|
||||
|
||||
// Create from corners
|
||||
const bounds1 = createBounds(0, 0, 100, 100);
|
||||
|
||||
// Create from center point and size
|
||||
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
|
||||
|
||||
// Create from circle (bounding box)
|
||||
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
|
||||
```
|
||||
|
||||
## Geometry Detection
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isPointInBounds,
|
||||
boundsIntersect,
|
||||
boundsIntersectsCircle,
|
||||
distance,
|
||||
distanceSquared
|
||||
} from '@esengine/spatial';
|
||||
|
||||
// Point inside bounds?
|
||||
if (isPointInBounds(point, bounds)) { ... }
|
||||
|
||||
// Two bounds intersect?
|
||||
if (boundsIntersect(boundsA, boundsB)) { ... }
|
||||
|
||||
// Bounds intersects circle?
|
||||
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
|
||||
|
||||
// Distance calculation
|
||||
const dist = distance(pointA, pointB);
|
||||
const distSq = distanceSquared(pointA, pointB); // Faster, avoids sqrt
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Choose the Right cellSize
|
||||
|
||||
- **Too small**: High memory usage, many cells
|
||||
- **Too large**: Many objects per cell, slow iteration
|
||||
- **Rule of thumb**: 1-2x average object spacing
|
||||
|
||||
```typescript
|
||||
// Scene with objects spaced about 50 units apart
|
||||
const spatialIndex = createGridSpatialIndex(75); // 1.5x
|
||||
```
|
||||
|
||||
### 2. Use Filters to Reduce Results
|
||||
|
||||
```typescript
|
||||
// Filter during spatial query, not afterward
|
||||
spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy');
|
||||
|
||||
// Avoid this pattern
|
||||
const all = spatialIndex.findInRadius(center, radius);
|
||||
const enemies = all.filter(e => e.type === 'enemy'); // Extra iteration
|
||||
```
|
||||
|
||||
### 3. Use distanceSquared Instead of distance
|
||||
|
||||
```typescript
|
||||
// Avoid sqrt calculation
|
||||
const thresholdSq = threshold * threshold;
|
||||
|
||||
if (distanceSquared(a, b) < thresholdSq) {
|
||||
// Within range
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Batch Update Optimization
|
||||
|
||||
```typescript
|
||||
// When updating many objects at once
|
||||
// Consider disabling/enabling events around batch updates
|
||||
aoi.disableEvents();
|
||||
for (const entity of entities) {
|
||||
aoi.updatePosition(entity, entity.position);
|
||||
}
|
||||
aoi.enableEvents();
|
||||
aoi.flushEvents(); // Send all events at once
|
||||
```
|
||||
|
||||
### 5. Layered Indexing
|
||||
|
||||
For very large scenes, use multiple spatial indexes:
|
||||
|
||||
```typescript
|
||||
// Static objects use large grid (queried less frequently)
|
||||
const staticIndex = createGridSpatialIndex(500);
|
||||
|
||||
// Dynamic objects use small grid (updated frequently)
|
||||
const dynamicIndex = createGridSpatialIndex(50);
|
||||
|
||||
// Merge results when querying
|
||||
function findInRadius(center: IVector2, radius: number): Entity[] {
|
||||
return [
|
||||
...staticIndex.findInRadius(center, radius),
|
||||
...dynamicIndex.findInRadius(center, radius)
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Reduce Query Frequency
|
||||
|
||||
```typescript
|
||||
class AISystem {
|
||||
private lastQueryTime = new Map<Entity, number>();
|
||||
private queryInterval = 100; // Query every 100ms
|
||||
|
||||
update(dt: number): void {
|
||||
const now = performance.now();
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const lastTime = this.lastQueryTime.get(entity) ?? 0;
|
||||
|
||||
if (now - lastTime >= this.queryInterval) {
|
||||
this.updateAIPerception(entity);
|
||||
this.lastQueryTime.set(entity, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Management
|
||||
|
||||
```typescript
|
||||
// Remove destroyed entities promptly
|
||||
spatialIndex.remove(destroyedEntity);
|
||||
|
||||
// Clear completely when switching scenes
|
||||
spatialIndex.clear();
|
||||
aoi.clear();
|
||||
```
|
||||
218
docs/src/content/docs/en/modules/timer/api.md
Normal file
218
docs/src/content/docs/en/modules/timer/api.md
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
title: "API Reference"
|
||||
description: "Complete timer and cooldown system API"
|
||||
---
|
||||
|
||||
## createTimerService
|
||||
|
||||
```typescript
|
||||
function createTimerService(config?: TimerServiceConfig): ITimerService
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `maxTimers` | `number` | `0` | Maximum timer count (0 = unlimited) |
|
||||
| `maxCooldowns` | `number` | `0` | Maximum cooldown count (0 = unlimited) |
|
||||
|
||||
## Timer API
|
||||
|
||||
### schedule
|
||||
|
||||
Schedule a one-time timer:
|
||||
|
||||
```typescript
|
||||
const handle = timerService.schedule('explosion', 2000, () => {
|
||||
createExplosion();
|
||||
});
|
||||
|
||||
// Cancel early
|
||||
handle.cancel();
|
||||
```
|
||||
|
||||
### scheduleRepeating
|
||||
|
||||
Schedule a repeating timer:
|
||||
|
||||
```typescript
|
||||
// Execute every second
|
||||
timerService.scheduleRepeating('regen', 1000, () => {
|
||||
player.hp += 5;
|
||||
});
|
||||
|
||||
// Execute immediately once, then repeat every second
|
||||
timerService.scheduleRepeating('tick', 1000, () => {
|
||||
console.log('Tick');
|
||||
}, true); // immediate = true
|
||||
```
|
||||
|
||||
### cancel / cancelById
|
||||
|
||||
Cancel timers:
|
||||
|
||||
```typescript
|
||||
// Cancel by handle
|
||||
handle.cancel();
|
||||
// or
|
||||
timerService.cancel(handle);
|
||||
|
||||
// Cancel by ID
|
||||
timerService.cancelById('regen');
|
||||
```
|
||||
|
||||
### hasTimer
|
||||
|
||||
Check if timer exists:
|
||||
|
||||
```typescript
|
||||
if (timerService.hasTimer('explosion')) {
|
||||
console.log('Explosion is pending');
|
||||
}
|
||||
```
|
||||
|
||||
### getTimerInfo
|
||||
|
||||
Get timer information:
|
||||
|
||||
```typescript
|
||||
const info = timerService.getTimerInfo('explosion');
|
||||
if (info) {
|
||||
console.log(`Remaining: ${info.remaining}ms`);
|
||||
console.log(`Repeating: ${info.repeating}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Cooldown API
|
||||
|
||||
### startCooldown
|
||||
|
||||
Start a cooldown:
|
||||
|
||||
```typescript
|
||||
timerService.startCooldown('skill_fireball', 5000);
|
||||
```
|
||||
|
||||
### isCooldownReady / isOnCooldown
|
||||
|
||||
Check cooldown status:
|
||||
|
||||
```typescript
|
||||
if (timerService.isCooldownReady('skill_fireball')) {
|
||||
castFireball();
|
||||
timerService.startCooldown('skill_fireball', 5000);
|
||||
} else {
|
||||
console.log('Skill still on cooldown');
|
||||
}
|
||||
|
||||
// or use isOnCooldown
|
||||
if (timerService.isOnCooldown('skill_fireball')) {
|
||||
console.log('On cooldown...');
|
||||
}
|
||||
```
|
||||
|
||||
### getCooldownProgress / getCooldownRemaining
|
||||
|
||||
Get cooldown progress:
|
||||
|
||||
```typescript
|
||||
// Progress 0-1 (0=started, 1=complete)
|
||||
const progress = timerService.getCooldownProgress('skill_fireball');
|
||||
console.log(`Progress: ${(progress * 100).toFixed(0)}%`);
|
||||
|
||||
// Remaining time (ms)
|
||||
const remaining = timerService.getCooldownRemaining('skill_fireball');
|
||||
console.log(`Remaining: ${(remaining / 1000).toFixed(1)}s`);
|
||||
```
|
||||
|
||||
### getCooldownInfo
|
||||
|
||||
Get complete cooldown info:
|
||||
|
||||
```typescript
|
||||
const info = timerService.getCooldownInfo('skill_fireball');
|
||||
if (info) {
|
||||
console.log(`Duration: ${info.duration}ms`);
|
||||
console.log(`Remaining: ${info.remaining}ms`);
|
||||
console.log(`Progress: ${info.progress}`);
|
||||
console.log(`Ready: ${info.isReady}`);
|
||||
}
|
||||
```
|
||||
|
||||
### resetCooldown / clearAllCooldowns
|
||||
|
||||
Reset cooldowns:
|
||||
|
||||
```typescript
|
||||
// Reset single cooldown
|
||||
timerService.resetCooldown('skill_fireball');
|
||||
|
||||
// Clear all cooldowns (e.g., on respawn)
|
||||
timerService.clearAllCooldowns();
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
### update
|
||||
|
||||
Update timer service (call every frame):
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
timerService.update(deltaTime); // deltaTime in ms
|
||||
}
|
||||
```
|
||||
|
||||
### clear
|
||||
|
||||
Clear all timers and cooldowns:
|
||||
|
||||
```typescript
|
||||
timerService.clear();
|
||||
```
|
||||
|
||||
## Debug Properties
|
||||
|
||||
```typescript
|
||||
// Get active timer count
|
||||
console.log(timerService.activeTimerCount);
|
||||
|
||||
// Get active cooldown count
|
||||
console.log(timerService.activeCooldownCount);
|
||||
|
||||
// Get all active timer IDs
|
||||
const timerIds = timerService.getActiveTimerIds();
|
||||
|
||||
// Get all active cooldown IDs
|
||||
const cooldownIds = timerService.getActiveCooldownIds();
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
### Cooldown Nodes
|
||||
|
||||
- `StartCooldown` - Start cooldown
|
||||
- `IsCooldownReady` - Check if cooldown is ready
|
||||
- `GetCooldownProgress` - Get cooldown progress
|
||||
- `GetCooldownInfo` - Get cooldown info
|
||||
- `ResetCooldown` - Reset cooldown
|
||||
|
||||
### Timer Nodes
|
||||
|
||||
- `HasTimer` - Check if timer exists
|
||||
- `CancelTimer` - Cancel timer
|
||||
- `GetTimerRemaining` - Get timer remaining time
|
||||
|
||||
## Service Token
|
||||
|
||||
For dependency injection:
|
||||
|
||||
```typescript
|
||||
import { TimerServiceToken, createTimerService } from '@esengine/timer';
|
||||
|
||||
// Register service
|
||||
services.register(TimerServiceToken, createTimerService());
|
||||
|
||||
// Get service
|
||||
const timerService = services.get(TimerServiceToken);
|
||||
```
|
||||
223
docs/src/content/docs/en/modules/timer/best-practices.md
Normal file
223
docs/src/content/docs/en/modules/timer/best-practices.md
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Usage tips and ECS integration"
|
||||
---
|
||||
|
||||
## Usage Tips
|
||||
|
||||
### 1. Use Meaningful IDs
|
||||
|
||||
Use descriptive IDs for easier debugging and management:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
timerService.startCooldown('skill_fireball', 5000);
|
||||
timerService.schedule('explosion_wave_1', 1000, callback);
|
||||
|
||||
// Bad
|
||||
timerService.startCooldown('cd1', 5000);
|
||||
timerService.schedule('t1', 1000, callback);
|
||||
```
|
||||
|
||||
### 2. Avoid Duplicate IDs
|
||||
|
||||
Timers with the same ID will overwrite previous ones. Use unique IDs:
|
||||
|
||||
```typescript
|
||||
// Use unique IDs
|
||||
const uniqueId = `explosion_${entity.id}_${Date.now()}`;
|
||||
timerService.schedule(uniqueId, 1000, callback);
|
||||
|
||||
// Or use a counter
|
||||
let timerCounter = 0;
|
||||
const timerId = `timer_${++timerCounter}`;
|
||||
```
|
||||
|
||||
### 3. Clean Up Promptly
|
||||
|
||||
Clean up timers and cooldowns at appropriate times:
|
||||
|
||||
```typescript
|
||||
class Entity {
|
||||
private timerId: string;
|
||||
|
||||
onDestroy(): void {
|
||||
// Clean up timer when entity is destroyed
|
||||
this.timerService.cancelById(this.timerId);
|
||||
}
|
||||
}
|
||||
|
||||
class Scene {
|
||||
onUnload(): void {
|
||||
// Clear all when scene unloads
|
||||
this.timerService.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Configure Limits
|
||||
|
||||
Consider setting maximum limits in production:
|
||||
|
||||
```typescript
|
||||
const timerService = createTimerService({
|
||||
maxTimers: 1000,
|
||||
maxCooldowns: 500
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Batch Management
|
||||
|
||||
Use prefixes to manage related timers:
|
||||
|
||||
```typescript
|
||||
// Use unified prefix for all timers of an entity
|
||||
const prefix = `entity_${entityId}_`;
|
||||
|
||||
timerService.schedule(`${prefix}explosion`, 1000, callback1);
|
||||
timerService.schedule(`${prefix}effect`, 2000, callback2);
|
||||
|
||||
// Clean up by iterating with prefix
|
||||
function clearEntityTimers(entityId: number): void {
|
||||
const prefix = `entity_${entityId}_`;
|
||||
const ids = timerService.getActiveTimerIds();
|
||||
|
||||
for (const id of ids) {
|
||||
if (id.startsWith(prefix)) {
|
||||
timerService.cancelById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ECS Integration
|
||||
|
||||
### Timer Component
|
||||
|
||||
```typescript
|
||||
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import { createTimerService, type ITimerService } from '@esengine/timer';
|
||||
|
||||
// Timer component
|
||||
class TimerComponent extends Component {
|
||||
timerService: ITimerService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.timerService = createTimerService();
|
||||
}
|
||||
}
|
||||
|
||||
// Timer system
|
||||
class TimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(TimerComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const timer = entity.getComponent(TimerComponent);
|
||||
timer.timerService.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cooldown Component (Shared Cooldowns)
|
||||
|
||||
```typescript
|
||||
// Cooldown component for shared cooldowns
|
||||
class CooldownComponent extends Component {
|
||||
constructor(public timerService: ITimerService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple entities share the same cooldown service
|
||||
const sharedCooldowns = createTimerService();
|
||||
|
||||
entity1.addComponent(new CooldownComponent(sharedCooldowns));
|
||||
entity2.addComponent(new CooldownComponent(sharedCooldowns));
|
||||
```
|
||||
|
||||
### Global Timer Service
|
||||
|
||||
```typescript
|
||||
// Use service container for global timer
|
||||
import { TimerServiceToken, createTimerService } from '@esengine/timer';
|
||||
|
||||
// Register global service
|
||||
services.register(TimerServiceToken, createTimerService());
|
||||
|
||||
// Use in systems
|
||||
class EffectSystem extends EntitySystem {
|
||||
private timerService: ITimerService;
|
||||
|
||||
constructor(services: ServiceContainer) {
|
||||
super(Matcher.all(EffectComponent));
|
||||
this.timerService = services.get(TimerServiceToken);
|
||||
}
|
||||
|
||||
applyEffect(entity: Entity, effect: Effect): void {
|
||||
const id = `effect_${entity.id}_${effect.id}`;
|
||||
|
||||
this.timerService.schedule(id, effect.duration, () => {
|
||||
entity.removeComponent(effect);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Consolidate Updates
|
||||
|
||||
If you have multiple independent timer services, consider merging them:
|
||||
|
||||
```typescript
|
||||
// Not recommended: each entity has its own timer service
|
||||
class BadEntity {
|
||||
private timerService = createTimerService(); // Memory waste
|
||||
}
|
||||
|
||||
// Recommended: share timer service
|
||||
class GoodSystem {
|
||||
private timerService = createTimerService();
|
||||
|
||||
addTimer(entityId: number, callback: () => void): void {
|
||||
this.timerService.schedule(`entity_${entityId}`, 1000, callback);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid Frequent Creation
|
||||
|
||||
Reuse timer IDs instead of creating new ones:
|
||||
|
||||
```typescript
|
||||
// Not recommended: create new timer every time
|
||||
function onHit(): void {
|
||||
timerService.schedule(`hit_${Date.now()}`, 100, showHitEffect);
|
||||
}
|
||||
|
||||
// Recommended: cancel old timer and reuse ID
|
||||
function onHit(): void {
|
||||
timerService.cancelById('hit_effect');
|
||||
timerService.schedule('hit_effect', 100, showHitEffect);
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cooldowns Instead of Timers
|
||||
|
||||
For scenarios without callbacks, cooldowns are more efficient:
|
||||
|
||||
```typescript
|
||||
// Use cooldown to limit attack frequency
|
||||
if (timerService.isCooldownReady('attack')) {
|
||||
attack();
|
||||
timerService.startCooldown('attack', 1000);
|
||||
}
|
||||
|
||||
// Instead of
|
||||
timerService.schedule('attack_cooldown', 1000, () => {
|
||||
canAttack = true;
|
||||
});
|
||||
```
|
||||
235
docs/src/content/docs/en/modules/timer/examples.md
Normal file
235
docs/src/content/docs/en/modules/timer/examples.md
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "Skill cooldowns, DOT effects, buff systems and more"
|
||||
---
|
||||
|
||||
## Skill Cooldown System
|
||||
|
||||
```typescript
|
||||
import { createTimerService, type ITimerService } from '@esengine/timer';
|
||||
|
||||
class SkillSystem {
|
||||
private timerService: ITimerService;
|
||||
private skills: Map<string, SkillData> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.timerService = createTimerService();
|
||||
}
|
||||
|
||||
registerSkill(id: string, data: SkillData): void {
|
||||
this.skills.set(id, data);
|
||||
}
|
||||
|
||||
useSkill(skillId: string): boolean {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill) return false;
|
||||
|
||||
// Check cooldown
|
||||
if (!this.timerService.isCooldownReady(skillId)) {
|
||||
const remaining = this.timerService.getCooldownRemaining(skillId);
|
||||
console.log(`Skill ${skillId} on cooldown, ${remaining}ms remaining`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use skill
|
||||
this.executeSkill(skill);
|
||||
|
||||
// Start cooldown
|
||||
this.timerService.startCooldown(skillId, skill.cooldown);
|
||||
return true;
|
||||
}
|
||||
|
||||
getSkillCooldownProgress(skillId: string): number {
|
||||
return this.timerService.getCooldownProgress(skillId);
|
||||
}
|
||||
|
||||
update(dt: number): void {
|
||||
this.timerService.update(dt);
|
||||
}
|
||||
}
|
||||
|
||||
interface SkillData {
|
||||
cooldown: number;
|
||||
// ... other properties
|
||||
}
|
||||
```
|
||||
|
||||
## Delayed and Timed Effects
|
||||
|
||||
```typescript
|
||||
class EffectSystem {
|
||||
private timerService: ITimerService;
|
||||
|
||||
constructor(timerService: ITimerService) {
|
||||
this.timerService = timerService;
|
||||
}
|
||||
|
||||
// Delayed explosion
|
||||
scheduleExplosion(position: { x: number; y: number }, delay: number): void {
|
||||
this.timerService.schedule(`explosion_${Date.now()}`, delay, () => {
|
||||
this.createExplosion(position);
|
||||
});
|
||||
}
|
||||
|
||||
// DOT damage (damage every second)
|
||||
applyDOT(target: Entity, damage: number, duration: number): void {
|
||||
const dotId = `dot_${target.id}_${Date.now()}`;
|
||||
let elapsed = 0;
|
||||
|
||||
this.timerService.scheduleRepeating(dotId, 1000, () => {
|
||||
elapsed += 1000;
|
||||
target.takeDamage(damage);
|
||||
|
||||
if (elapsed >= duration) {
|
||||
this.timerService.cancelById(dotId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// BUFF effect (lasts for a duration)
|
||||
applyBuff(target: Entity, buffId: string, duration: number): void {
|
||||
target.addBuff(buffId);
|
||||
|
||||
this.timerService.schedule(`buff_expire_${buffId}`, duration, () => {
|
||||
target.removeBuff(buffId);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Combo System
|
||||
|
||||
```typescript
|
||||
class ComboSystem {
|
||||
private timerService: ITimerService;
|
||||
private comboCount = 0;
|
||||
private comboWindowId = 'combo_window';
|
||||
|
||||
constructor(timerService: ITimerService) {
|
||||
this.timerService = timerService;
|
||||
}
|
||||
|
||||
onAttack(): void {
|
||||
// Increase combo count
|
||||
this.comboCount++;
|
||||
|
||||
// Cancel previous combo window
|
||||
this.timerService.cancelById(this.comboWindowId);
|
||||
|
||||
// Start new combo window (reset if no action within 2 seconds)
|
||||
this.timerService.schedule(this.comboWindowId, 2000, () => {
|
||||
this.comboCount = 0;
|
||||
console.log('Combo reset');
|
||||
});
|
||||
|
||||
console.log(`Combo: ${this.comboCount}x`);
|
||||
}
|
||||
|
||||
getComboMultiplier(): number {
|
||||
return 1 + this.comboCount * 0.1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Auto-Save System
|
||||
|
||||
```typescript
|
||||
class AutoSaveSystem {
|
||||
private timerService: ITimerService;
|
||||
|
||||
constructor(timerService: ITimerService) {
|
||||
this.timerService = timerService;
|
||||
this.startAutoSave();
|
||||
}
|
||||
|
||||
private startAutoSave(): void {
|
||||
// Auto-save every 5 minutes
|
||||
this.timerService.scheduleRepeating('autosave', 5 * 60 * 1000, () => {
|
||||
this.saveGame();
|
||||
console.log('Game auto-saved');
|
||||
});
|
||||
}
|
||||
|
||||
private saveGame(): void {
|
||||
// Save logic
|
||||
}
|
||||
|
||||
stopAutoSave(): void {
|
||||
this.timerService.cancelById('autosave');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Charge Skill System
|
||||
|
||||
```typescript
|
||||
class ChargeSkillSystem {
|
||||
private timerService: ITimerService;
|
||||
private chargeStartTime = 0;
|
||||
private maxChargeTime = 3000; // 3 seconds max charge
|
||||
|
||||
constructor(timerService: ITimerService) {
|
||||
this.timerService = timerService;
|
||||
}
|
||||
|
||||
startCharge(): void {
|
||||
this.chargeStartTime = performance.now();
|
||||
|
||||
// Auto-release when fully charged
|
||||
this.timerService.schedule('charge_complete', this.maxChargeTime, () => {
|
||||
this.releaseSkill();
|
||||
});
|
||||
}
|
||||
|
||||
releaseSkill(): void {
|
||||
this.timerService.cancelById('charge_complete');
|
||||
|
||||
const chargeTime = performance.now() - this.chargeStartTime;
|
||||
const chargePercent = Math.min(chargeTime / this.maxChargeTime, 1);
|
||||
|
||||
const damage = 100 + chargePercent * 200; // 100-300 damage
|
||||
console.log(`Release skill with ${damage} damage (${(chargePercent * 100).toFixed(0)}% charge)`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Quest Timer
|
||||
|
||||
```typescript
|
||||
class QuestTimerSystem {
|
||||
private timerService: ITimerService;
|
||||
|
||||
constructor(timerService: ITimerService) {
|
||||
this.timerService = timerService;
|
||||
}
|
||||
|
||||
startTimedQuest(questId: string, timeLimit: number): void {
|
||||
this.timerService.schedule(`quest_${questId}_timeout`, timeLimit, () => {
|
||||
this.failQuest(questId);
|
||||
});
|
||||
|
||||
// UI update for remaining time
|
||||
this.timerService.scheduleRepeating(`quest_${questId}_tick`, 1000, () => {
|
||||
const info = this.timerService.getTimerInfo(`quest_${questId}_timeout`);
|
||||
if (info) {
|
||||
this.updateQuestTimerUI(questId, info.remaining);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
completeQuest(questId: string): void {
|
||||
this.timerService.cancelById(`quest_${questId}_timeout`);
|
||||
this.timerService.cancelById(`quest_${questId}_tick`);
|
||||
console.log(`Quest ${questId} completed!`);
|
||||
}
|
||||
|
||||
private failQuest(questId: string): void {
|
||||
this.timerService.cancelById(`quest_${questId}_tick`);
|
||||
console.log(`Quest ${questId} failed - time's up!`);
|
||||
}
|
||||
|
||||
private updateQuestTimerUI(questId: string, remaining: number): void {
|
||||
// Update UI
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "Timer System"
|
||||
description: "Flexible timer and cooldown system"
|
||||
---
|
||||
|
||||
`@esengine/timer` provides a flexible timer and cooldown system for delayed execution, repeating tasks, skill cooldowns, and more.
|
||||
@@ -67,8 +68,6 @@ interface TimerHandle {
|
||||
|
||||
### TimerInfo
|
||||
|
||||
Timer information object:
|
||||
|
||||
```typescript
|
||||
interface TimerInfo {
|
||||
readonly id: string; // Timer ID
|
||||
@@ -80,8 +79,6 @@ interface TimerInfo {
|
||||
|
||||
### CooldownInfo
|
||||
|
||||
Cooldown information object:
|
||||
|
||||
```typescript
|
||||
interface CooldownInfo {
|
||||
readonly id: string; // Cooldown ID
|
||||
@@ -92,263 +89,8 @@ interface CooldownInfo {
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
## Documentation
|
||||
|
||||
### createTimerService
|
||||
|
||||
```typescript
|
||||
function createTimerService(config?: TimerServiceConfig): ITimerService
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `maxTimers` | `number` | `0` | Maximum timer count (0 = unlimited) |
|
||||
| `maxCooldowns` | `number` | `0` | Maximum cooldown count (0 = unlimited) |
|
||||
|
||||
### Timer API
|
||||
|
||||
#### schedule
|
||||
|
||||
Schedule a one-time timer:
|
||||
|
||||
```typescript
|
||||
const handle = timerService.schedule('explosion', 2000, () => {
|
||||
createExplosion();
|
||||
});
|
||||
|
||||
// Cancel early
|
||||
handle.cancel();
|
||||
```
|
||||
|
||||
#### scheduleRepeating
|
||||
|
||||
Schedule a repeating timer:
|
||||
|
||||
```typescript
|
||||
// Execute every second
|
||||
timerService.scheduleRepeating('regen', 1000, () => {
|
||||
player.hp += 5;
|
||||
});
|
||||
|
||||
// Execute immediately once, then repeat every second
|
||||
timerService.scheduleRepeating('tick', 1000, () => {
|
||||
console.log('Tick');
|
||||
}, true); // immediate = true
|
||||
```
|
||||
|
||||
#### cancel / cancelById
|
||||
|
||||
Cancel timers:
|
||||
|
||||
```typescript
|
||||
// Cancel by handle
|
||||
handle.cancel();
|
||||
// or
|
||||
timerService.cancel(handle);
|
||||
|
||||
// Cancel by ID
|
||||
timerService.cancelById('regen');
|
||||
```
|
||||
|
||||
#### hasTimer
|
||||
|
||||
Check if timer exists:
|
||||
|
||||
```typescript
|
||||
if (timerService.hasTimer('explosion')) {
|
||||
console.log('Explosion is pending');
|
||||
}
|
||||
```
|
||||
|
||||
#### getTimerInfo
|
||||
|
||||
Get timer information:
|
||||
|
||||
```typescript
|
||||
const info = timerService.getTimerInfo('explosion');
|
||||
if (info) {
|
||||
console.log(`Remaining: ${info.remaining}ms`);
|
||||
console.log(`Repeating: ${info.repeating}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Cooldown API
|
||||
|
||||
#### startCooldown
|
||||
|
||||
Start a cooldown:
|
||||
|
||||
```typescript
|
||||
timerService.startCooldown('skill_fireball', 5000);
|
||||
```
|
||||
|
||||
#### isCooldownReady / isOnCooldown
|
||||
|
||||
Check cooldown status:
|
||||
|
||||
```typescript
|
||||
if (timerService.isCooldownReady('skill_fireball')) {
|
||||
castFireball();
|
||||
timerService.startCooldown('skill_fireball', 5000);
|
||||
}
|
||||
|
||||
if (timerService.isOnCooldown('skill_fireball')) {
|
||||
console.log('On cooldown...');
|
||||
}
|
||||
```
|
||||
|
||||
#### getCooldownProgress / getCooldownRemaining
|
||||
|
||||
Get cooldown progress:
|
||||
|
||||
```typescript
|
||||
// Progress 0-1 (0=started, 1=complete)
|
||||
const progress = timerService.getCooldownProgress('skill_fireball');
|
||||
console.log(`Progress: ${(progress * 100).toFixed(0)}%`);
|
||||
|
||||
// Remaining time (ms)
|
||||
const remaining = timerService.getCooldownRemaining('skill_fireball');
|
||||
console.log(`Remaining: ${(remaining / 1000).toFixed(1)}s`);
|
||||
```
|
||||
|
||||
#### getCooldownInfo
|
||||
|
||||
Get complete cooldown info:
|
||||
|
||||
```typescript
|
||||
const info = timerService.getCooldownInfo('skill_fireball');
|
||||
if (info) {
|
||||
console.log(`Duration: ${info.duration}ms`);
|
||||
console.log(`Remaining: ${info.remaining}ms`);
|
||||
console.log(`Progress: ${info.progress}`);
|
||||
console.log(`Ready: ${info.isReady}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### resetCooldown / clearAllCooldowns
|
||||
|
||||
Reset cooldowns:
|
||||
|
||||
```typescript
|
||||
// Reset single cooldown
|
||||
timerService.resetCooldown('skill_fireball');
|
||||
|
||||
// Clear all cooldowns (e.g., on respawn)
|
||||
timerService.clearAllCooldowns();
|
||||
```
|
||||
|
||||
### Lifecycle
|
||||
|
||||
#### update
|
||||
|
||||
Update timer service (call every frame):
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
timerService.update(deltaTime); // deltaTime in ms
|
||||
}
|
||||
```
|
||||
|
||||
#### clear
|
||||
|
||||
Clear all timers and cooldowns:
|
||||
|
||||
```typescript
|
||||
timerService.clear();
|
||||
```
|
||||
|
||||
### Debug Properties
|
||||
|
||||
```typescript
|
||||
console.log(timerService.activeTimerCount);
|
||||
console.log(timerService.activeCooldownCount);
|
||||
const timerIds = timerService.getActiveTimerIds();
|
||||
const cooldownIds = timerService.getActiveCooldownIds();
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Skill Cooldown System
|
||||
|
||||
```typescript
|
||||
import { createTimerService, type ITimerService } from '@esengine/timer';
|
||||
|
||||
class SkillSystem {
|
||||
private timerService: ITimerService;
|
||||
private skills: Map<string, SkillData> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.timerService = createTimerService();
|
||||
}
|
||||
|
||||
useSkill(skillId: string): boolean {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill) return false;
|
||||
|
||||
if (!this.timerService.isCooldownReady(skillId)) {
|
||||
const remaining = this.timerService.getCooldownRemaining(skillId);
|
||||
console.log(`Skill ${skillId} on cooldown, ${remaining}ms remaining`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.executeSkill(skill);
|
||||
this.timerService.startCooldown(skillId, skill.cooldown);
|
||||
return true;
|
||||
}
|
||||
|
||||
update(dt: number): void {
|
||||
this.timerService.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DOT Effects
|
||||
|
||||
```typescript
|
||||
class EffectSystem {
|
||||
private timerService: ITimerService;
|
||||
|
||||
applyDOT(target: Entity, damage: number, duration: number): void {
|
||||
const dotId = `dot_${target.id}_${Date.now()}`;
|
||||
let elapsed = 0;
|
||||
|
||||
this.timerService.scheduleRepeating(dotId, 1000, () => {
|
||||
elapsed += 1000;
|
||||
target.takeDamage(damage);
|
||||
|
||||
if (elapsed >= duration) {
|
||||
this.timerService.cancelById(dotId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
### Cooldown Nodes
|
||||
|
||||
- `StartCooldown` - Start cooldown
|
||||
- `IsCooldownReady` - Check if cooldown is ready
|
||||
- `GetCooldownProgress` - Get cooldown progress
|
||||
- `GetCooldownInfo` - Get cooldown info
|
||||
- `ResetCooldown` - Reset cooldown
|
||||
|
||||
### Timer Nodes
|
||||
|
||||
- `HasTimer` - Check if timer exists
|
||||
- `CancelTimer` - Cancel timer
|
||||
- `GetTimerRemaining` - Get timer remaining time
|
||||
|
||||
## Service Token
|
||||
|
||||
For dependency injection:
|
||||
|
||||
```typescript
|
||||
import { TimerServiceToken, createTimerService } from '@esengine/timer';
|
||||
|
||||
services.register(TimerServiceToken, createTimerService());
|
||||
const timerService = services.get(TimerServiceToken);
|
||||
```
|
||||
- [API Reference](./api) - Complete timer and cooldown API
|
||||
- [Examples](./examples) - Skill cooldowns, DOT effects, buff systems
|
||||
- [Best Practices](./best-practices) - Usage tips and ECS integration
|
||||
|
||||
@@ -1,645 +0,0 @@
|
||||
---
|
||||
title: "插件系统"
|
||||
---
|
||||
|
||||
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是插件
|
||||
|
||||
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
|
||||
|
||||
- 注册自定义服务到服务容器
|
||||
- 添加系统到场景
|
||||
- 注册自定义组件
|
||||
- 扩展框架功能
|
||||
|
||||
### 插件的优势
|
||||
|
||||
- **模块化**: 将功能封装为独立模块,提高代码可维护性
|
||||
- **可复用**: 同一个插件可以在多个项目中使用
|
||||
- **解耦**: 核心框架与扩展功能分离
|
||||
- **热插拔**: 运行时动态安装和卸载插件
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建第一个插件
|
||||
|
||||
创建一个简单的调试插件:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// 可以在这里注册服务、添加系统等
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装插件
|
||||
|
||||
使用 `Core.installPlugin()` 安装插件:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 安装插件
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载插件
|
||||
|
||||
```typescript
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### 获取插件实例
|
||||
|
||||
```typescript
|
||||
// 获取已安装的插件
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发
|
||||
|
||||
### IPlugin 接口
|
||||
|
||||
所有插件必须实现 `IPlugin` 接口:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// 插件唯一名称
|
||||
readonly name: string;
|
||||
|
||||
// 插件版本(建议遵循semver规范)
|
||||
readonly version: string;
|
||||
|
||||
// 依赖的其他插件(可选)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// 安装插件时调用
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// 卸载插件时调用
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 插件生命周期
|
||||
|
||||
#### install 方法
|
||||
|
||||
在插件安装时调用,用于初始化插件:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. 访问当前场景
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. 添加系统
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. 其他初始化逻辑
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall 方法
|
||||
|
||||
在插件卸载时调用,用于清理资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理服务
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 释放其他资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步插件
|
||||
|
||||
插件的 `install` 和 `uninstall` 方法都支持异步:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// 异步加载资源
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// 使用加载的配置初始化服务
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 异步清理
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// 保存插件状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### 注册服务
|
||||
|
||||
插件可以向服务容器注册自己的服务:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// 解析并使用服务
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务容器会自动调用服务的dispose方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加系统
|
||||
|
||||
插件可以向场景添加自定义系统:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 物理模拟逻辑
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除系统
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖管理
|
||||
|
||||
### 声明依赖
|
||||
|
||||
插件可以声明对其他插件的依赖:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// 声明依赖基础物理插件
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 可以安全地使用physics-plugin提供的服务
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖检查
|
||||
|
||||
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
|
||||
|
||||
```typescript
|
||||
// 错误:physics-plugin 未安装
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// 正确:先安装依赖
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### 卸载顺序
|
||||
|
||||
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// 错误:physics-plugin 被 advanced-physics 依赖
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// 正确:先卸载依赖它的插件
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## 插件管理
|
||||
|
||||
### 通过 Core 管理
|
||||
|
||||
Core 类提供了便捷的插件管理方法:
|
||||
|
||||
```typescript
|
||||
// 安装插件
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### 通过 PluginManager 管理
|
||||
|
||||
也可以直接使用 PluginManager 服务:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有插件
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// 获取插件元数据
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// 获取所有插件元数据
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 实用插件示例
|
||||
|
||||
### 网络同步插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// 处理网络消息
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// 自动连接
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务会自动dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能分析插件
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// 定期输出性能报告
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 命名规范
|
||||
|
||||
- 插件名称使用小写字母和连字符:`my-awesome-plugin`
|
||||
- 版本号遵循语义化版本规范:`1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // 好
|
||||
readonly version = '1.0.0'; // 好
|
||||
}
|
||||
```
|
||||
|
||||
### 清理资源
|
||||
|
||||
始终在 `uninstall` 中清理插件创建的所有资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 添加定时器
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// 添加事件监听
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理定时器
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
在插件中妥善处理错误,避免影响整个应用:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// 可能失败的操作
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // 重新抛出,让框架知道安装失败
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// 即使清理失败也不应该阻止卸载
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// 加载配置
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置化
|
||||
|
||||
允许用户配置插件行为:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 插件安装失败
|
||||
|
||||
**问题**: 插件安装时抛出错误
|
||||
|
||||
**原因**:
|
||||
- 依赖未满足
|
||||
- install 方法中有异常
|
||||
- 服务注册冲突
|
||||
|
||||
**解决**:
|
||||
1. 检查依赖是否已安装
|
||||
2. 查看错误日志
|
||||
3. 确保服务名称不冲突
|
||||
|
||||
### 插件卸载后仍有副作用
|
||||
|
||||
**问题**: 卸载插件后,插件的功能仍在运行
|
||||
|
||||
**原因**: uninstall 方法中未正确清理资源
|
||||
|
||||
**解决**: 确保在 uninstall 中清理:
|
||||
- 定时器
|
||||
- 事件监听器
|
||||
- WebSocket连接
|
||||
- 系统引用
|
||||
|
||||
### 何时使用插件
|
||||
|
||||
**适合使用插件**:
|
||||
- 可选功能(调试工具、性能分析)
|
||||
- 第三方集成(网络库、物理引擎)
|
||||
- 跨项目复用的功能模块
|
||||
|
||||
**不适合使用插件**:
|
||||
- 核心游戏逻辑
|
||||
- 简单的工具类
|
||||
- 项目特定的功能
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [服务容器](./service-container/) - 在插件中使用服务容器
|
||||
- [系统架构](./system/) - 在插件中添加系统
|
||||
- [快速开始](./getting-started/) - Core 初始化和基础使用
|
||||
151
docs/src/content/docs/guide/plugin-system/best-practices.md
Normal file
151
docs/src/content/docs/guide/plugin-system/best-practices.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "最佳实践"
|
||||
description: "插件设计规范和常见问题"
|
||||
---
|
||||
|
||||
## 命名规范
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
// 使用小写字母和连字符
|
||||
readonly name = 'my-awesome-plugin'; // ✅
|
||||
|
||||
// 遵循语义化版本
|
||||
readonly version = '1.0.0'; // ✅
|
||||
}
|
||||
```
|
||||
|
||||
## 清理资源
|
||||
|
||||
始终在 `uninstall` 中清理插件创建的所有资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 添加定时器
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// 添加事件监听
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理定时器
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // 重新抛出,让框架知道安装失败
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// 即使清理失败也不应该阻止卸载
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() { /* ... */ }
|
||||
private async cleanup() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 配置化
|
||||
|
||||
允许用户配置插件行为:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 插件安装失败
|
||||
|
||||
**原因**:
|
||||
- 依赖未满足
|
||||
- install 方法中有异常
|
||||
- 服务注册冲突
|
||||
|
||||
**解决**:
|
||||
1. 检查依赖是否已安装
|
||||
2. 查看错误日志
|
||||
3. 确保服务名称不冲突
|
||||
|
||||
### 插件卸载后仍有副作用
|
||||
|
||||
**原因**: uninstall 方法中未正确清理资源
|
||||
|
||||
**解决**: 确保在 uninstall 中清理:
|
||||
- 定时器
|
||||
- 事件监听器
|
||||
- WebSocket 连接
|
||||
- 系统引用
|
||||
|
||||
### 何时使用插件
|
||||
|
||||
| 适合使用插件 | 不适合使用插件 |
|
||||
|-------------|---------------|
|
||||
| 可选功能(调试工具、性能分析) | 核心游戏逻辑 |
|
||||
| 第三方集成(网络库、物理引擎) | 简单的工具类 |
|
||||
| 跨项目复用的功能模块 | 项目特定的功能 |
|
||||
106
docs/src/content/docs/guide/plugin-system/dependencies.md
Normal file
106
docs/src/content/docs/guide/plugin-system/dependencies.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: "依赖管理"
|
||||
description: "声明和检查插件依赖"
|
||||
---
|
||||
|
||||
## 声明依赖
|
||||
|
||||
插件可以声明对其他插件的依赖:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// 声明依赖基础物理插件
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 可以安全地使用physics-plugin提供的服务
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖检查
|
||||
|
||||
框架会自动检查依赖关系,如果依赖未满足会抛出错误:
|
||||
|
||||
```typescript
|
||||
// 错误:physics-plugin 未安装
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// 正确:先安装依赖
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
## 卸载顺序
|
||||
|
||||
框架会检查依赖关系,防止卸载被其他插件依赖的插件:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// 错误:physics-plugin 被 advanced-physics 依赖
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// 正确:先卸载依赖它的插件
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## 依赖图示例
|
||||
|
||||
```
|
||||
physics-plugin (基础)
|
||||
↑
|
||||
advanced-physics (依赖 physics-plugin)
|
||||
↑
|
||||
game-physics (依赖 advanced-physics)
|
||||
```
|
||||
|
||||
安装顺序:`physics-plugin` → `advanced-physics` → `game-physics`
|
||||
|
||||
卸载顺序:`game-physics` → `advanced-physics` → `physics-plugin`
|
||||
|
||||
## 多依赖
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
// 声明多个依赖
|
||||
readonly dependencies = [
|
||||
'physics-plugin',
|
||||
'network-plugin',
|
||||
'audio-plugin'
|
||||
] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 所有依赖都已可用
|
||||
const physics = services.resolve(PhysicsService);
|
||||
const network = services.resolve(NetworkService);
|
||||
const audio = services.resolve(AudioService);
|
||||
}
|
||||
|
||||
uninstall(): void {}
|
||||
}
|
||||
```
|
||||
139
docs/src/content/docs/guide/plugin-system/development.md
Normal file
139
docs/src/content/docs/guide/plugin-system/development.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: "插件开发"
|
||||
description: "IPlugin 接口和生命周期"
|
||||
---
|
||||
|
||||
## IPlugin 接口
|
||||
|
||||
所有插件必须实现 `IPlugin` 接口:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// 插件唯一名称
|
||||
readonly name: string;
|
||||
|
||||
// 插件版本(建议遵循semver规范)
|
||||
readonly version: string;
|
||||
|
||||
// 依赖的其他插件(可选)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// 安装插件时调用
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// 卸载插件时调用
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## 生命周期方法
|
||||
|
||||
### install 方法
|
||||
|
||||
在插件安装时调用,用于初始化插件:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. 访问当前场景
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. 添加系统
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. 其他初始化逻辑
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### uninstall 方法
|
||||
|
||||
在插件卸载时调用,用于清理资源:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理服务
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 释放其他资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 异步插件
|
||||
|
||||
插件的 `install` 和 `uninstall` 方法都支持异步:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// 异步加载资源
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// 使用加载的配置初始化服务
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 异步清理
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// 保存插件状态
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
## 生命周期流程
|
||||
|
||||
```
|
||||
安装: Core.installPlugin(plugin)
|
||||
↓
|
||||
依赖检查: 检查 dependencies 是否满足
|
||||
↓
|
||||
调用 install(): 注册服务、添加系统
|
||||
↓
|
||||
状态更新: 标记为已安装
|
||||
|
||||
卸载: Core.uninstallPlugin(name)
|
||||
↓
|
||||
依赖检查: 检查是否被其他插件依赖
|
||||
↓
|
||||
调用 uninstall(): 清理资源
|
||||
↓
|
||||
状态更新: 从插件列表移除
|
||||
```
|
||||
188
docs/src/content/docs/guide/plugin-system/examples.md
Normal file
188
docs/src/content/docs/guide/plugin-system/examples.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
title: "示例插件"
|
||||
description: "完整的插件实现示例"
|
||||
---
|
||||
|
||||
## 网络同步插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// 处理网络消息
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// 自动连接
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务会自动dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能分析插件
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// 定期输出性能报告
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 清理
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试工具插件
|
||||
|
||||
```typescript
|
||||
class DebugToolsPlugin implements IPlugin {
|
||||
readonly name = 'debug-tools';
|
||||
readonly version = '1.0.0';
|
||||
private debugUI?: DebugUI;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 创建调试UI
|
||||
this.debugUI = new DebugUI();
|
||||
this.debugUI.mount(document.body);
|
||||
|
||||
// 注册快捷键
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
// 添加调试系统
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new DebugRenderSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除UI
|
||||
if (this.debugUI) {
|
||||
this.debugUI.unmount();
|
||||
this.debugUI = undefined;
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F12') {
|
||||
this.debugUI?.toggle();
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 音频插件
|
||||
|
||||
```typescript
|
||||
class AudioPlugin implements IPlugin {
|
||||
readonly name = 'audio';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: { volume: number }) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const audioService = new AudioService(this.config);
|
||||
services.registerInstance(AudioService, audioService);
|
||||
|
||||
// 添加音频系统
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new AudioSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 停止所有音频
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.stopAll();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
await Core.installPlugin(new AudioPlugin({ volume: 0.8 }));
|
||||
```
|
||||
|
||||
## 输入管理插件
|
||||
|
||||
```typescript
|
||||
class InputPlugin implements IPlugin {
|
||||
readonly name = 'input';
|
||||
readonly version = '1.0.0';
|
||||
private inputManager?: InputManager;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.inputManager = new InputManager();
|
||||
services.registerInstance(InputManager, this.inputManager);
|
||||
|
||||
// 绑定默认按键
|
||||
this.inputManager.bind('jump', ['Space', 'KeyW']);
|
||||
this.inputManager.bind('attack', ['MouseLeft', 'KeyJ']);
|
||||
|
||||
// 添加输入系统
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
scene.addSystem(new InputSystem());
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
if (this.inputManager) {
|
||||
this.inputManager.dispose();
|
||||
this.inputManager = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
85
docs/src/content/docs/guide/plugin-system/index.md
Normal file
85
docs/src/content/docs/guide/plugin-system/index.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "插件系统"
|
||||
description: "以模块化方式扩展 ECS Framework"
|
||||
---
|
||||
|
||||
插件系统允许你以模块化的方式扩展 ECS Framework 的功能。通过插件,你可以封装特定功能(如网络同步、物理引擎、调试工具等),并在多个项目中复用。
|
||||
|
||||
## 什么是插件
|
||||
|
||||
插件是实现了 `IPlugin` 接口的类,可以在运行时动态安装到框架中。插件可以:
|
||||
|
||||
- 注册自定义服务到服务容器
|
||||
- 添加系统到场景
|
||||
- 注册自定义组件
|
||||
- 扩展框架功能
|
||||
|
||||
## 插件的优势
|
||||
|
||||
| 优势 | 说明 |
|
||||
|------|------|
|
||||
| **模块化** | 将功能封装为独立模块,提高代码可维护性 |
|
||||
| **可复用** | 同一个插件可以在多个项目中使用 |
|
||||
| **解耦** | 核心框架与扩展功能分离 |
|
||||
| **热插拔** | 运行时动态安装和卸载插件 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建插件
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装插件
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 安装插件
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### 卸载插件
|
||||
|
||||
```typescript
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### 获取插件实例
|
||||
|
||||
```typescript
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件开发](./development/) - IPlugin 接口和生命周期
|
||||
- [服务与系统](./services-systems/) - 注册服务和添加系统
|
||||
- [依赖管理](./dependencies/) - 声明和检查依赖
|
||||
- [插件管理](./management/) - 通过 Core 和 PluginManager 管理
|
||||
- [示例插件](./examples/) - 完整示例
|
||||
- [最佳实践](./best-practices/) - 设计规范
|
||||
93
docs/src/content/docs/guide/plugin-system/management.md
Normal file
93
docs/src/content/docs/guide/plugin-system/management.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: "插件管理"
|
||||
description: "通过 Core 和 PluginManager 管理插件"
|
||||
---
|
||||
|
||||
## 通过 Core 管理
|
||||
|
||||
Core 类提供了便捷的插件管理方法:
|
||||
|
||||
```typescript
|
||||
// 安装插件
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// 卸载插件
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
## 通过 PluginManager 管理
|
||||
|
||||
也可以直接使用 PluginManager 服务:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有插件
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// 获取插件元数据
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// 获取所有插件元数据
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `installPlugin(plugin)` | 安装插件 |
|
||||
| `uninstallPlugin(name)` | 卸载插件 |
|
||||
| `isPluginInstalled(name)` | 检查是否已安装 |
|
||||
| `getPlugin(name)` | 获取插件实例 |
|
||||
|
||||
### PluginManager 方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `getAllPlugins()` | 获取所有插件 |
|
||||
| `getMetadata(name)` | 获取插件元数据 |
|
||||
| `getAllMetadata()` | 获取所有插件元数据 |
|
||||
|
||||
## 插件状态
|
||||
|
||||
```typescript
|
||||
enum PluginState {
|
||||
Pending = 'pending',
|
||||
Installing = 'installing',
|
||||
Installed = 'installed',
|
||||
Uninstalling = 'uninstalling',
|
||||
Failed = 'failed'
|
||||
}
|
||||
```
|
||||
|
||||
## 元数据信息
|
||||
|
||||
```typescript
|
||||
interface PluginMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
state: PluginState;
|
||||
dependencies?: string[];
|
||||
installedAt?: number;
|
||||
error?: Error;
|
||||
}
|
||||
```
|
||||
133
docs/src/content/docs/guide/plugin-system/services-systems.md
Normal file
133
docs/src/content/docs/guide/plugin-system/services-systems.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "服务与系统"
|
||||
description: "在插件中注册服务和添加系统"
|
||||
---
|
||||
|
||||
## 注册服务
|
||||
|
||||
插件可以向服务容器注册自己的服务:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 注册网络服务
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// 解析并使用服务
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 服务容器会自动调用服务的dispose方法
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 服务注册方式
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `registerSingleton(Type)` | 注册单例服务 |
|
||||
| `registerInstance(Type, instance)` | 注册现有实例 |
|
||||
| `registerTransient(Type)` | 每次解析创建新实例 |
|
||||
|
||||
## 添加系统
|
||||
|
||||
插件可以向场景添加自定义系统:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 物理模拟逻辑
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除系统
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组合使用
|
||||
|
||||
```typescript
|
||||
class GamePlugin implements IPlugin {
|
||||
readonly name = 'game-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private systems: EntitySystem[] = [];
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. 注册服务
|
||||
services.registerSingleton(ScoreService);
|
||||
services.registerSingleton(AudioService);
|
||||
|
||||
// 2. 添加系统
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
const systems = [
|
||||
new InputSystem(),
|
||||
new MovementSystem(),
|
||||
new ScoringSystem()
|
||||
];
|
||||
|
||||
systems.forEach(system => {
|
||||
scene.addSystem(system);
|
||||
this.systems.push(system);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// 移除所有系统
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
this.systems.forEach(system => {
|
||||
scene.removeSystem(system);
|
||||
});
|
||||
}
|
||||
this.systems = [];
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,666 +0,0 @@
|
||||
---
|
||||
title: "scene"
|
||||
---
|
||||
|
||||
# 场景管理
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
## 基本概念
|
||||
|
||||
场景是 ECS 框架的核心容器,提供:
|
||||
- 实体的创建、管理和销毁
|
||||
- 系统的注册和执行调度
|
||||
- 组件的存储和查询
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
1. **[SceneManager](./scene-manager/)** - 适用于 95% 的游戏应用
|
||||
- 单人游戏、简单多人游戏、移动游戏
|
||||
- 轻量级,简单直观的 API
|
||||
- 支持场景切换
|
||||
|
||||
2. **[WorldManager](./world-manager/)** - 适用于高级多世界隔离场景
|
||||
- MMO 游戏服务器、游戏房间系统
|
||||
- 多 World 管理,每个 World 可包含多个场景
|
||||
- 完全隔离的独立环境
|
||||
|
||||
本文档重点介绍 Scene 类本身的使用方法。关于场景管理器的详细信息,请查看对应的文档。
|
||||
|
||||
## 创建场景
|
||||
|
||||
### 继承 Scene 类
|
||||
|
||||
**推荐做法:继承 Scene 类来创建自定义场景**
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 设置场景名称
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
|
||||
// 创建初始实体
|
||||
this.createInitialEntities();
|
||||
}
|
||||
|
||||
private createInitialEntities(): void {
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new PlayerController());
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
enemy.addComponent(new Health(50));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
// 场景启动时的逻辑
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景已卸载");
|
||||
// 场景卸载时的清理逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景配置
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景生命周期
|
||||
|
||||
场景提供了完整的生命周期管理:
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 场景初始化:设置系统和初始实体
|
||||
console.log("场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 场景开始运行:游戏逻辑开始执行
|
||||
console.log("场景开始运行");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 场景卸载:清理资源
|
||||
console.log("场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景(由框架自动管理生命周期)
|
||||
const scene = new ExampleScene();
|
||||
// 场景的 initialize(), begin(), update(), end() 由框架自动调用
|
||||
```
|
||||
|
||||
**生命周期方法**:
|
||||
|
||||
1. `initialize()` - 场景初始化,设置系统和初始实体
|
||||
2. `begin()` / `onStart()` - 场景开始运行
|
||||
3. `update()` - 每帧更新(由场景管理器调用)
|
||||
4. `end()` / `unload()` - 场景卸载,清理资源
|
||||
|
||||
## 实体管理
|
||||
|
||||
### 创建实体
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// 创建单个实体
|
||||
const player = this.createEntity("Player");
|
||||
|
||||
// 批量创建实体(高性能)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// 为批量创建的实体添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查找实体
|
||||
|
||||
```typescript
|
||||
class SearchScene extends Scene {
|
||||
findEntities(): void {
|
||||
// 按名称查找
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按 ID 查找
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
// 按标签查找
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
if (player) {
|
||||
console.log(`找到玩家: ${player.name}`);
|
||||
}
|
||||
|
||||
console.log(`找到 ${enemies.length} 个敌人`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
class DestroyScene extends Scene {
|
||||
cleanupEntities(): void {
|
||||
// 销毁所有实体
|
||||
this.destroyAllEntities();
|
||||
|
||||
// 单个实体的销毁通过实体本身
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // 实体会自动从场景中移除
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 添加和移除系统
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// 设置系统更新顺序
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// 添加更多系统
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
|
||||
public removeUnnecessarySystems(): void {
|
||||
// 获取系统
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
// 移除系统
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 系统访问
|
||||
|
||||
```typescript
|
||||
class SystemAccessScene extends Scene {
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // 获取所有系统
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统
|
||||
|
||||
场景内置了类型安全的事件系统:
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听事件
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡事件');
|
||||
// 处理玩家死亡
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('敌人生成事件');
|
||||
// 处理敌人生成
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成事件');
|
||||
// 处理关卡完成
|
||||
}
|
||||
|
||||
public triggerGameEvent(): void {
|
||||
// 发送事件(同步)
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 发送事件(异步)
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统 API
|
||||
|
||||
```typescript
|
||||
// 监听事件
|
||||
this.eventSystem.on('event_name', callback);
|
||||
|
||||
// 监听一次(自动取消订阅)
|
||||
this.eventSystem.once('event_name', callback);
|
||||
|
||||
// 取消监听
|
||||
this.eventSystem.off('event_name', callback);
|
||||
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('event_name', data);
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('event_name', data);
|
||||
|
||||
// 清除所有事件监听
|
||||
this.eventSystem.clear();
|
||||
```
|
||||
|
||||
## 场景统计和调试
|
||||
|
||||
### 获取场景统计
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
console.log(`系统数量: ${stats.processorCount}`);
|
||||
console.log('组件存储统计:', stats.componentStorageStats);
|
||||
}
|
||||
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('场景调试信息:', debugInfo);
|
||||
|
||||
// 显示所有实体信息
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name}(${entity.id}): ${entity.componentCount} 个组件`);
|
||||
console.log('组件类型:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// 显示所有系统信息
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`系统 ${processor.name}: 处理 ${processor.entityCount} 个实体`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件查询
|
||||
|
||||
Scene 提供了强大的组件查询系统:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建一些实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
if (i % 2 === 0) {
|
||||
entity.addComponent(new Renderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
|
||||
// 使用 ECS 流式 API(如果通过 SceneManager)
|
||||
// const api = sceneManager.api;
|
||||
// const entities = api?.find(Transform, Velocity);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
Scene 内置了性能监控功能:
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 场景职责分离
|
||||
|
||||
```typescript
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理的系统组织
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 按功能和依赖关系添加系统
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 资源管理
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
// 加载场景所需资源
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any {
|
||||
// 加载纹理
|
||||
return null;
|
||||
}
|
||||
|
||||
private loadSound(path: string): any {
|
||||
// 加载音效
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 事件处理规范
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 集中管理事件监听
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// 暂停游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// 恢复游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void {
|
||||
// 创建实体
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
// 设置事件监听
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
// 方式1:通过 SceneManager(推荐)
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 方式2:通过 WorldManager(高级用例)
|
||||
import { WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
const world = worldManager.createWorld('game');
|
||||
world.createScene('main', new GameScene());
|
||||
world.setSceneActive('main', true);
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [SceneManager](./scene-manager/) - 适用于大多数游戏的简单场景管理
|
||||
- 了解 [WorldManager](./world-manager/) - 适用于需要多世界隔离的高级场景
|
||||
- 了解 [持久化实体](./persistent-entity/) - 让实体跨场景保持状态(v2.3.0+)
|
||||
|
||||
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。
|
||||
179
docs/src/content/docs/guide/scene/best-practices.md
Normal file
179
docs/src/content/docs/guide/scene/best-practices.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "最佳实践"
|
||||
description: "场景设计模式和完整示例"
|
||||
---
|
||||
|
||||
## 场景职责分离
|
||||
|
||||
```typescript
|
||||
// 好的场景设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class InventoryScene extends Scene {
|
||||
// 只处理物品栏逻辑
|
||||
}
|
||||
|
||||
// 避免的场景设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、物品栏等所有逻辑 ❌
|
||||
}
|
||||
```
|
||||
|
||||
## 资源管理
|
||||
|
||||
```typescript
|
||||
class ResourceScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', this.loadTexture('player.png'));
|
||||
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
|
||||
private loadTexture(path: string): any { return null; }
|
||||
private loadSound(path: string): any { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
## 初始化顺序
|
||||
|
||||
```typescript
|
||||
class ProperInitScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 首先设置场景配置
|
||||
this.name = "GameScene";
|
||||
|
||||
// 2. 然后添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 3. 最后创建实体
|
||||
this.createEntities();
|
||||
|
||||
// 4. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
private createEntities(): void { /* ... */ }
|
||||
private setupEvents(): void { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class Transform {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
class Velocity {
|
||||
constructor(public vx: number, public vy: number) {}
|
||||
}
|
||||
|
||||
class Health {
|
||||
constructor(public value: number) {}
|
||||
}
|
||||
|
||||
// 定义系统
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (transform && velocity) {
|
||||
transform.x += velocity.vx;
|
||||
transform.y += velocity.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
|
||||
// 创建玩家
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Velocity(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 创建敌人
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Transform(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new Velocity(
|
||||
Math.random() * 100 - 50,
|
||||
Math.random() * 100 - 50
|
||||
));
|
||||
enemy.addComponent(new Health(50));
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡!');
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用场景
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| 单一职责 | 每个场景只负责一个游戏状态 |
|
||||
| 资源清理 | 在 `unload()` 中清理所有资源 |
|
||||
| 系统顺序 | 按输入→逻辑→渲染顺序添加系统 |
|
||||
| 事件解耦 | 使用事件系统进行场景内通信 |
|
||||
| 初始化分层 | 配置→系统→实体→事件的初始化顺序 |
|
||||
124
docs/src/content/docs/guide/scene/debugging.md
Normal file
124
docs/src/content/docs/guide/scene/debugging.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: "调试与监控"
|
||||
description: "场景统计、性能监控和调试信息"
|
||||
---
|
||||
|
||||
Scene 内置了完整的调试和性能监控功能。
|
||||
|
||||
## 获取场景统计
|
||||
|
||||
```typescript
|
||||
class StatsScene extends Scene {
|
||||
public showStats(): void {
|
||||
const stats = this.getStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
console.log(`系统数量: ${stats.processorCount}`);
|
||||
console.log('组件存储统计:', stats.componentStorageStats);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试信息
|
||||
|
||||
```typescript
|
||||
public showDebugInfo(): void {
|
||||
const debugInfo = this.getDebugInfo();
|
||||
console.log('场景调试信息:', debugInfo);
|
||||
|
||||
// 显示所有实体信息
|
||||
debugInfo.entities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name}(${entity.id}): ${entity.componentCount} 个组件`);
|
||||
console.log('组件类型:', entity.componentTypes);
|
||||
});
|
||||
|
||||
// 显示所有系统信息
|
||||
debugInfo.processors.forEach(processor => {
|
||||
console.log(`系统 ${processor.name}: 处理 ${processor.entityCount} 个实体`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
```typescript
|
||||
class PerformanceScene extends Scene {
|
||||
public showPerformance(): void {
|
||||
// 获取性能数据
|
||||
const perfData = this.performanceMonitor?.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log('FPS:', perfData.fps);
|
||||
console.log('帧时间:', perfData.frameTime);
|
||||
console.log('实体更新时间:', perfData.entityUpdateTime);
|
||||
console.log('系统更新时间:', perfData.systemUpdateTime);
|
||||
}
|
||||
|
||||
// 获取性能报告
|
||||
const report = this.performanceMonitor?.generateReport();
|
||||
if (report) {
|
||||
console.log('性能报告:', report);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### getStats()
|
||||
|
||||
返回场景统计信息:
|
||||
|
||||
```typescript
|
||||
interface SceneStats {
|
||||
entityCount: number;
|
||||
processorCount: number;
|
||||
componentStorageStats: ComponentStorageStats;
|
||||
}
|
||||
```
|
||||
|
||||
### getDebugInfo()
|
||||
|
||||
返回详细调试信息:
|
||||
|
||||
```typescript
|
||||
interface DebugInfo {
|
||||
entities: EntityDebugInfo[];
|
||||
processors: ProcessorDebugInfo[];
|
||||
}
|
||||
|
||||
interface EntityDebugInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
componentCount: number;
|
||||
componentTypes: string[];
|
||||
}
|
||||
|
||||
interface ProcessorDebugInfo {
|
||||
name: string;
|
||||
entityCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
### performanceMonitor
|
||||
|
||||
性能监控器接口:
|
||||
|
||||
```typescript
|
||||
interface PerformanceMonitor {
|
||||
getPerformanceData(): PerformanceData;
|
||||
generateReport(): string;
|
||||
}
|
||||
|
||||
interface PerformanceData {
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
entityUpdateTime: number;
|
||||
systemUpdateTime: number;
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
1. **开发模式** - 在 `Core.create({ debug: true })` 启用调试模式
|
||||
2. **性能分析** - 定期调用 `getStats()` 监控实体和系统数量
|
||||
3. **内存监控** - 检查 `componentStorageStats` 发现内存问题
|
||||
4. **系统性能** - 使用 `performanceMonitor` 识别慢系统
|
||||
125
docs/src/content/docs/guide/scene/entity-management.md
Normal file
125
docs/src/content/docs/guide/scene/entity-management.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: "实体管理"
|
||||
description: "场景中的实体创建、查找和销毁"
|
||||
---
|
||||
|
||||
场景提供了完整的实体管理 API,包括创建、查找和销毁实体。
|
||||
|
||||
## 创建实体
|
||||
|
||||
### 单个实体
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createGameEntities(): void {
|
||||
// 创建命名实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 批量创建
|
||||
|
||||
```typescript
|
||||
class EntityScene extends Scene {
|
||||
createBullets(): void {
|
||||
// 批量创建实体(高性能)
|
||||
const bullets = this.createEntities(100, "Bullet");
|
||||
|
||||
// 为批量创建的实体添加组件
|
||||
bullets.forEach((bullet, index) => {
|
||||
bullet.addComponent(new Position(index * 10, 100));
|
||||
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 查找实体
|
||||
|
||||
### 按名称查找
|
||||
|
||||
```typescript
|
||||
// 按名称查找(返回第一个匹配)
|
||||
const player = this.findEntity("Player");
|
||||
const player2 = this.getEntityByName("Player"); // 别名方法
|
||||
|
||||
if (player) {
|
||||
console.log(`找到玩家: ${player.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 按 ID 查找
|
||||
|
||||
```typescript
|
||||
// 按唯一 ID 查找
|
||||
const entity = this.findEntityById(123);
|
||||
|
||||
if (entity) {
|
||||
console.log(`找到实体: ${entity.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 按标签查找
|
||||
|
||||
```typescript
|
||||
// 按标签查找(返回数组)
|
||||
const enemies = this.findEntitiesByTag(2);
|
||||
const enemies2 = this.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
console.log(`找到 ${enemies.length} 个敌人`);
|
||||
```
|
||||
|
||||
## 销毁实体
|
||||
|
||||
### 销毁单个实体
|
||||
|
||||
```typescript
|
||||
const enemy = this.findEntity("Enemy_1");
|
||||
if (enemy) {
|
||||
enemy.destroy(); // 实体会自动从场景中移除
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁所有实体
|
||||
|
||||
```typescript
|
||||
// 销毁场景中所有实体
|
||||
this.destroyAllEntities();
|
||||
```
|
||||
|
||||
## 实体查询
|
||||
|
||||
Scene 提供了组件查询系统:
|
||||
|
||||
```typescript
|
||||
class QueryScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建测试实体
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const entity = this.createEntity(`Entity_${i}`);
|
||||
entity.addComponent(new Transform(i * 10, 0));
|
||||
entity.addComponent(new Velocity(1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
public queryEntities(): void {
|
||||
// 通过 QuerySystem 查询
|
||||
const entities = this.querySystem.query([Transform, Velocity]);
|
||||
console.log(`找到 ${entities.length} 个有 Transform 和 Velocity 的实体`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
| 方法 | 返回值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `createEntity(name)` | `Entity` | 创建单个实体 |
|
||||
| `createEntities(count, prefix)` | `Entity[]` | 批量创建实体 |
|
||||
| `findEntity(name)` | `Entity \| undefined` | 按名称查找 |
|
||||
| `findEntityById(id)` | `Entity \| undefined` | 按 ID 查找 |
|
||||
| `findEntitiesByTag(tag)` | `Entity[]` | 按标签查找 |
|
||||
| `destroyAllEntities()` | `void` | 销毁所有实体 |
|
||||
122
docs/src/content/docs/guide/scene/events.md
Normal file
122
docs/src/content/docs/guide/scene/events.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: "事件系统"
|
||||
description: "场景内置的类型安全事件系统"
|
||||
---
|
||||
|
||||
场景内置了类型安全的事件系统,用于场景内的解耦通信。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 监听事件
|
||||
|
||||
```typescript
|
||||
class EventScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听事件
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('玩家死亡事件');
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: any): void {
|
||||
console.log('敌人生成事件');
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('关卡完成事件');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 发送事件
|
||||
|
||||
```typescript
|
||||
public triggerGameEvent(): void {
|
||||
// 同步发送事件
|
||||
this.eventSystem.emitSync('custom_event', {
|
||||
message: "这是自定义事件",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 异步发送事件
|
||||
this.eventSystem.emit('async_event', {
|
||||
data: "异步事件数据"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `on(event, callback)` | 监听事件 |
|
||||
| `once(event, callback)` | 监听一次(自动取消订阅) |
|
||||
| `off(event, callback)` | 取消监听 |
|
||||
| `emitSync(event, data)` | 同步发送事件 |
|
||||
| `emit(event, data)` | 异步发送事件 |
|
||||
| `clear()` | 清除所有事件监听 |
|
||||
|
||||
## 事件处理规范
|
||||
|
||||
### 集中管理事件监听
|
||||
|
||||
```typescript
|
||||
class EventHandlingScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.eventSystem.on('game_pause', this.onGamePause.bind(this));
|
||||
this.eventSystem.on('game_resume', this.onGameResume.bind(this));
|
||||
this.eventSystem.on('player_input', this.onPlayerInput.bind(this));
|
||||
}
|
||||
|
||||
private onGamePause(): void {
|
||||
// 暂停游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onGameResume(): void {
|
||||
// 恢复游戏逻辑
|
||||
this.systems.forEach(system => {
|
||||
if (system instanceof GameLogicSystem) {
|
||||
system.enabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPlayerInput(data: any): void {
|
||||
// 处理玩家输入
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 清理事件监听
|
||||
|
||||
在场景卸载时清理事件监听,避免内存泄漏:
|
||||
|
||||
```typescript
|
||||
public unload(): void {
|
||||
// 清理所有事件监听
|
||||
this.eventSystem.clear();
|
||||
}
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
| 场景 | 示例事件 |
|
||||
|------|----------|
|
||||
| 游戏状态 | `game_start`, `game_pause`, `game_over` |
|
||||
| 玩家行为 | `player_died`, `player_jump`, `player_attack` |
|
||||
| 敌人行为 | `enemy_spawned`, `enemy_killed` |
|
||||
| 关卡进度 | `level_start`, `level_complete` |
|
||||
| UI 交互 | `button_click`, `menu_open` |
|
||||
91
docs/src/content/docs/guide/scene/index.md
Normal file
91
docs/src/content/docs/guide/scene/index.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: "场景"
|
||||
description: "ECS框架的核心容器,管理实体、系统和组件的生命周期"
|
||||
---
|
||||
|
||||
在 ECS 架构中,场景(Scene)是游戏世界的容器,负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
|
||||
|
||||
## 核心功能
|
||||
|
||||
场景是 ECS 框架的核心容器,提供:
|
||||
- 实体的创建、管理和销毁
|
||||
- 系统的注册和执行调度
|
||||
- 组件的存储和查询
|
||||
- 事件系统支持
|
||||
- 性能监控和调试信息
|
||||
|
||||
## 场景管理方式
|
||||
|
||||
ECS Framework 提供了两种场景管理方式:
|
||||
|
||||
| 管理器 | 适用场景 | 特点 |
|
||||
|--------|----------|------|
|
||||
| **SceneManager** | 95% 的游戏应用 | 轻量级,支持场景切换 |
|
||||
| **WorldManager** | MMO 服务器、房间系统 | 多 World 管理,完全隔离 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 继承 Scene 类
|
||||
|
||||
```typescript
|
||||
import { Scene, EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 创建初始实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景已卸载");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景配置
|
||||
|
||||
```typescript
|
||||
import { ISceneConfig } from '@esengine/ecs-framework';
|
||||
|
||||
const config: ISceneConfig = {
|
||||
name: "MainGame",
|
||||
enableEntityDirectUpdate: false
|
||||
};
|
||||
|
||||
class ConfiguredScene extends Scene {
|
||||
constructor() {
|
||||
super(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 运行场景
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager } from '@esengine/ecs-framework';
|
||||
|
||||
Core.create({ debug: true });
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [生命周期](./lifecycle/) - 场景生命周期方法
|
||||
- [实体管理](./entity-management/) - 创建、查找、销毁实体
|
||||
- [系统管理](./system-management/) - 系统添加与控制
|
||||
- [事件系统](./events/) - 场景内事件通信
|
||||
- [调试与监控](./debugging/) - 性能分析和调试
|
||||
- [最佳实践](./best-practices/) - 场景设计模式
|
||||
103
docs/src/content/docs/guide/scene/lifecycle.md
Normal file
103
docs/src/content/docs/guide/scene/lifecycle.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: "场景生命周期"
|
||||
description: "场景的生命周期方法和执行顺序"
|
||||
---
|
||||
|
||||
场景提供了完整的生命周期管理,确保资源正确初始化和清理。
|
||||
|
||||
## 生命周期方法
|
||||
|
||||
```typescript
|
||||
class ExampleScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 1. 场景初始化:设置系统和初始实体
|
||||
console.log("场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 2. 场景开始运行:游戏逻辑开始执行
|
||||
console.log("场景开始运行");
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 3. 每帧更新(由场景管理器调用)
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 4. 场景卸载:清理资源
|
||||
console.log("场景卸载");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 执行顺序
|
||||
|
||||
| 阶段 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| 初始化 | `initialize()` | 设置系统和初始实体 |
|
||||
| 开始 | `begin()` / `onStart()` | 场景开始运行 |
|
||||
| 更新 | `update()` | 每帧更新(框架自动调用) |
|
||||
| 结束 | `end()` / `unload()` | 场景卸载,清理资源 |
|
||||
|
||||
## 生命周期示例
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private resourcesLoaded = false;
|
||||
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 1. 添加系统(按依赖顺序)
|
||||
this.addSystem(new InputSystem());
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// 2. 创建初始实体
|
||||
this.createPlayer();
|
||||
this.createEnemies();
|
||||
|
||||
// 3. 设置事件监听
|
||||
this.setupEvents();
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
this.resourcesLoaded = true;
|
||||
console.log("场景资源加载完成,游戏开始");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理事件监听
|
||||
this.eventSystem.clear();
|
||||
|
||||
// 清理其他资源
|
||||
this.resourcesLoaded = false;
|
||||
console.log("场景资源已清理");
|
||||
}
|
||||
|
||||
private createPlayer(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Position(400, 300));
|
||||
}
|
||||
|
||||
private createEnemies(): void {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const enemy = this.createEntity(`Enemy_${i}`);
|
||||
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
|
||||
}
|
||||
}
|
||||
|
||||
private setupEvents(): void {
|
||||
this.eventSystem.on('player_died', () => {
|
||||
console.log('玩家死亡');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **initialize() 只调用一次** - 用于设置初始状态
|
||||
2. **onStart() 在场景激活时调用** - 可能多次调用(如场景切换)
|
||||
3. **unload() 必须清理资源** - 避免内存泄漏
|
||||
4. **update() 由框架管理** - 不需要手动调用
|
||||
115
docs/src/content/docs/guide/scene/system-management.md
Normal file
115
docs/src/content/docs/guide/scene/system-management.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: "系统管理"
|
||||
description: "场景中的系统添加、移除和控制"
|
||||
---
|
||||
|
||||
场景负责管理系统的注册、执行顺序和生命周期。
|
||||
|
||||
## 添加系统
|
||||
|
||||
```typescript
|
||||
class SystemScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 添加系统
|
||||
const movementSystem = new MovementSystem();
|
||||
this.addSystem(movementSystem);
|
||||
|
||||
// 设置系统更新顺序(数值越小越先执行)
|
||||
movementSystem.updateOrder = 1;
|
||||
|
||||
// 添加更多系统
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取系统
|
||||
|
||||
```typescript
|
||||
// 获取指定类型的系统
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
console.log("找到物理系统");
|
||||
}
|
||||
```
|
||||
|
||||
## 移除系统
|
||||
|
||||
```typescript
|
||||
public removeUnnecessarySystems(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
|
||||
if (physicsSystem) {
|
||||
this.removeSystem(physicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 控制系统
|
||||
|
||||
### 启用/禁用系统
|
||||
|
||||
```typescript
|
||||
public pausePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = false; // 禁用系统
|
||||
}
|
||||
}
|
||||
|
||||
public resumePhysics(): void {
|
||||
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
|
||||
if (physicsSystem) {
|
||||
physicsSystem.enabled = true; // 启用系统
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取所有系统
|
||||
|
||||
```typescript
|
||||
public getAllSystems(): EntitySystem[] {
|
||||
return this.systems; // 获取所有已注册系统
|
||||
}
|
||||
```
|
||||
|
||||
## 系统组织最佳实践
|
||||
|
||||
按功能分组添加系统:
|
||||
|
||||
```typescript
|
||||
class OrganizedScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 按功能和依赖关系添加系统
|
||||
this.addInputSystems();
|
||||
this.addLogicSystems();
|
||||
this.addRenderSystems();
|
||||
}
|
||||
|
||||
private addInputSystems(): void {
|
||||
this.addSystem(new InputSystem());
|
||||
}
|
||||
|
||||
private addLogicSystems(): void {
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new PhysicsSystem());
|
||||
this.addSystem(new CollisionSystem());
|
||||
}
|
||||
|
||||
private addRenderSystems(): void {
|
||||
this.addSystem(new RenderSystem());
|
||||
this.addSystem(new UISystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
| 方法 | 返回值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `addSystem(system)` | `void` | 添加系统到场景 |
|
||||
| `removeSystem(system)` | `void` | 从场景移除系统 |
|
||||
| `getEntityProcessor(Type)` | `T \| undefined` | 获取指定类型的系统 |
|
||||
| `systems` | `EntitySystem[]` | 获取所有系统 |
|
||||
@@ -1,774 +0,0 @@
|
||||
---
|
||||
title: "Worker系统"
|
||||
---
|
||||
|
||||
Worker系统(WorkerEntitySystem)是ECS框架中基于Web Worker的多线程处理系统,专为计算密集型任务设计,能够充分利用多核CPU性能,实现真正的并行计算。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **真正的并行计算**:利用Web Worker在后台线程执行计算密集型任务
|
||||
- **自动负载均衡**:根据CPU核心数自动分配工作负载
|
||||
- **SharedArrayBuffer优化**:零拷贝数据共享,提升大规模计算性能
|
||||
- **降级支持**:不支持Worker时自动回退到主线程处理
|
||||
- **类型安全**:完整的TypeScript支持和类型检查
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 简单的物理系统示例
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true, // 启用Worker并行处理
|
||||
workerCount: 8, // Worker数量,系统会自动限制在硬件支持范围内
|
||||
entitiesPerWorker: 100, // 每个Worker处理的实体数量
|
||||
useSharedArrayBuffer: true, // 启用SharedArrayBuffer优化
|
||||
entityDataSize: 7, // 每个实体数据大小
|
||||
maxEntities: 10000, // 最大实体数量
|
||||
systemConfig: { // 传递给Worker的配置
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 数据提取:将Entity转换为可序列化的数据
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass,
|
||||
radius: physics.radius
|
||||
};
|
||||
}
|
||||
|
||||
// Worker处理函数:纯函数,在Worker中执行
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
// 应用重力
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// 应用摩擦力
|
||||
entity.vx *= config.friction;
|
||||
entity.vy *= config.friction;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// 结果应用:将Worker处理结果应用回Entity
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
position.x = result.x;
|
||||
position.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer优化支持
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 7; // id, x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = entityData.id;
|
||||
this.sharedFloatArray[offset + 1] = entityData.x;
|
||||
this.sharedFloatArray[offset + 2] = entityData.y;
|
||||
this.sharedFloatArray[offset + 3] = entityData.vx;
|
||||
this.sharedFloatArray[offset + 4] = entityData.vy;
|
||||
this.sharedFloatArray[offset + 5] = entityData.mass;
|
||||
this.sharedFloatArray[offset + 6] = entityData.radius;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
Worker系统支持丰富的配置选项:
|
||||
|
||||
```typescript
|
||||
interface WorkerSystemConfig {
|
||||
/** 是否启用Worker并行处理 */
|
||||
enableWorker?: boolean;
|
||||
/** Worker数量,默认为CPU核心数,自动限制在系统最大值内 */
|
||||
workerCount?: number;
|
||||
/** 每个Worker处理的实体数量,用于控制负载分布 */
|
||||
entitiesPerWorker?: number;
|
||||
/** 系统配置数据,会传递给Worker */
|
||||
systemConfig?: any;
|
||||
/** 是否使用SharedArrayBuffer优化 */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
/** 每个实体在SharedArrayBuffer中占用的Float32数量 */
|
||||
entityDataSize?: number;
|
||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||
maxEntities?: number;
|
||||
/** 预编译的Worker脚本路径(用于微信小游戏等不支持动态脚本的平台) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置建议
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 根据任务复杂度决定是否启用
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
|
||||
// Worker数量:系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker,实际数量受CPU核心数限制
|
||||
|
||||
// 每个Worker处理的实体数量(可选)
|
||||
entitiesPerWorker: 200, // 精确控制负载分布
|
||||
|
||||
// 大量简单计算时启用SharedArrayBuffer
|
||||
useSharedArrayBuffer: this.entityCount > 1000,
|
||||
|
||||
// 根据实际数据结构设置
|
||||
entityDataSize: 8, // 确保与数据结构匹配
|
||||
|
||||
// 预估最大实体数量
|
||||
maxEntities: 10000,
|
||||
|
||||
// 传递给Worker的全局配置
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95,
|
||||
worldBounds: { width: 1920, height: 1080 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和计算复杂度决定
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`Worker数量: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
console.log(`当前模式: ${info.currentMode}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 处理模式
|
||||
|
||||
Worker系统支持两种处理模式:
|
||||
|
||||
### 1. 传统Worker模式
|
||||
|
||||
数据通过序列化在主线程和Worker间传递:
|
||||
|
||||
```typescript
|
||||
// 适用于:复杂计算逻辑,实体数量适中
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false, // 使用传统模式
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
|
||||
// 复杂的算法逻辑
|
||||
return entities.map(entity => {
|
||||
// AI决策、路径规划等复杂计算
|
||||
return this.complexAILogic(entity, deltaTime);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SharedArrayBuffer模式
|
||||
|
||||
零拷贝数据共享,适合大量简单计算:
|
||||
|
||||
```typescript
|
||||
// 适用于:大量实体的简单计算
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true, // 启用共享内存
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
|
||||
// 读取数据
|
||||
let x = sharedFloatArray[offset];
|
||||
let y = sharedFloatArray[offset + 1];
|
||||
let vx = sharedFloatArray[offset + 2];
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
|
||||
// 物理计算
|
||||
vy += config.gravity * deltaTime;
|
||||
x += vx * deltaTime;
|
||||
y += vy * deltaTime;
|
||||
|
||||
// 写回数据
|
||||
sharedFloatArray[offset] = x;
|
||||
sharedFloatArray[offset + 1] = y;
|
||||
sharedFloatArray[offset + 2] = vx;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例:粒子物理系统
|
||||
|
||||
一个包含碰撞检测的完整粒子物理系统:
|
||||
|
||||
```typescript
|
||||
interface ParticleData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('ParticlePhysics')
|
||||
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics, Renderable), {
|
||||
enableWorker: true,
|
||||
workerCount: 6, // 请求6个Worker,自动限制在CPU核心数内
|
||||
entitiesPerWorker: 150, // 每个Worker处理150个粒子
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 9,
|
||||
maxEntities: 5000,
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): ParticleData {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
const renderable = entity.getComponent(Renderable);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
dx: velocity.dx,
|
||||
dy: velocity.dy,
|
||||
mass: physics.mass,
|
||||
radius: renderable.size,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: ParticleData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): ParticleData[] {
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 基础物理更新
|
||||
for (const particle of result) {
|
||||
// 应用重力
|
||||
particle.dy += config.gravity * deltaTime;
|
||||
|
||||
// 更新位置
|
||||
particle.x += particle.dx * deltaTime;
|
||||
particle.y += particle.dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (particle.x <= particle.radius) {
|
||||
particle.x = particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
} else if (particle.x >= config.canvasWidth - particle.radius) {
|
||||
particle.x = config.canvasWidth - particle.radius;
|
||||
particle.dx = -particle.dx * particle.bounce;
|
||||
}
|
||||
|
||||
if (particle.y <= particle.radius) {
|
||||
particle.y = particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
} else if (particle.y >= config.canvasHeight - particle.radius) {
|
||||
particle.y = config.canvasHeight - particle.radius;
|
||||
particle.dy = -particle.dy * particle.bounce;
|
||||
particle.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
// 空气阻力
|
||||
particle.dx *= particle.friction;
|
||||
particle.dy *= particle.friction;
|
||||
}
|
||||
|
||||
// 粒子间碰撞检测(O(n²)算法)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const p1 = result[i];
|
||||
const p2 = result[j];
|
||||
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDistance = p1.radius + p2.radius;
|
||||
|
||||
if (distance < minDistance && distance > 0) {
|
||||
// 分离粒子
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
const overlap = minDistance - distance;
|
||||
|
||||
p1.x -= nx * overlap * 0.5;
|
||||
p1.y -= ny * overlap * 0.5;
|
||||
p2.x += nx * overlap * 0.5;
|
||||
p2.y += ny * overlap * 0.5;
|
||||
|
||||
// 弹性碰撞
|
||||
const relativeVelocityX = p2.dx - p1.dx;
|
||||
const relativeVelocityY = p2.dy - p1.dy;
|
||||
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
|
||||
|
||||
if (velocityAlongNormal > 0) continue;
|
||||
|
||||
const restitution = (p1.bounce + p2.bounce) * 0.5;
|
||||
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/p1.mass + 1/p2.mass);
|
||||
|
||||
p1.dx -= impulseScalar * nx / p1.mass;
|
||||
p1.dy -= impulseScalar * ny / p1.mass;
|
||||
p2.dx += impulseScalar * nx / p2.mass;
|
||||
p2.dy += impulseScalar * ny / p2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: ParticleData): void {
|
||||
if (!entity?.enabled) return;
|
||||
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.set(result.x, result.y);
|
||||
velocity.set(result.dx, result.dy);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.dx;
|
||||
this.sharedFloatArray[offset + 4] = data.dy;
|
||||
this.sharedFloatArray[offset + 5] = data.mass;
|
||||
this.sharedFloatArray[offset + 6] = data.radius;
|
||||
this.sharedFloatArray[offset + 7] = data.bounce;
|
||||
this.sharedFloatArray[offset + 8] = data.friction;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): ParticleData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
dx: this.sharedFloatArray[offset + 3],
|
||||
dy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6],
|
||||
bounce: this.sharedFloatArray[offset + 7],
|
||||
friction: this.sharedFloatArray[offset + 8]
|
||||
};
|
||||
}
|
||||
|
||||
// 性能监控
|
||||
public getPerformanceInfo(): {
|
||||
enabled: boolean;
|
||||
workerCount: number;
|
||||
entitiesPerWorker?: number;
|
||||
maxSystemWorkerCount: number;
|
||||
entityCount: number;
|
||||
isProcessing: boolean;
|
||||
currentMode: string;
|
||||
} {
|
||||
const workerInfo = this.getWorkerInfo();
|
||||
return {
|
||||
...workerInfo,
|
||||
entityCount: this.entities.length
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
Worker系统特别适合以下场景:
|
||||
|
||||
### 1. 物理模拟
|
||||
- **重力系统**:大量实体的重力计算
|
||||
- **碰撞检测**:复杂的碰撞算法
|
||||
- **流体模拟**:粒子流体系统
|
||||
- **布料模拟**:顶点物理计算
|
||||
|
||||
### 2. AI计算
|
||||
- **路径寻找**:A*、Dijkstra等算法
|
||||
- **行为树**:复杂的AI决策逻辑
|
||||
- **群体智能**:鸟群、鱼群算法
|
||||
- **神经网络**:简单的AI推理
|
||||
|
||||
### 3. 数据处理
|
||||
- **大量实体更新**:状态机、生命周期管理
|
||||
- **统计计算**:游戏数据分析
|
||||
- **图像处理**:纹理生成、效果计算
|
||||
- **音频处理**:音效合成、频谱分析
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. Worker函数要求
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:Worker处理函数是纯函数
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||
// 只使用参数和标准JavaScript API
|
||||
return entities.map(entity => {
|
||||
// 纯计算逻辑,不依赖外部状态
|
||||
entity.y += entity.velocity * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 避免:在Worker函数中使用外部引用
|
||||
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
|
||||
// this 和外部变量在Worker中不可用
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // ❌ 错误
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据设计
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:合理的数据设计
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
// 保持数据结构简单,便于序列化
|
||||
}
|
||||
|
||||
// ❌ 避免:复杂的嵌套对象
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// 复杂嵌套结构增加序列化开销
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Worker数量控制
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:灵活的Worker配置
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
// 直接指定需要的Worker数量,系统会自动限制在硬件支持范围内
|
||||
workerCount: 8, // 请求8个Worker
|
||||
entitiesPerWorker: 100, // 每个Worker处理100个实体
|
||||
enableWorker: this.shouldUseWorker(), // 条件启用
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
// 根据实体数量和复杂度决定是否使用Worker
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取实际使用的Worker信息
|
||||
checkWorkerConfiguration() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`请求Worker数量: 8`);
|
||||
console.log(`实际Worker数量: ${info.workerCount}`);
|
||||
console.log(`系统最大支持: ${info.maxSystemWorkerCount}`);
|
||||
console.log(`每Worker实体数: ${info.entitiesPerWorker || '自动分配'}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能监控
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:性能监控
|
||||
public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
||||
return {
|
||||
...this.getWorkerInfo(),
|
||||
entityCount: this.entities.length,
|
||||
averageProcessTime: this.getAverageProcessTime(),
|
||||
workerUtilization: this.getWorkerUtilization()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 计算密集度评估
|
||||
只对计算密集型任务使用Worker,避免在简单计算上增加线程开销。
|
||||
|
||||
### 2. 数据传输优化
|
||||
- 使用SharedArrayBuffer减少序列化开销
|
||||
- 保持数据结构简单和扁平
|
||||
- 避免频繁的大数据传输
|
||||
|
||||
### 3. 降级策略
|
||||
始终提供主线程回退方案,确保在不支持Worker的环境中正常运行。
|
||||
|
||||
### 4. 内存管理
|
||||
及时清理Worker池和共享缓冲区,避免内存泄漏。
|
||||
|
||||
### 5. 负载均衡
|
||||
使用 `entitiesPerWorker` 参数精确控制负载分布,避免某些Worker空闲而其他Worker过载。
|
||||
|
||||
## 在线演示
|
||||
|
||||
查看完整的Worker系统演示:[Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/)
|
||||
|
||||
该演示展示了:
|
||||
- 多线程物理计算
|
||||
- 实时性能对比
|
||||
- SharedArrayBuffer优化
|
||||
- 大量实体的并行处理
|
||||
|
||||
## 微信小游戏支持
|
||||
|
||||
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。
|
||||
|
||||
### 微信小游戏 Worker 限制
|
||||
|
||||
| 特性 | 浏览器 | 微信小游戏 |
|
||||
|------|--------|-----------|
|
||||
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
|
||||
| Worker 数量 | 多个 | 最多 1 个 |
|
||||
| 脚本来源 | 任意 | 必须是代码包内文件 |
|
||||
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
|
||||
|
||||
### 使用 Worker Generator CLI
|
||||
|
||||
#### 1. 安装工具
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### 2. 配置 workerScriptPath
|
||||
|
||||
在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: PhysicsData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): PhysicsData[] {
|
||||
// 物理计算逻辑
|
||||
return entities.map(entity => {
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 生成 Worker 文件
|
||||
|
||||
运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件:
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# 完整选项
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \ # 源码目录
|
||||
--wechat \ # 生成微信小游戏兼容代码
|
||||
--mapping \ # 生成 worker-mapping.json
|
||||
--verbose # 详细输出
|
||||
```
|
||||
|
||||
CLI 工具会:
|
||||
1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类
|
||||
2. 读取每个类的 `workerScriptPath` 配置
|
||||
3. 提取 `workerProcess` 方法体
|
||||
4. 转换为 ES5 语法(微信小游戏兼容)
|
||||
5. 生成到配置的路径
|
||||
|
||||
#### 4. 配置 game.json
|
||||
|
||||
在微信小游戏的 `game.json` 中配置 workers 目录:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 项目结构
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json # 配置 "workers": "workers"
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
|
||||
└── workers/
|
||||
├── physics-worker.js # 自动生成
|
||||
└── worker-mapping.json # 自动生成
|
||||
```
|
||||
|
||||
### 临时禁用 Worker
|
||||
|
||||
如果需要临时禁用 Worker(例如调试时),有两种方式:
|
||||
|
||||
#### 方式 1:配置禁用
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false, // 禁用 Worker,使用主线程处理
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式 2:平台适配器禁用
|
||||
|
||||
在自定义平台适配器中返回不支持 Worker:
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false; // 返回 false 禁用 Worker
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件
|
||||
|
||||
2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量:
|
||||
```typescript
|
||||
// ✅ 正确:只使用参数
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * deltaTime;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 this
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(e => {
|
||||
e.y += this.gravity * deltaTime; // Worker 中无法访问 this
|
||||
return e;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **配置数据通过 `systemConfig` 传递**,而不是类属性
|
||||
|
||||
4. **开发者工具中的警告可以忽略**:
|
||||
- `getNetworkType:fail not support` - 微信开发者工具内部行为
|
||||
- `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现
|
||||
|
||||
Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||
120
docs/src/content/docs/guide/worker-system/best-practices.md
Normal file
120
docs/src/content/docs/guide/worker-system/best-practices.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: "最佳实践"
|
||||
description: "Worker 系统性能优化建议"
|
||||
---
|
||||
|
||||
## Worker 函数要求
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:纯函数,只使用参数和标准 API
|
||||
protected workerProcess(entities: PhysicsData[], dt: number, config: any): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
entity.y += entity.velocity * dt;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 避免:使用 this 或外部变量
|
||||
protected workerProcess(entities: PhysicsData[], dt: number): PhysicsData[] {
|
||||
return entities.map(entity => {
|
||||
entity.y += this.someProperty; // ❌ Worker 中无法访问 this
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 数据设计
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:简单扁平的数据结构
|
||||
interface SimplePhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
}
|
||||
|
||||
// ❌ 避免:复杂嵌套对象
|
||||
interface ComplexData {
|
||||
transform: {
|
||||
position: { x: number; y: number };
|
||||
rotation: { angle: number };
|
||||
};
|
||||
// 嵌套结构增加序列化开销
|
||||
}
|
||||
```
|
||||
|
||||
## Worker 数量控制
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
workerCount: 8, // 系统自动限制在 CPU 核心数内
|
||||
entitiesPerWorker: 100, // 精确控制负载分布
|
||||
enableWorker: this.shouldUseWorker(),
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseWorker(): boolean {
|
||||
return this.expectedEntityCount > 100;
|
||||
}
|
||||
|
||||
// 获取实际配置
|
||||
checkConfig() {
|
||||
const info = this.getWorkerInfo();
|
||||
console.log(`实际 Worker 数量: ${info.workerCount}/${info.maxSystemWorkerCount}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 计算密集度评估
|
||||
|
||||
只对计算密集型任务使用 Worker,避免简单计算的线程开销。
|
||||
|
||||
### 2. 数据传输优化
|
||||
|
||||
- 使用 SharedArrayBuffer 减少序列化开销
|
||||
- 保持数据结构简单扁平
|
||||
- 避免频繁大数据传输
|
||||
|
||||
### 3. 降级策略
|
||||
|
||||
始终提供主线程回退方案:
|
||||
|
||||
```typescript
|
||||
protected processSynchronously(entities: readonly Entity[]): void {
|
||||
// 当 Worker 不可用时执行
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 内存管理
|
||||
|
||||
及时清理 Worker 池和共享缓冲区,避免内存泄漏。
|
||||
|
||||
### 5. 负载均衡
|
||||
|
||||
使用 `entitiesPerWorker` 精确控制,避免部分 Worker 空闲。
|
||||
|
||||
## 何时使用 Worker
|
||||
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 实体数量 < 100 | 不推荐使用 Worker |
|
||||
| 100 < 实体 < 1000 | 传统 Worker 模式 |
|
||||
| 实体 > 1000 | SharedArrayBuffer 模式 |
|
||||
| 复杂 AI 逻辑 | 传统 Worker 模式 |
|
||||
| 简单物理计算 | SharedArrayBuffer 模式 |
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```typescript
|
||||
// 获取完整系统信息
|
||||
const info = this.getWorkerInfo();
|
||||
console.log({
|
||||
enabled: info.enabled,
|
||||
workerCount: info.workerCount,
|
||||
currentMode: info.currentMode,
|
||||
isProcessing: info.isProcessing
|
||||
});
|
||||
```
|
||||
130
docs/src/content/docs/guide/worker-system/configuration.md
Normal file
130
docs/src/content/docs/guide/worker-system/configuration.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: "配置选项"
|
||||
description: "Worker 系统配置和处理模式"
|
||||
---
|
||||
|
||||
## 配置接口
|
||||
|
||||
```typescript
|
||||
interface IWorkerSystemConfig {
|
||||
/** 是否启用 Worker 并行处理 */
|
||||
enableWorker?: boolean;
|
||||
|
||||
/** Worker 数量,默认为 CPU 核心数 */
|
||||
workerCount?: number;
|
||||
|
||||
/** 每个 Worker 处理的实体数量 */
|
||||
entitiesPerWorker?: number;
|
||||
|
||||
/** 系统配置数据,传递给 Worker */
|
||||
systemConfig?: unknown;
|
||||
|
||||
/** 是否使用 SharedArrayBuffer 优化 */
|
||||
useSharedArrayBuffer?: boolean;
|
||||
|
||||
/** 每个实体占用的 Float32 数量 */
|
||||
entityDataSize?: number;
|
||||
|
||||
/** 最大实体数量(预分配 SharedArrayBuffer) */
|
||||
maxEntities?: number;
|
||||
|
||||
/** 预编译 Worker 脚本路径(微信小游戏必需) */
|
||||
workerScriptPath?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
workerCount: 8, // 请求 8 个 Worker
|
||||
entitiesPerWorker: 200, // 每个 Worker 处理 200 个实体
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 8,
|
||||
maxEntities: 10000,
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 处理模式
|
||||
|
||||
### 传统 Worker 模式
|
||||
|
||||
数据通过序列化在主线程和 Worker 间传递:
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: false,
|
||||
workerCount: 2
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: EntityData[], dt: number): EntityData[] {
|
||||
return entities.map(entity => {
|
||||
// 复杂算法逻辑
|
||||
return this.complexAILogic(entity, dt);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景**:复杂计算逻辑,实体数量适中
|
||||
|
||||
### SharedArrayBuffer 模式
|
||||
|
||||
零拷贝数据共享,适合大量简单计算:
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: true,
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 6,
|
||||
maxEntities: 10000
|
||||
});
|
||||
}
|
||||
|
||||
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
|
||||
return function(sharedFloatArray, startIndex, endIndex, dt, config) {
|
||||
const entitySize = 6;
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const offset = i * entitySize;
|
||||
let vy = sharedFloatArray[offset + 3];
|
||||
vy += config.gravity * dt;
|
||||
sharedFloatArray[offset + 3] = vy;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景**:大量实体的简单计算
|
||||
|
||||
## 获取系统信息
|
||||
|
||||
```typescript
|
||||
const info = this.getWorkerInfo();
|
||||
console.log({
|
||||
enabled: info.enabled,
|
||||
workerCount: info.workerCount,
|
||||
maxSystemWorkerCount: info.maxSystemWorkerCount,
|
||||
currentMode: info.currentMode,
|
||||
sharedArrayBufferEnabled: info.sharedArrayBufferEnabled
|
||||
});
|
||||
```
|
||||
|
||||
## 动态更新配置
|
||||
|
||||
```typescript
|
||||
// 运行时更新配置
|
||||
this.updateConfig({
|
||||
workerCount: 4,
|
||||
entitiesPerWorker: 100
|
||||
});
|
||||
```
|
||||
190
docs/src/content/docs/guide/worker-system/examples.md
Normal file
190
docs/src/content/docs/guide/worker-system/examples.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
title: "完整示例"
|
||||
description: "粒子物理系统等复杂 Worker 示例"
|
||||
---
|
||||
|
||||
## 粒子物理系统
|
||||
|
||||
包含碰撞检测的完整粒子物理系统:
|
||||
|
||||
```typescript
|
||||
interface ParticleData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
bounce: number;
|
||||
friction: number;
|
||||
}
|
||||
|
||||
@ECSSystem('ParticlePhysics')
|
||||
class ParticlePhysicsWorkerSystem extends WorkerEntitySystem<ParticleData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity, Physics, Renderable), {
|
||||
enableWorker: true,
|
||||
workerCount: 6,
|
||||
entitiesPerWorker: 150,
|
||||
useSharedArrayBuffer: true,
|
||||
entityDataSize: 9,
|
||||
maxEntities: 5000,
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
canvasWidth: 800,
|
||||
canvasHeight: 600,
|
||||
groundFriction: 0.98
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): ParticleData {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
const render = entity.getComponent(Renderable);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
dx: vel.dx,
|
||||
dy: vel.dy,
|
||||
mass: physics.mass,
|
||||
radius: render.size,
|
||||
bounce: physics.bounce,
|
||||
friction: physics.friction
|
||||
};
|
||||
}
|
||||
|
||||
protected workerProcess(
|
||||
entities: ParticleData[],
|
||||
deltaTime: number,
|
||||
config: any
|
||||
): ParticleData[] {
|
||||
const result = entities.map(e => ({ ...e }));
|
||||
|
||||
// 基础物理更新
|
||||
for (const p of result) {
|
||||
p.dy += config.gravity * deltaTime;
|
||||
p.x += p.dx * deltaTime;
|
||||
p.y += p.dy * deltaTime;
|
||||
|
||||
// 边界碰撞
|
||||
if (p.x <= p.radius || p.x >= config.canvasWidth - p.radius) {
|
||||
p.dx = -p.dx * p.bounce;
|
||||
p.x = Math.max(p.radius, Math.min(config.canvasWidth - p.radius, p.x));
|
||||
}
|
||||
if (p.y <= p.radius || p.y >= config.canvasHeight - p.radius) {
|
||||
p.dy = -p.dy * p.bounce;
|
||||
p.y = Math.max(p.radius, Math.min(config.canvasHeight - p.radius, p.y));
|
||||
p.dx *= config.groundFriction;
|
||||
}
|
||||
|
||||
p.dx *= p.friction;
|
||||
p.dy *= p.friction;
|
||||
}
|
||||
|
||||
// 粒子间碰撞检测
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
for (let j = i + 1; j < result.length; j++) {
|
||||
const p1 = result[i];
|
||||
const p2 = result[j];
|
||||
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const minDist = p1.radius + p2.radius;
|
||||
|
||||
if (dist < minDist && dist > 0) {
|
||||
// 分离粒子
|
||||
const nx = dx / dist;
|
||||
const ny = dy / dist;
|
||||
const overlap = minDist - dist;
|
||||
|
||||
p1.x -= nx * overlap * 0.5;
|
||||
p1.y -= ny * overlap * 0.5;
|
||||
p2.x += nx * overlap * 0.5;
|
||||
p2.y += ny * overlap * 0.5;
|
||||
|
||||
// 弹性碰撞
|
||||
const relVx = p2.dx - p1.dx;
|
||||
const relVy = p2.dy - p1.dy;
|
||||
const velNormal = relVx * nx + relVy * ny;
|
||||
|
||||
if (velNormal > 0) continue;
|
||||
|
||||
const restitution = (p1.bounce + p2.bounce) * 0.5;
|
||||
const impulse = -(1 + restitution) * velNormal / (1/p1.mass + 1/p2.mass);
|
||||
|
||||
p1.dx -= impulse * nx / p1.mass;
|
||||
p1.dy -= impulse * ny / p1.mass;
|
||||
p2.dx += impulse * nx / p2.mass;
|
||||
p2.dy += impulse * ny / p2.mass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: ParticleData): void {
|
||||
if (!entity?.enabled) return;
|
||||
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
|
||||
if (pos && vel) {
|
||||
pos.set(result.x, result.y);
|
||||
vel.set(result.dx, result.dy);
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 9;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: ParticleData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
|
||||
this.sharedFloatArray[offset + 0] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.dx;
|
||||
this.sharedFloatArray[offset + 4] = data.dy;
|
||||
this.sharedFloatArray[offset + 5] = data.mass;
|
||||
this.sharedFloatArray[offset + 6] = data.radius;
|
||||
this.sharedFloatArray[offset + 7] = data.bounce;
|
||||
this.sharedFloatArray[offset + 8] = data.friction;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): ParticleData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
|
||||
return {
|
||||
id: this.sharedFloatArray[offset + 0],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
dx: this.sharedFloatArray[offset + 3],
|
||||
dy: this.sharedFloatArray[offset + 4],
|
||||
mass: this.sharedFloatArray[offset + 5],
|
||||
radius: this.sharedFloatArray[offset + 6],
|
||||
bounce: this.sharedFloatArray[offset + 7],
|
||||
friction: this.sharedFloatArray[offset + 8]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
```typescript
|
||||
public getPerformanceInfo() {
|
||||
const info = this.getWorkerInfo();
|
||||
return {
|
||||
...info,
|
||||
entityCount: this.entities.length
|
||||
};
|
||||
}
|
||||
```
|
||||
105
docs/src/content/docs/guide/worker-system/index.md
Normal file
105
docs/src/content/docs/guide/worker-system/index.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "Worker 系统"
|
||||
description: "基于 Web Worker 的多线程并行处理系统"
|
||||
---
|
||||
|
||||
Worker 系统(WorkerEntitySystem)是 ECS 框架中基于 Web Worker 的多线程处理系统,专为计算密集型任务设计。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **真正的并行计算**:利用 Web Worker 在后台线程执行任务
|
||||
- **自动负载均衡**:根据 CPU 核心数自动分配工作负载
|
||||
- **SharedArrayBuffer 优化**:零拷贝数据共享,提升大规模计算性能
|
||||
- **降级支持**:不支持 Worker 时自动回退到主线程
|
||||
- **类型安全**:完整的 TypeScript 支持
|
||||
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 4,
|
||||
systemConfig: { gravity: 100 }
|
||||
});
|
||||
}
|
||||
|
||||
// 必须实现的方法
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 5; // id, x, y, vx, vy
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
return { id: entity.id, x: pos.x, y: pos.y, vx: vel.x, vy: vel.y };
|
||||
}
|
||||
|
||||
protected workerProcess(entities: PhysicsData[], dt: number, config: any): PhysicsData[] {
|
||||
return entities.map(e => {
|
||||
e.vy += config.gravity * dt;
|
||||
e.x += e.vx * dt;
|
||||
e.y += e.vy * dt;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const pos = entity.getComponent(Position);
|
||||
const vel = entity.getComponent(Velocity);
|
||||
pos.x = result.x;
|
||||
pos.y = result.y;
|
||||
vel.x = result.vx;
|
||||
vel.y = result.vy;
|
||||
}
|
||||
|
||||
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {
|
||||
if (!this.sharedFloatArray) return;
|
||||
this.sharedFloatArray[offset] = data.id;
|
||||
this.sharedFloatArray[offset + 1] = data.x;
|
||||
this.sharedFloatArray[offset + 2] = data.y;
|
||||
this.sharedFloatArray[offset + 3] = data.vx;
|
||||
this.sharedFloatArray[offset + 4] = data.vy;
|
||||
}
|
||||
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null {
|
||||
if (!this.sharedFloatArray) return null;
|
||||
return {
|
||||
id: this.sharedFloatArray[offset],
|
||||
x: this.sharedFloatArray[offset + 1],
|
||||
y: this.sharedFloatArray[offset + 2],
|
||||
vx: this.sharedFloatArray[offset + 3],
|
||||
vy: this.sharedFloatArray[offset + 4]
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 适用场景
|
||||
|
||||
| 场景 | 示例 |
|
||||
|------|------|
|
||||
| **物理模拟** | 重力、碰撞检测、流体模拟 |
|
||||
| **AI 计算** | 路径寻找、行为树、群体智能 |
|
||||
| **数据处理** | 状态机、统计计算、图像处理 |
|
||||
|
||||
## 文档导航
|
||||
|
||||
- [配置选项](/guide/worker-system/configuration/) - 详细配置和处理模式
|
||||
- [完整示例](/guide/worker-system/examples/) - 粒子物理等复杂示例
|
||||
- [微信小游戏](/guide/worker-system/wechat/) - 微信小游戏 Worker 支持
|
||||
- [最佳实践](/guide/worker-system/best-practices/) - 性能优化建议
|
||||
|
||||
## 在线演示
|
||||
|
||||
[Worker 系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理计算、实时性能对比
|
||||
138
docs/src/content/docs/guide/worker-system/wechat.md
Normal file
138
docs/src/content/docs/guide/worker-system/wechat.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: "微信小游戏支持"
|
||||
description: "微信小游戏 Worker 限制和解决方案"
|
||||
---
|
||||
|
||||
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 CLI 工具来解决这个问题。
|
||||
|
||||
## 平台差异
|
||||
|
||||
| 特性 | 浏览器 | 微信小游戏 |
|
||||
|------|--------|-----------|
|
||||
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
|
||||
| Worker 数量 | 多个 | 最多 1 个 |
|
||||
| 脚本来源 | 任意 | 必须是代码包内文件 |
|
||||
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
|
||||
|
||||
## 使用 Worker Generator CLI
|
||||
|
||||
### 1. 安装工具
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
```
|
||||
|
||||
### 2. 配置 workerScriptPath
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity), {
|
||||
enableWorker: true,
|
||||
workerScriptPath: 'workers/physics-worker.js',
|
||||
systemConfig: { gravity: 100 }
|
||||
});
|
||||
}
|
||||
|
||||
protected workerProcess(entities: PhysicsData[], dt: number, config: any): PhysicsData[] {
|
||||
return entities.map(e => {
|
||||
e.vy += config.gravity * dt;
|
||||
e.x += e.vx * dt;
|
||||
e.y += e.vy * dt;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 生成 Worker 文件
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
npx esengine-worker-gen --src ./src --wechat
|
||||
|
||||
# 完整选项
|
||||
npx esengine-worker-gen \
|
||||
--src ./src \
|
||||
--wechat \
|
||||
--mapping \
|
||||
--verbose
|
||||
```
|
||||
|
||||
CLI 工具会自动:
|
||||
1. 扫描所有 `WorkerEntitySystem` 子类
|
||||
2. 提取 `workerProcess` 方法
|
||||
3. 转换为 ES5 语法
|
||||
4. 生成到配置的路径
|
||||
|
||||
### 4. 配置 game.json
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 项目结构
|
||||
|
||||
```
|
||||
your-game/
|
||||
├── game.js
|
||||
├── game.json
|
||||
├── src/
|
||||
│ └── systems/
|
||||
│ └── PhysicsSystem.ts
|
||||
└── workers/
|
||||
├── physics-worker.js # 自动生成
|
||||
└── worker-mapping.json # 自动生成
|
||||
```
|
||||
|
||||
## 临时禁用 Worker
|
||||
|
||||
### 配置禁用
|
||||
|
||||
```typescript
|
||||
constructor() {
|
||||
super(matcher, {
|
||||
enableWorker: false,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 平台适配器禁用
|
||||
|
||||
```typescript
|
||||
class MyPlatformAdapter implements IPlatformAdapter {
|
||||
isWorkerSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **每次修改 workerProcess 后需重新运行 CLI**
|
||||
|
||||
2. **Worker 函数必须是纯函数**:
|
||||
```typescript
|
||||
// ✅ 正确
|
||||
protected workerProcess(entities, dt, config) {
|
||||
return entities.map(e => {
|
||||
e.y += config.gravity * dt;
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
// ❌ 错误:使用 this
|
||||
protected workerProcess(entities, dt, config) {
|
||||
e.y += this.gravity * dt; // Worker 中无法访问 this
|
||||
}
|
||||
```
|
||||
|
||||
3. **配置数据通过 systemConfig 传递**
|
||||
|
||||
4. **开发者工具警告可忽略**:
|
||||
- `getNetworkType:fail not support`
|
||||
- `SharedArrayBuffer will require cross-origin isolation`
|
||||
133
docs/src/content/docs/modules/blueprint/composition.md
Normal file
133
docs/src/content/docs/modules/blueprint/composition.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "蓝图组合"
|
||||
description: "片段、组合器和触发器"
|
||||
---
|
||||
|
||||
## 蓝图片段
|
||||
|
||||
将可复用的逻辑封装为片段:
|
||||
|
||||
```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: [...]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 组合蓝图
|
||||
|
||||
```typescript
|
||||
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
|
||||
|
||||
// 注册片段
|
||||
FragmentRegistry.instance.register('health', healthFragment);
|
||||
FragmentRegistry.instance.register('movement', movementFragment);
|
||||
|
||||
// 创建组合器
|
||||
const composer = createComposer('PlayerBlueprint');
|
||||
|
||||
// 添加片段到槽位
|
||||
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
|
||||
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
|
||||
|
||||
// 连接槽位
|
||||
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
|
||||
|
||||
// 验证
|
||||
const validation = composer.validate();
|
||||
if (!validation.isValid) {
|
||||
console.error(validation.errors);
|
||||
}
|
||||
|
||||
// 编译成蓝图
|
||||
const blueprint = composer.compile();
|
||||
```
|
||||
|
||||
## 片段注册表
|
||||
|
||||
```typescript
|
||||
import { FragmentRegistry } from '@esengine/blueprint';
|
||||
|
||||
const registry = FragmentRegistry.instance;
|
||||
|
||||
// 注册片段
|
||||
registry.register('health', healthFragment);
|
||||
|
||||
// 获取片段
|
||||
const fragment = registry.get('health');
|
||||
|
||||
// 获取所有片段
|
||||
const allFragments = registry.getAll();
|
||||
|
||||
// 按类别获取
|
||||
const combatFragments = registry.getByCategory('combat');
|
||||
```
|
||||
|
||||
## 触发器系统
|
||||
|
||||
### 定义触发条件
|
||||
|
||||
```typescript
|
||||
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
|
||||
|
||||
const lowHealthCondition: TriggerCondition = {
|
||||
type: 'comparison',
|
||||
left: { type: 'variable', name: 'health' },
|
||||
operator: '<',
|
||||
right: { type: 'constant', value: 20 }
|
||||
};
|
||||
```
|
||||
|
||||
### 使用触发器分发器
|
||||
|
||||
```typescript
|
||||
const dispatcher = new TriggerDispatcher();
|
||||
|
||||
// 注册触发器
|
||||
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
|
||||
context.triggerEvent('OnLowHealth');
|
||||
});
|
||||
|
||||
// 每帧评估
|
||||
dispatcher.evaluate(context);
|
||||
```
|
||||
|
||||
### 复合条件
|
||||
|
||||
```typescript
|
||||
const complexCondition: TriggerCondition = {
|
||||
type: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'comparison',
|
||||
left: { type: 'variable', name: 'health' },
|
||||
operator: '<',
|
||||
right: { type: 'constant', value: 50 }
|
||||
},
|
||||
{
|
||||
type: 'comparison',
|
||||
left: { type: 'variable', name: 'inCombat' },
|
||||
operator: '==',
|
||||
right: { type: 'constant', value: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## 片段最佳实践
|
||||
|
||||
1. **单一职责** - 每个片段只做一件事
|
||||
2. **清晰接口** - 输入输出引脚命名明确
|
||||
3. **文档注释** - 为片段添加描述和使用示例
|
||||
4. **版本控制** - 更新片段时注意向后兼容
|
||||
128
docs/src/content/docs/modules/blueprint/custom-nodes.md
Normal file
128
docs/src/content/docs/modules/blueprint/custom-nodes.md
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
title: "自定义节点"
|
||||
description: "创建自定义蓝图节点"
|
||||
---
|
||||
|
||||
## 定义节点模板
|
||||
|
||||
```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' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## 实现节点执行器
|
||||
|
||||
```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' // 继续执行
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注册方式
|
||||
|
||||
```typescript
|
||||
// 方式 1: 使用装饰器
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor { ... }
|
||||
|
||||
// 方式 2: 手动注册
|
||||
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
|
||||
```
|
||||
|
||||
## 节点注册表
|
||||
|
||||
```typescript
|
||||
import { NodeRegistry } from '@esengine/blueprint';
|
||||
|
||||
// 获取单例
|
||||
const registry = NodeRegistry.instance;
|
||||
|
||||
// 获取所有模板
|
||||
const allTemplates = registry.getAllTemplates();
|
||||
|
||||
// 按类别获取
|
||||
const mathNodes = registry.getTemplatesByCategory('math');
|
||||
|
||||
// 搜索节点
|
||||
const results = registry.searchTemplates('add');
|
||||
|
||||
// 检查是否存在
|
||||
if (registry.has('MyCustomNode')) { ... }
|
||||
```
|
||||
|
||||
## 纯节点
|
||||
|
||||
纯节点没有副作用,其输出会被缓存:
|
||||
|
||||
```typescript
|
||||
const PureNodeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDistance',
|
||||
title: 'Get Distance',
|
||||
category: 'math',
|
||||
isPure: true, // 标记为纯节点
|
||||
inputs: [
|
||||
{ name: 'a', type: 'vector2', direction: 'input' },
|
||||
{ name: 'b', type: 'vector2', direction: 'input' }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'distance', type: 'number', direction: 'output' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## 实际示例:输入处理节点
|
||||
|
||||
```typescript
|
||||
const InputMoveTemplate: BlueprintNodeTemplate = {
|
||||
type: 'InputMove',
|
||||
title: 'Get Movement Input',
|
||||
category: 'input',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'direction', type: 'vector2', direction: 'output' }
|
||||
],
|
||||
isPure: true
|
||||
};
|
||||
|
||||
@RegisterNode(InputMoveTemplate)
|
||||
class InputMoveExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const input = context.scene.services.get(InputServiceToken);
|
||||
const direction = {
|
||||
x: input.getAxis('horizontal'),
|
||||
y: input.getAxis('vertical')
|
||||
};
|
||||
return { outputs: { direction } };
|
||||
}
|
||||
}
|
||||
```
|
||||
168
docs/src/content/docs/modules/blueprint/examples.md
Normal file
168
docs/src/content/docs/modules/blueprint/examples.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
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
|
||||
};
|
||||
|
||||
@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 } };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 状态切换逻辑
|
||||
|
||||
```typescript
|
||||
// 在蓝图中实现状态机逻辑
|
||||
const stateBlueprint = createEmptyBlueprint('PlayerState');
|
||||
|
||||
// 添加状态变量
|
||||
stateBlueprint.variables.push({
|
||||
name: 'currentState',
|
||||
type: 'string',
|
||||
defaultValue: 'idle',
|
||||
scope: 'instance'
|
||||
});
|
||||
|
||||
// 在 Tick 事件中检查状态转换
|
||||
// ... 通过节点连接实现
|
||||
```
|
||||
|
||||
## 伤害处理系统
|
||||
|
||||
```typescript
|
||||
// 自定义伤害节点
|
||||
const ApplyDamageTemplate: BlueprintNodeTemplate = {
|
||||
type: 'ApplyDamage',
|
||||
title: 'Apply Damage',
|
||||
category: 'combat',
|
||||
inputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
|
||||
{ name: 'target', type: 'entity', direction: 'input' },
|
||||
{ name: 'amount', type: 'number', direction: 'input', defaultValue: 10 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
|
||||
{ name: 'killed', type: 'boolean', direction: 'output' }
|
||||
]
|
||||
};
|
||||
|
||||
@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 health = target.getComponent(HealthComponent);
|
||||
if (health) {
|
||||
health.current -= amount;
|
||||
const killed = health.current <= 0;
|
||||
return {
|
||||
outputs: { killed },
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
|
||||
return { outputs: { killed: false }, nextExec: 'exec' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 技能冷却系统
|
||||
|
||||
```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. 使用片段复用逻辑
|
||||
|
||||
```typescript
|
||||
// 将通用逻辑封装为片段
|
||||
const movementFragment = createFragment('Movement', {
|
||||
inputs: [{ name: 'speed', type: 'number', ... }],
|
||||
outputs: [{ name: 'position', type: 'vector2', ... }],
|
||||
graph: { ... }
|
||||
});
|
||||
|
||||
// 通过组合器构建复杂蓝图
|
||||
const composer = createComposer('Player');
|
||||
composer.addFragment(movementFragment, 'movement');
|
||||
composer.addFragment(combatFragment, 'combat');
|
||||
```
|
||||
|
||||
### 2. 合理使用变量作用域
|
||||
|
||||
```typescript
|
||||
// local: 临时计算结果
|
||||
{ name: 'tempValue', scope: 'local' }
|
||||
|
||||
// instance: 实体状态(如生命值)
|
||||
{ name: 'health', scope: 'instance' }
|
||||
|
||||
// global: 游戏全局状态
|
||||
{ name: 'score', scope: 'global' }
|
||||
```
|
||||
|
||||
### 3. 避免无限循环
|
||||
|
||||
```typescript
|
||||
// VM 有每帧最大执行步数限制(默认 1000)
|
||||
// 使用 Delay 节点打断长执行链
|
||||
vm.maxStepsPerFrame = 1000;
|
||||
```
|
||||
|
||||
### 4. 调试技巧
|
||||
|
||||
```typescript
|
||||
// 启用调试模式查看执行日志
|
||||
vm.debug = true;
|
||||
|
||||
// 使用 Print 节点输出中间值
|
||||
// 在编辑器中设置断点
|
||||
```
|
||||
|
||||
### 5. 性能优化
|
||||
|
||||
```typescript
|
||||
// 纯节点的输出会被缓存
|
||||
{ isPure: true }
|
||||
|
||||
// 避免在 Tick 中执行重计算
|
||||
// 使用事件驱动而非轮询
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "蓝图可视化脚本 (Blueprint)"
|
||||
description: "完整的可视化脚本系统"
|
||||
---
|
||||
|
||||
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。
|
||||
@@ -104,406 +105,10 @@ type VariableScope =
|
||||
| 'global'; // 全局共享
|
||||
```
|
||||
|
||||
## 虚拟机 API
|
||||
## 文档导航
|
||||
|
||||
### BlueprintVM
|
||||
|
||||
蓝图虚拟机负责执行蓝图图:
|
||||
|
||||
```typescript
|
||||
import { BlueprintVM } from '@esengine/blueprint';
|
||||
|
||||
// 创建 VM
|
||||
const vm = new BlueprintVM(blueprintAsset, entity, scene);
|
||||
|
||||
// 启动(触发 BeginPlay)
|
||||
vm.start();
|
||||
|
||||
// 每帧更新(触发 Tick)
|
||||
vm.tick(deltaTime);
|
||||
|
||||
// 停止(触发 EndPlay)
|
||||
vm.stop();
|
||||
|
||||
// 暂停/恢复
|
||||
vm.pause();
|
||||
vm.resume();
|
||||
|
||||
// 触发事件
|
||||
vm.triggerEvent('EventCollision', { other: otherEntity });
|
||||
vm.triggerCustomEvent('OnDamage', { amount: 50 });
|
||||
|
||||
// 调试模式
|
||||
vm.debug = true;
|
||||
```
|
||||
|
||||
### 执行上下文
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
### 执行结果
|
||||
|
||||
```typescript
|
||||
interface ExecutionResult {
|
||||
outputs?: Record<string, unknown>; // 输出值
|
||||
nextExec?: string | null; // 下一个执行引脚
|
||||
delay?: number; // 延迟执行(毫秒)
|
||||
yield?: boolean; // 暂停到下一帧
|
||||
error?: string; // 错误信息
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义节点
|
||||
|
||||
### 定义节点模板
|
||||
|
||||
```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' }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 实现节点执行器
|
||||
|
||||
```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' // 继续执行
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用装饰器注册
|
||||
|
||||
```typescript
|
||||
// 方式 1: 使用装饰器
|
||||
@RegisterNode(MyNodeTemplate)
|
||||
class MyNodeExecutor implements INodeExecutor { ... }
|
||||
|
||||
// 方式 2: 手动注册
|
||||
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
|
||||
```
|
||||
|
||||
## 节点注册表
|
||||
|
||||
```typescript
|
||||
import { NodeRegistry } from '@esengine/blueprint';
|
||||
|
||||
// 获取单例
|
||||
const registry = NodeRegistry.instance;
|
||||
|
||||
// 获取所有模板
|
||||
const allTemplates = registry.getAllTemplates();
|
||||
|
||||
// 按类别获取
|
||||
const mathNodes = registry.getTemplatesByCategory('math');
|
||||
|
||||
// 搜索节点
|
||||
const results = registry.searchTemplates('add');
|
||||
|
||||
// 检查是否存在
|
||||
if (registry.has('MyCustomNode')) { ... }
|
||||
```
|
||||
|
||||
## 内置节点
|
||||
|
||||
### 事件节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `EventBeginPlay` | 蓝图启动时触发 |
|
||||
| `EventTick` | 每帧触发 |
|
||||
| `EventEndPlay` | 蓝图停止时触发 |
|
||||
| `EventCollision` | 碰撞时触发 |
|
||||
| `EventInput` | 输入事件触发 |
|
||||
| `EventTimer` | 定时器触发 |
|
||||
| `EventMessage` | 自定义消息触发 |
|
||||
|
||||
### 时间节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Delay` | 延迟执行 |
|
||||
| `GetDeltaTime` | 获取帧间隔 |
|
||||
| `GetTime` | 获取运行时间 |
|
||||
|
||||
### 数学节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Add` | 加法 |
|
||||
| `Subtract` | 减法 |
|
||||
| `Multiply` | 乘法 |
|
||||
| `Divide` | 除法 |
|
||||
| `Abs` | 绝对值 |
|
||||
| `Clamp` | 限制范围 |
|
||||
| `Lerp` | 线性插值 |
|
||||
| `Min` / `Max` | 最小/最大值 |
|
||||
|
||||
### 调试节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Print` | 打印到控制台 |
|
||||
|
||||
## 蓝图组合
|
||||
|
||||
### 蓝图片段
|
||||
|
||||
将可复用的逻辑封装为片段:
|
||||
|
||||
```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: [...]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 组合蓝图
|
||||
|
||||
```typescript
|
||||
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
|
||||
|
||||
// 注册片段
|
||||
FragmentRegistry.instance.register('health', healthFragment);
|
||||
FragmentRegistry.instance.register('movement', movementFragment);
|
||||
|
||||
// 创建组合器
|
||||
const composer = createComposer('PlayerBlueprint');
|
||||
|
||||
// 添加片段到槽位
|
||||
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
|
||||
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
|
||||
|
||||
// 连接槽位
|
||||
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
|
||||
|
||||
// 验证
|
||||
const validation = composer.validate();
|
||||
if (!validation.isValid) {
|
||||
console.error(validation.errors);
|
||||
}
|
||||
|
||||
// 编译成蓝图
|
||||
const blueprint = composer.compile();
|
||||
```
|
||||
|
||||
## 触发器系统
|
||||
|
||||
### 定义触发条件
|
||||
|
||||
```typescript
|
||||
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
|
||||
|
||||
const lowHealthCondition: TriggerCondition = {
|
||||
type: 'comparison',
|
||||
left: { type: 'variable', name: 'health' },
|
||||
operator: '<',
|
||||
right: { type: 'constant', value: 20 }
|
||||
};
|
||||
```
|
||||
|
||||
### 使用触发器分发器
|
||||
|
||||
```typescript
|
||||
const dispatcher = new TriggerDispatcher();
|
||||
|
||||
// 注册触发器
|
||||
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
|
||||
context.triggerEvent('OnLowHealth');
|
||||
});
|
||||
|
||||
// 每帧评估
|
||||
dispatcher.evaluate(context);
|
||||
```
|
||||
|
||||
## 与 ECS 集成
|
||||
|
||||
### 使用蓝图系统
|
||||
|
||||
```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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 触发蓝图事件
|
||||
|
||||
```typescript
|
||||
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
|
||||
|
||||
// 触发内置事件
|
||||
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
|
||||
|
||||
// 触发自定义事件
|
||||
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 玩家控制蓝图
|
||||
|
||||
```typescript
|
||||
// 定义输入处理节点
|
||||
const InputMoveTemplate: BlueprintNodeTemplate = {
|
||||
type: 'InputMove',
|
||||
title: 'Get Movement Input',
|
||||
category: 'input',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'direction', type: 'vector2', direction: 'output' }
|
||||
],
|
||||
isPure: true
|
||||
};
|
||||
|
||||
@RegisterNode(InputMoveTemplate)
|
||||
class InputMoveExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const input = context.scene.services.get(InputServiceToken);
|
||||
const direction = {
|
||||
x: input.getAxis('horizontal'),
|
||||
y: input.getAxis('vertical')
|
||||
};
|
||||
return { outputs: { direction } };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态切换逻辑
|
||||
|
||||
```typescript
|
||||
// 在蓝图中实现状态机逻辑
|
||||
const stateBlueprint = createEmptyBlueprint('PlayerState');
|
||||
|
||||
// 添加状态变量
|
||||
stateBlueprint.variables.push({
|
||||
name: 'currentState',
|
||||
type: 'string',
|
||||
defaultValue: 'idle',
|
||||
scope: 'instance'
|
||||
});
|
||||
|
||||
// 在 Tick 事件中检查状态转换
|
||||
// ... 通过节点连接实现
|
||||
```
|
||||
|
||||
## 序列化
|
||||
|
||||
### 保存蓝图
|
||||
|
||||
```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);
|
||||
}
|
||||
```
|
||||
|
||||
### 加载蓝图
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用片段复用逻辑**
|
||||
- 将通用逻辑封装为片段
|
||||
- 通过组合器构建复杂蓝图
|
||||
|
||||
2. **合理使用变量作用域**
|
||||
- `local`: 临时计算结果
|
||||
- `instance`: 实体状态(如生命值)
|
||||
- `global`: 游戏全局状态
|
||||
|
||||
3. **避免无限循环**
|
||||
- VM 有每帧最大执行步数限制(默认 1000)
|
||||
- 使用 Delay 节点打断长执行链
|
||||
|
||||
4. **调试技巧**
|
||||
- 启用 `vm.debug = true` 查看执行日志
|
||||
- 使用 Print 节点输出中间值
|
||||
|
||||
5. **性能优化**
|
||||
- 纯节点(`isPure: true`)的输出会被缓存
|
||||
- 避免在 Tick 中执行重计算
|
||||
- [虚拟机 API](./vm) - BlueprintVM 执行和上下文
|
||||
- [自定义节点](./custom-nodes) - 创建自定义节点
|
||||
- [内置节点](./nodes) - 内置节点参考
|
||||
- [蓝图组合](./composition) - 片段和组合器
|
||||
- [实际示例](./examples) - ECS 集成和最佳实践
|
||||
|
||||
107
docs/src/content/docs/modules/blueprint/nodes.md
Normal file
107
docs/src/content/docs/modules/blueprint/nodes.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: "内置节点"
|
||||
description: "蓝图内置节点参考"
|
||||
---
|
||||
|
||||
## 事件节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `EventBeginPlay` | 蓝图启动时触发 |
|
||||
| `EventTick` | 每帧触发 |
|
||||
| `EventEndPlay` | 蓝图停止时触发 |
|
||||
| `EventCollision` | 碰撞时触发 |
|
||||
| `EventInput` | 输入事件触发 |
|
||||
| `EventTimer` | 定时器触发 |
|
||||
| `EventMessage` | 自定义消息触发 |
|
||||
|
||||
## 流程控制节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Branch` | 条件分支 (if/else) |
|
||||
| `Sequence` | 顺序执行多个输出 |
|
||||
| `ForLoop` | 循环执行 |
|
||||
| `WhileLoop` | 条件循环 |
|
||||
| `DoOnce` | 只执行一次 |
|
||||
| `FlipFlop` | 交替执行两个分支 |
|
||||
| `Gate` | 可开关的执行门 |
|
||||
|
||||
## 时间节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Delay` | 延迟执行 |
|
||||
| `GetDeltaTime` | 获取帧间隔 |
|
||||
| `GetTime` | 获取运行时间 |
|
||||
| `SetTimer` | 设置定时器 |
|
||||
| `ClearTimer` | 清除定时器 |
|
||||
|
||||
## 数学节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Add` | 加法 |
|
||||
| `Subtract` | 减法 |
|
||||
| `Multiply` | 乘法 |
|
||||
| `Divide` | 除法 |
|
||||
| `Abs` | 绝对值 |
|
||||
| `Clamp` | 限制范围 |
|
||||
| `Lerp` | 线性插值 |
|
||||
| `Min` / `Max` | 最小/最大值 |
|
||||
| `Sin` / `Cos` | 三角函数 |
|
||||
| `Sqrt` | 平方根 |
|
||||
| `Power` | 幂运算 |
|
||||
|
||||
## 逻辑节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `And` | 逻辑与 |
|
||||
| `Or` | 逻辑或 |
|
||||
| `Not` | 逻辑非 |
|
||||
| `Equal` | 相等比较 |
|
||||
| `NotEqual` | 不等比较 |
|
||||
| `Greater` | 大于比较 |
|
||||
| `Less` | 小于比较 |
|
||||
|
||||
## 向量节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `MakeVector2` | 创建 2D 向量 |
|
||||
| `BreakVector2` | 分解 2D 向量 |
|
||||
| `VectorAdd` | 向量加法 |
|
||||
| `VectorSubtract` | 向量减法 |
|
||||
| `VectorMultiply` | 向量乘法 |
|
||||
| `VectorLength` | 向量长度 |
|
||||
| `VectorNormalize` | 向量归一化 |
|
||||
| `VectorDistance` | 向量距离 |
|
||||
|
||||
## 实体节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `GetSelf` | 获取当前实体 |
|
||||
| `GetComponent` | 获取组件 |
|
||||
| `HasComponent` | 检查组件 |
|
||||
| `AddComponent` | 添加组件 |
|
||||
| `RemoveComponent` | 移除组件 |
|
||||
| `SpawnEntity` | 创建实体 |
|
||||
| `DestroyEntity` | 销毁实体 |
|
||||
|
||||
## 变量节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `GetVariable` | 获取变量值 |
|
||||
| `SetVariable` | 设置变量值 |
|
||||
|
||||
## 调试节点
|
||||
|
||||
| 节点 | 说明 |
|
||||
|------|------|
|
||||
| `Print` | 打印到控制台 |
|
||||
| `DrawDebugLine` | 绘制调试线 |
|
||||
| `DrawDebugPoint` | 绘制调试点 |
|
||||
| `Breakpoint` | 调试断点 |
|
||||
133
docs/src/content/docs/modules/blueprint/vm.md
Normal file
133
docs/src/content/docs/modules/blueprint/vm.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "虚拟机 API"
|
||||
description: "BlueprintVM 执行和上下文"
|
||||
---
|
||||
|
||||
## BlueprintVM
|
||||
|
||||
蓝图虚拟机负责执行蓝图图:
|
||||
|
||||
```typescript
|
||||
import { BlueprintVM } from '@esengine/blueprint';
|
||||
|
||||
// 创建 VM
|
||||
const vm = new BlueprintVM(blueprintAsset, entity, scene);
|
||||
|
||||
// 启动(触发 BeginPlay)
|
||||
vm.start();
|
||||
|
||||
// 每帧更新(触发 Tick)
|
||||
vm.tick(deltaTime);
|
||||
|
||||
// 停止(触发 EndPlay)
|
||||
vm.stop();
|
||||
|
||||
// 暂停/恢复
|
||||
vm.pause();
|
||||
vm.resume();
|
||||
|
||||
// 触发事件
|
||||
vm.triggerEvent('EventCollision', { other: otherEntity });
|
||||
vm.triggerCustomEvent('OnDamage', { amount: 50 });
|
||||
|
||||
// 调试模式
|
||||
vm.debug = true;
|
||||
```
|
||||
|
||||
## 执行上下文
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
## 执行结果
|
||||
|
||||
```typescript
|
||||
interface ExecutionResult {
|
||||
outputs?: Record<string, unknown>; // 输出值
|
||||
nextExec?: string | null; // 下一个执行引脚
|
||||
delay?: number; // 延迟执行(毫秒)
|
||||
yield?: boolean; // 暂停到下一帧
|
||||
error?: string; // 错误信息
|
||||
}
|
||||
```
|
||||
|
||||
## 与 ECS 集成
|
||||
|
||||
### 使用蓝图系统
|
||||
|
||||
```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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 触发蓝图事件
|
||||
|
||||
```typescript
|
||||
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
|
||||
|
||||
// 触发内置事件
|
||||
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
|
||||
|
||||
// 触发自定义事件
|
||||
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
|
||||
```
|
||||
|
||||
## 序列化
|
||||
|
||||
### 保存蓝图
|
||||
|
||||
```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);
|
||||
}
|
||||
```
|
||||
|
||||
### 加载蓝图
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
135
docs/src/content/docs/modules/fsm/api.md
Normal file
135
docs/src/content/docs/modules/fsm/api.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: "API 参考"
|
||||
description: "状态机完整 API"
|
||||
---
|
||||
|
||||
## createStateMachine
|
||||
|
||||
```typescript
|
||||
function createStateMachine<TState extends string, TContext = unknown>(
|
||||
initialState: TState,
|
||||
options?: StateMachineOptions<TContext>
|
||||
): IStateMachine<TState, TContext>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `initialState` - 初始状态
|
||||
- `options.context` - 上下文对象,在回调中可访问
|
||||
- `options.maxHistorySize` - 最大历史记录数(默认 100)
|
||||
- `options.enableHistory` - 是否启用历史记录(默认 true)
|
||||
|
||||
## 状态机属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| `current` | `TState` | 当前状态 |
|
||||
| `previous` | `TState \| null` | 上一个状态 |
|
||||
| `context` | `TContext` | 上下文对象 |
|
||||
| `isTransitioning` | `boolean` | 是否正在转换中 |
|
||||
| `currentStateDuration` | `number` | 当前状态持续时间(毫秒) |
|
||||
|
||||
## 状态定义
|
||||
|
||||
```typescript
|
||||
// 定义状态
|
||||
fsm.defineState('idle', {
|
||||
onEnter: (ctx, from) => {},
|
||||
onExit: (ctx, to) => {},
|
||||
onUpdate: (ctx, dt) => {}
|
||||
});
|
||||
|
||||
// 检查状态是否存在
|
||||
fsm.hasState('idle'); // true
|
||||
|
||||
// 获取状态配置
|
||||
fsm.getStateConfig('idle');
|
||||
|
||||
// 获取所有状态
|
||||
fsm.getStates(); // ['idle', 'walk', ...]
|
||||
```
|
||||
|
||||
## 转换操作
|
||||
|
||||
```typescript
|
||||
// 定义转换
|
||||
fsm.defineTransition('idle', 'walk', condition, priority);
|
||||
|
||||
// 移除转换
|
||||
fsm.removeTransition('idle', 'walk');
|
||||
|
||||
// 获取从某状态出发的所有转换
|
||||
fsm.getTransitionsFrom('idle');
|
||||
|
||||
// 检查是否可以转换
|
||||
fsm.canTransition('walk'); // true/false
|
||||
|
||||
// 手动转换
|
||||
fsm.transition('walk');
|
||||
|
||||
// 强制转换(忽略条件)
|
||||
fsm.transition('walk', true);
|
||||
|
||||
// 自动评估转换条件
|
||||
fsm.evaluateTransitions();
|
||||
```
|
||||
|
||||
## 生命周期
|
||||
|
||||
```typescript
|
||||
// 更新状态机(调用当前状态的 onUpdate)
|
||||
fsm.update(deltaTime);
|
||||
|
||||
// 重置状态机
|
||||
fsm.reset(); // 重置到当前状态
|
||||
fsm.reset('idle'); // 重置到指定状态
|
||||
```
|
||||
|
||||
## 事件监听
|
||||
|
||||
```typescript
|
||||
// 监听进入特定状态
|
||||
const unsubscribe = fsm.onEnter('walk', (from) => {
|
||||
console.log(`从 ${from} 进入 walk`);
|
||||
});
|
||||
|
||||
// 监听退出特定状态
|
||||
fsm.onExit('walk', (to) => {
|
||||
console.log(`从 walk 退出到 ${to}`);
|
||||
});
|
||||
|
||||
// 监听任意状态变化
|
||||
fsm.onChange((event) => {
|
||||
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
|
||||
});
|
||||
|
||||
// 取消订阅
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
## 调试
|
||||
|
||||
```typescript
|
||||
// 获取状态历史
|
||||
const history = fsm.getHistory();
|
||||
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
|
||||
|
||||
// 清除历史
|
||||
fsm.clearHistory();
|
||||
|
||||
// 获取调试信息
|
||||
const info = fsm.getDebugInfo();
|
||||
// { current, previous, duration, stateCount, transitionCount, historySize }
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
FSM 模块提供了可视化脚本支持的蓝图节点:
|
||||
|
||||
- `GetCurrentState` - 获取当前状态
|
||||
- `TransitionTo` - 转换到指定状态
|
||||
- `CanTransition` - 检查是否可以转换
|
||||
- `IsInState` - 检查是否在指定状态
|
||||
- `WasInState` - 检查是否曾在指定状态
|
||||
- `GetStateDuration` - 获取状态持续时间
|
||||
- `EvaluateTransitions` - 评估转换条件
|
||||
- `ResetStateMachine` - 重置状态机
|
||||
230
docs/src/content/docs/modules/fsm/examples.md
Normal file
230
docs/src/content/docs/modules/fsm/examples.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
title: "实际示例"
|
||||
description: "角色状态机、ECS 集成"
|
||||
---
|
||||
|
||||
## 角色状态机
|
||||
|
||||
```typescript
|
||||
import { createStateMachine } from '@esengine/fsm';
|
||||
|
||||
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
|
||||
|
||||
interface CharacterContext {
|
||||
velocity: { x: number; y: number };
|
||||
isGrounded: boolean;
|
||||
isAttacking: boolean;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
|
||||
context: {
|
||||
velocity: { x: 0, y: 0 },
|
||||
isGrounded: true,
|
||||
isAttacking: false,
|
||||
speed: 0
|
||||
}
|
||||
});
|
||||
|
||||
// 定义状态
|
||||
characterFSM.defineState('idle', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.speed = 0;
|
||||
},
|
||||
onUpdate: (ctx, dt) => {
|
||||
// 播放待机动画
|
||||
}
|
||||
});
|
||||
|
||||
characterFSM.defineState('walk', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.speed = 100;
|
||||
}
|
||||
});
|
||||
|
||||
characterFSM.defineState('run', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.speed = 200;
|
||||
}
|
||||
});
|
||||
|
||||
characterFSM.defineState('jump', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.velocity.y = -300;
|
||||
ctx.isGrounded = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 定义转换
|
||||
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
|
||||
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
|
||||
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
|
||||
characterFSM.defineTransition('run', 'walk', (ctx) => Math.abs(ctx.velocity.x) <= 150);
|
||||
|
||||
// 跳跃有最高优先级
|
||||
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
|
||||
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
|
||||
characterFSM.defineTransition('run', 'jump', (ctx) => !ctx.isGrounded, 10);
|
||||
|
||||
characterFSM.defineTransition('jump', 'fall', (ctx) => ctx.velocity.y > 0);
|
||||
characterFSM.defineTransition('fall', 'idle', (ctx) => ctx.isGrounded);
|
||||
|
||||
// 游戏循环中使用
|
||||
function gameUpdate(dt: number) {
|
||||
// 更新上下文
|
||||
characterFSM.context.velocity.x = getInputVelocity();
|
||||
characterFSM.context.isGrounded = checkGrounded();
|
||||
|
||||
// 评估状态转换
|
||||
characterFSM.evaluateTransitions();
|
||||
|
||||
// 更新当前状态
|
||||
characterFSM.update(dt);
|
||||
}
|
||||
```
|
||||
|
||||
## 与 ECS 集成
|
||||
|
||||
```typescript
|
||||
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
|
||||
|
||||
// 状态机组件
|
||||
class FSMComponent extends Component {
|
||||
fsm: IStateMachine<string>;
|
||||
|
||||
constructor(initialState: string) {
|
||||
super();
|
||||
this.fsm = createStateMachine(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态机系统
|
||||
class FSMSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(FSMComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const fsmComp = entity.getComponent(FSMComponent);
|
||||
fsmComp.fsm.evaluateTransitions();
|
||||
fsmComp.fsm.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI 行为状态机
|
||||
|
||||
```typescript
|
||||
type AIState = 'patrol' | 'chase' | 'attack' | 'flee' | 'dead';
|
||||
|
||||
interface AIContext {
|
||||
health: number;
|
||||
target: Entity | null;
|
||||
distanceToTarget: number;
|
||||
attackRange: number;
|
||||
sightRange: number;
|
||||
}
|
||||
|
||||
const aiFSM = createStateMachine<AIState, AIContext>('patrol', {
|
||||
context: {
|
||||
health: 100,
|
||||
target: null,
|
||||
distanceToTarget: Infinity,
|
||||
attackRange: 50,
|
||||
sightRange: 200
|
||||
}
|
||||
});
|
||||
|
||||
// 巡逻状态
|
||||
aiFSM.defineState('patrol', {
|
||||
onEnter: () => console.log('开始巡逻'),
|
||||
onUpdate: (ctx, dt) => {
|
||||
// 沿巡逻路径移动
|
||||
}
|
||||
});
|
||||
|
||||
// 追击状态
|
||||
aiFSM.defineState('chase', {
|
||||
onEnter: () => console.log('发现目标,开始追击'),
|
||||
onUpdate: (ctx, dt) => {
|
||||
// 向目标移动
|
||||
}
|
||||
});
|
||||
|
||||
// 攻击状态
|
||||
aiFSM.defineState('attack', {
|
||||
onEnter: () => console.log('进入攻击范围'),
|
||||
onUpdate: (ctx, dt) => {
|
||||
// 执行攻击
|
||||
}
|
||||
});
|
||||
|
||||
// 逃跑状态
|
||||
aiFSM.defineState('flee', {
|
||||
onEnter: () => console.log('血量过低,逃跑'),
|
||||
onUpdate: (ctx, dt) => {
|
||||
// 远离目标
|
||||
}
|
||||
});
|
||||
|
||||
// 转换规则
|
||||
aiFSM.defineTransition('patrol', 'chase',
|
||||
(ctx) => ctx.target !== null && ctx.distanceToTarget < ctx.sightRange);
|
||||
aiFSM.defineTransition('chase', 'attack',
|
||||
(ctx) => ctx.distanceToTarget < ctx.attackRange);
|
||||
aiFSM.defineTransition('attack', 'chase',
|
||||
(ctx) => ctx.distanceToTarget >= ctx.attackRange);
|
||||
aiFSM.defineTransition('chase', 'patrol',
|
||||
(ctx) => ctx.target === null || ctx.distanceToTarget > ctx.sightRange);
|
||||
|
||||
// 逃跑优先级最高
|
||||
aiFSM.defineTransition('patrol', 'flee', (ctx) => ctx.health < 20, 100);
|
||||
aiFSM.defineTransition('chase', 'flee', (ctx) => ctx.health < 20, 100);
|
||||
aiFSM.defineTransition('attack', 'flee', (ctx) => ctx.health < 20, 100);
|
||||
|
||||
aiFSM.defineTransition('flee', 'patrol', (ctx) => ctx.health >= 20);
|
||||
```
|
||||
|
||||
## 动画状态机
|
||||
|
||||
```typescript
|
||||
type AnimState = 'idle' | 'walk' | 'run' | 'jump_up' | 'jump_down' | 'land';
|
||||
|
||||
interface AnimContext {
|
||||
animator: Animator;
|
||||
velocityX: number;
|
||||
velocityY: number;
|
||||
isGrounded: boolean;
|
||||
}
|
||||
|
||||
const animFSM = createStateMachine<AnimState, AnimContext>('idle', {
|
||||
context: { animator: null!, velocityX: 0, velocityY: 0, isGrounded: true }
|
||||
});
|
||||
|
||||
animFSM.defineState('idle', {
|
||||
onEnter: (ctx) => ctx.animator.play('idle')
|
||||
});
|
||||
|
||||
animFSM.defineState('walk', {
|
||||
onEnter: (ctx) => ctx.animator.play('walk')
|
||||
});
|
||||
|
||||
animFSM.defineState('run', {
|
||||
onEnter: (ctx) => ctx.animator.play('run')
|
||||
});
|
||||
|
||||
animFSM.defineState('jump_up', {
|
||||
onEnter: (ctx) => ctx.animator.play('jump_up')
|
||||
});
|
||||
|
||||
animFSM.defineState('jump_down', {
|
||||
onEnter: (ctx) => ctx.animator.play('jump_down')
|
||||
});
|
||||
|
||||
animFSM.defineState('land', {
|
||||
onEnter: (ctx) => ctx.animator.play('land')
|
||||
});
|
||||
|
||||
// 设置转换(略)
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "状态机 (FSM)"
|
||||
description: "类型安全的有限状态机实现"
|
||||
---
|
||||
|
||||
`@esengine/fsm` 提供了一个类型安全的有限状态机实现,用于角色、AI 或任何需要状态管理的场景。
|
||||
@@ -91,249 +92,7 @@ fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
|
||||
// 如果同时满足,会先尝试 attack(优先级 10)
|
||||
```
|
||||
|
||||
## API 参考
|
||||
## 文档导航
|
||||
|
||||
### createStateMachine
|
||||
|
||||
```typescript
|
||||
function createStateMachine<TState extends string, TContext = unknown>(
|
||||
initialState: TState,
|
||||
options?: StateMachineOptions<TContext>
|
||||
): IStateMachine<TState, TContext>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `initialState` - 初始状态
|
||||
- `options.context` - 上下文对象,在回调中可访问
|
||||
- `options.maxHistorySize` - 最大历史记录数(默认 100)
|
||||
- `options.enableHistory` - 是否启用历史记录(默认 true)
|
||||
|
||||
### 状态机属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| `current` | `TState` | 当前状态 |
|
||||
| `previous` | `TState \| null` | 上一个状态 |
|
||||
| `context` | `TContext` | 上下文对象 |
|
||||
| `isTransitioning` | `boolean` | 是否正在转换中 |
|
||||
| `currentStateDuration` | `number` | 当前状态持续时间(毫秒) |
|
||||
|
||||
### 状态机方法
|
||||
|
||||
#### 状态定义
|
||||
|
||||
```typescript
|
||||
// 定义状态
|
||||
fsm.defineState('idle', {
|
||||
onEnter: (ctx, from) => {},
|
||||
onExit: (ctx, to) => {},
|
||||
onUpdate: (ctx, dt) => {}
|
||||
});
|
||||
|
||||
// 检查状态是否存在
|
||||
fsm.hasState('idle'); // true
|
||||
|
||||
// 获取状态配置
|
||||
fsm.getStateConfig('idle');
|
||||
|
||||
// 获取所有状态
|
||||
fsm.getStates(); // ['idle', 'walk', ...]
|
||||
```
|
||||
|
||||
#### 转换操作
|
||||
|
||||
```typescript
|
||||
// 定义转换
|
||||
fsm.defineTransition('idle', 'walk', condition, priority);
|
||||
|
||||
// 移除转换
|
||||
fsm.removeTransition('idle', 'walk');
|
||||
|
||||
// 获取从某状态出发的所有转换
|
||||
fsm.getTransitionsFrom('idle');
|
||||
|
||||
// 检查是否可以转换
|
||||
fsm.canTransition('walk'); // true/false
|
||||
|
||||
// 手动转换
|
||||
fsm.transition('walk');
|
||||
|
||||
// 强制转换(忽略条件)
|
||||
fsm.transition('walk', true);
|
||||
|
||||
// 自动评估转换条件
|
||||
fsm.evaluateTransitions();
|
||||
```
|
||||
|
||||
#### 生命周期
|
||||
|
||||
```typescript
|
||||
// 更新状态机(调用当前状态的 onUpdate)
|
||||
fsm.update(deltaTime);
|
||||
|
||||
// 重置状态机
|
||||
fsm.reset(); // 重置到当前状态
|
||||
fsm.reset('idle'); // 重置到指定状态
|
||||
```
|
||||
|
||||
#### 事件监听
|
||||
|
||||
```typescript
|
||||
// 监听进入特定状态
|
||||
const unsubscribe = fsm.onEnter('walk', (from) => {
|
||||
console.log(`从 ${from} 进入 walk`);
|
||||
});
|
||||
|
||||
// 监听退出特定状态
|
||||
fsm.onExit('walk', (to) => {
|
||||
console.log(`从 walk 退出到 ${to}`);
|
||||
});
|
||||
|
||||
// 监听任意状态变化
|
||||
fsm.onChange((event) => {
|
||||
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
|
||||
});
|
||||
|
||||
// 取消订阅
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
#### 调试
|
||||
|
||||
```typescript
|
||||
// 获取状态历史
|
||||
const history = fsm.getHistory();
|
||||
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
|
||||
|
||||
// 清除历史
|
||||
fsm.clearHistory();
|
||||
|
||||
// 获取调试信息
|
||||
const info = fsm.getDebugInfo();
|
||||
// { current, previous, duration, stateCount, transitionCount, historySize }
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 角色状态机
|
||||
|
||||
```typescript
|
||||
import { createStateMachine } from '@esengine/fsm';
|
||||
|
||||
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
|
||||
|
||||
interface CharacterContext {
|
||||
velocity: { x: number; y: number };
|
||||
isGrounded: boolean;
|
||||
isAttacking: boolean;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
|
||||
context: {
|
||||
velocity: { x: 0, y: 0 },
|
||||
isGrounded: true,
|
||||
isAttacking: false,
|
||||
speed: 0
|
||||
}
|
||||
});
|
||||
|
||||
// 定义状态
|
||||
characterFSM.defineState('idle', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.speed = 0;
|
||||
},
|
||||
onUpdate: (ctx, dt) => {
|
||||
// 播放待机动画
|
||||
}
|
||||
});
|
||||
|
||||
characterFSM.defineState('walk', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.speed = 100;
|
||||
}
|
||||
});
|
||||
|
||||
characterFSM.defineState('run', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.speed = 200;
|
||||
}
|
||||
});
|
||||
|
||||
characterFSM.defineState('jump', {
|
||||
onEnter: (ctx) => {
|
||||
ctx.velocity.y = -300;
|
||||
ctx.isGrounded = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 定义转换
|
||||
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
|
||||
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
|
||||
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
|
||||
characterFSM.defineTransition('run', 'walk', (ctx) => Math.abs(ctx.velocity.x) <= 150);
|
||||
|
||||
// 跳跃有最高优先级
|
||||
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
|
||||
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
|
||||
characterFSM.defineTransition('run', 'jump', (ctx) => !ctx.isGrounded, 10);
|
||||
|
||||
characterFSM.defineTransition('jump', 'fall', (ctx) => ctx.velocity.y > 0);
|
||||
characterFSM.defineTransition('fall', 'idle', (ctx) => ctx.isGrounded);
|
||||
|
||||
// 游戏循环中使用
|
||||
function gameUpdate(dt: number) {
|
||||
// 更新上下文
|
||||
characterFSM.context.velocity.x = getInputVelocity();
|
||||
characterFSM.context.isGrounded = checkGrounded();
|
||||
|
||||
// 评估状态转换
|
||||
characterFSM.evaluateTransitions();
|
||||
|
||||
// 更新当前状态
|
||||
characterFSM.update(dt);
|
||||
}
|
||||
```
|
||||
|
||||
### 与 ECS 集成
|
||||
|
||||
```typescript
|
||||
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
|
||||
|
||||
// 状态机组件
|
||||
class FSMComponent extends Component {
|
||||
fsm: IStateMachine<string>;
|
||||
|
||||
constructor(initialState: string) {
|
||||
super();
|
||||
this.fsm = createStateMachine(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态机系统
|
||||
class FSMSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(FSMComponent));
|
||||
}
|
||||
|
||||
protected processEntity(entity: Entity, dt: number): void {
|
||||
const fsmComp = entity.getComponent(FSMComponent);
|
||||
fsmComp.fsm.evaluateTransitions();
|
||||
fsmComp.fsm.update(dt);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
FSM 模块提供了可视化脚本支持的蓝图节点:
|
||||
|
||||
- `GetCurrentState` - 获取当前状态
|
||||
- `TransitionTo` - 转换到指定状态
|
||||
- `CanTransition` - 检查是否可以转换
|
||||
- `IsInState` - 检查是否在指定状态
|
||||
- `WasInState` - 检查是否曾在指定状态
|
||||
- `GetStateDuration` - 获取状态持续时间
|
||||
- `EvaluateTransitions` - 评估转换条件
|
||||
- `ResetStateMachine` - 重置状态机
|
||||
- [API 参考](./api) - 完整的状态机 API
|
||||
- [实际示例](./examples) - 角色状态机、ECS 集成
|
||||
|
||||
164
docs/src/content/docs/modules/pathfinding/examples.md
Normal file
164
docs/src/content/docs/modules/pathfinding/examples.md
Normal file
@@ -0,0 +1,164 @@
|
||||
---
|
||||
title: "实际示例"
|
||||
description: "游戏移动、动态障碍物、分层寻路"
|
||||
---
|
||||
|
||||
## 游戏角色移动
|
||||
|
||||
```typescript
|
||||
class MovementSystem {
|
||||
private grid: GridMap;
|
||||
private pathfinder: AStarPathfinder;
|
||||
private smoother: CombinedSmoother;
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
this.grid = createGridMap(width, height);
|
||||
this.pathfinder = createAStarPathfinder(this.grid);
|
||||
this.smoother = createCombinedSmoother();
|
||||
}
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPoint[] | null {
|
||||
const result = this.pathfinder.findPath(
|
||||
from.x, from.y,
|
||||
to.x, to.y
|
||||
);
|
||||
|
||||
if (!result.found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 平滑路径
|
||||
return this.smoother.smooth(result.path, this.grid);
|
||||
}
|
||||
|
||||
setObstacle(x: number, y: number): void {
|
||||
this.grid.setWalkable(x, y, false);
|
||||
}
|
||||
|
||||
setTerrain(x: number, y: number, cost: number): void {
|
||||
this.grid.setCost(x, y, cost);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 动态障碍物
|
||||
|
||||
```typescript
|
||||
class DynamicPathfinding {
|
||||
private grid: GridMap;
|
||||
private pathfinder: AStarPathfinder;
|
||||
private dynamicObstacles: Set<string> = new Set();
|
||||
|
||||
addDynamicObstacle(x: number, y: number): void {
|
||||
const key = `${x},${y}`;
|
||||
if (!this.dynamicObstacles.has(key)) {
|
||||
this.dynamicObstacles.add(key);
|
||||
this.grid.setWalkable(x, y, false);
|
||||
}
|
||||
}
|
||||
|
||||
removeDynamicObstacle(x: number, y: number): void {
|
||||
const key = `${x},${y}`;
|
||||
if (this.dynamicObstacles.has(key)) {
|
||||
this.dynamicObstacles.delete(key);
|
||||
this.grid.setWalkable(x, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPathResult {
|
||||
return this.pathfinder.findPath(from.x, from.y, to.x, to.y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 不同地形代价
|
||||
|
||||
```typescript
|
||||
// 设置不同地形的移动代价
|
||||
const grid = createGridMap(50, 50);
|
||||
|
||||
// 普通地面 - 代价 1(默认)
|
||||
// 沙地 - 代价 2
|
||||
for (let y = 10; y < 20; y++) {
|
||||
for (let x = 0; x < 50; x++) {
|
||||
grid.setCost(x, y, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 沼泽 - 代价 4
|
||||
for (let y = 30; y < 35; y++) {
|
||||
for (let x = 20; x < 30; x++) {
|
||||
grid.setCost(x, y, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// 寻路时会自动考虑地形代价
|
||||
const result = pathfinder.findPath(0, 0, 49, 49);
|
||||
```
|
||||
|
||||
## 分层寻路
|
||||
|
||||
对于大型地图,使用层级化寻路:
|
||||
|
||||
```typescript
|
||||
class HierarchicalPathfinding {
|
||||
private coarseGrid: GridMap; // 粗粒度网格
|
||||
private fineGrid: GridMap; // 细粒度网格
|
||||
private coarsePathfinder: AStarPathfinder;
|
||||
private finePathfinder: AStarPathfinder;
|
||||
private cellSize = 10;
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPoint[] {
|
||||
// 1. 在粗粒度网格上寻路
|
||||
const coarseFrom = this.toCoarse(from);
|
||||
const coarseTo = this.toCoarse(to);
|
||||
const coarseResult = this.coarsePathfinder.findPath(
|
||||
coarseFrom.x, coarseFrom.y,
|
||||
coarseTo.x, coarseTo.y
|
||||
);
|
||||
|
||||
if (!coarseResult.found) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. 在每个粗粒度单元内进行细粒度寻路
|
||||
const finePath: IPoint[] = [];
|
||||
// ... 详细实现略
|
||||
return finePath;
|
||||
}
|
||||
|
||||
private toCoarse(p: IPoint): IPoint {
|
||||
return {
|
||||
x: Math.floor(p.x / this.cellSize),
|
||||
y: Math.floor(p.y / this.cellSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **限制搜索范围**
|
||||
```typescript
|
||||
pathfinder.findPath(x1, y1, x2, y2, { maxNodes: 1000 });
|
||||
```
|
||||
|
||||
2. **使用启发式权重**
|
||||
```typescript
|
||||
// 权重 > 1 会更快但可能不是最优路径
|
||||
pathfinder.findPath(x1, y1, x2, y2, { heuristicWeight: 1.5 });
|
||||
```
|
||||
|
||||
3. **复用寻路器实例**
|
||||
```typescript
|
||||
// 创建一次,多次使用
|
||||
const pathfinder = createAStarPathfinder(grid);
|
||||
```
|
||||
|
||||
4. **使用导航网格**
|
||||
- 对于复杂地形,NavMesh 比网格寻路更高效
|
||||
- 多边形数量远少于网格单元格数量
|
||||
|
||||
5. **选择合适的启发式**
|
||||
- 4方向移动用 `manhattanDistance`
|
||||
- 8方向移动用 `octileDistance`(默认)
|
||||
112
docs/src/content/docs/modules/pathfinding/grid-map.md
Normal file
112
docs/src/content/docs/modules/pathfinding/grid-map.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "网格地图 API"
|
||||
description: "网格操作和 A* 寻路"
|
||||
---
|
||||
|
||||
## createGridMap
|
||||
|
||||
```typescript
|
||||
function createGridMap(
|
||||
width: number,
|
||||
height: number,
|
||||
options?: IGridMapOptions
|
||||
): GridMap
|
||||
```
|
||||
|
||||
**配置选项:**
|
||||
|
||||
| 属性 | 类型 | 默认值 | 描述 |
|
||||
|------|------|--------|------|
|
||||
| `allowDiagonal` | `boolean` | `true` | 允许对角移动 |
|
||||
| `diagonalCost` | `number` | `√2` | 对角移动代价 |
|
||||
| `avoidCorners` | `boolean` | `true` | 避免穿角 |
|
||||
| `heuristic` | `HeuristicFunction` | `octileDistance` | 启发式函数 |
|
||||
|
||||
## 地图操作
|
||||
|
||||
```typescript
|
||||
// 检查/设置可通行性
|
||||
grid.isWalkable(x, y);
|
||||
grid.setWalkable(x, y, false);
|
||||
|
||||
// 设置移动代价(如沼泽、沙地)
|
||||
grid.setCost(x, y, 2); // 代价为 2(默认 1)
|
||||
|
||||
// 设置矩形区域
|
||||
grid.setRectWalkable(0, 0, 5, 5, false);
|
||||
|
||||
// 从数组加载(0=可通行,非0=障碍)
|
||||
grid.loadFromArray([
|
||||
[0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0]
|
||||
]);
|
||||
|
||||
// 从字符串加载(.=可通行,#=障碍)
|
||||
grid.loadFromString(`
|
||||
.....
|
||||
.#.#.
|
||||
.#...
|
||||
`);
|
||||
|
||||
// 导出为字符串
|
||||
console.log(grid.toString());
|
||||
|
||||
// 重置所有节点为可通行
|
||||
grid.reset();
|
||||
```
|
||||
|
||||
## A* 寻路器
|
||||
|
||||
### createAStarPathfinder
|
||||
|
||||
```typescript
|
||||
function createAStarPathfinder(map: IPathfindingMap): AStarPathfinder
|
||||
```
|
||||
|
||||
### findPath
|
||||
|
||||
```typescript
|
||||
const result = pathfinder.findPath(
|
||||
startX, startY,
|
||||
endX, endY,
|
||||
{
|
||||
maxNodes: 5000, // 限制搜索节点数
|
||||
heuristicWeight: 1.5 // 加速但可能非最优
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 重用寻路器
|
||||
|
||||
```typescript
|
||||
// 寻路器可重用,内部会自动清理状态
|
||||
pathfinder.findPath(0, 0, 10, 10);
|
||||
pathfinder.findPath(5, 5, 15, 15);
|
||||
|
||||
// 手动清理(可选)
|
||||
pathfinder.clear();
|
||||
```
|
||||
|
||||
## 方向常量
|
||||
|
||||
```typescript
|
||||
import { DIRECTIONS_4, DIRECTIONS_8 } from '@esengine/pathfinding';
|
||||
|
||||
// 4方向(上下左右)
|
||||
DIRECTIONS_4 // [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, ...]
|
||||
|
||||
// 8方向(含对角线)
|
||||
DIRECTIONS_8 // [{ dx: 0, dy: -1 }, { dx: 1, dy: -1 }, ...]
|
||||
```
|
||||
|
||||
## 启发式函数
|
||||
|
||||
```typescript
|
||||
import { manhattanDistance, octileDistance } from '@esengine/pathfinding';
|
||||
|
||||
// 自定义启发式
|
||||
const grid = createGridMap(20, 20, {
|
||||
heuristic: manhattanDistance // 使用曼哈顿距离
|
||||
});
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "寻路系统 (Pathfinding)"
|
||||
description: "完整的 2D 寻路解决方案"
|
||||
---
|
||||
|
||||
`@esengine/pathfinding` 提供了完整的 2D 寻路解决方案,包括 A* 算法、网格地图、导航网格和路径平滑。
|
||||
@@ -67,29 +68,21 @@ const result = navmesh.findPath(1, 1, 18, 8);
|
||||
|
||||
## 核心概念
|
||||
|
||||
### IPoint - 坐标点
|
||||
### 核心接口
|
||||
|
||||
```typescript
|
||||
interface IPoint {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
}
|
||||
```
|
||||
|
||||
### IPathResult - 寻路结果
|
||||
|
||||
```typescript
|
||||
interface IPathResult {
|
||||
readonly found: boolean; // 是否找到路径
|
||||
readonly path: readonly IPoint[]; // 路径点列表
|
||||
readonly cost: number; // 路径总代价
|
||||
readonly nodesSearched: number; // 搜索的节点数
|
||||
}
|
||||
```
|
||||
|
||||
### IPathfindingOptions - 寻路配置
|
||||
|
||||
```typescript
|
||||
interface IPathfindingOptions {
|
||||
maxNodes?: number; // 最大搜索节点数(默认 10000)
|
||||
heuristicWeight?: number; // 启发式权重(>1 更快但可能非最优)
|
||||
@@ -98,402 +91,16 @@ interface IPathfindingOptions {
|
||||
}
|
||||
```
|
||||
|
||||
## 启发式函数
|
||||
|
||||
模块提供了四种启发式函数:
|
||||
### 启发式函数
|
||||
|
||||
| 函数 | 适用场景 | 说明 |
|
||||
|------|----------|------|
|
||||
| `manhattanDistance` | 4方向移动 | 曼哈顿距离,只考虑水平/垂直 |
|
||||
| `euclideanDistance` | 任意方向 | 欧几里得距离,直线距离 |
|
||||
| `chebyshevDistance` | 8方向移动 | 切比雪夫距离,对角线代价为 1 |
|
||||
| `octileDistance` | 8方向移动 | 八角距离,对角线代价为 √2(默认) |
|
||||
| `manhattanDistance` | 4方向移动 | 曼哈顿距离 |
|
||||
| `euclideanDistance` | 任意方向 | 欧几里得距离 |
|
||||
| `chebyshevDistance` | 8方向移动 | 切比雪夫距离 |
|
||||
| `octileDistance` | 8方向移动 | 八角距离(默认) |
|
||||
|
||||
```typescript
|
||||
import { manhattanDistance, octileDistance } from '@esengine/pathfinding';
|
||||
|
||||
// 自定义启发式
|
||||
const grid = createGridMap(20, 20, {
|
||||
heuristic: manhattanDistance // 使用曼哈顿距离
|
||||
});
|
||||
```
|
||||
|
||||
## 网格地图 API
|
||||
|
||||
### createGridMap
|
||||
|
||||
```typescript
|
||||
function createGridMap(
|
||||
width: number,
|
||||
height: number,
|
||||
options?: IGridMapOptions
|
||||
): GridMap
|
||||
```
|
||||
|
||||
**配置选项:**
|
||||
|
||||
| 属性 | 类型 | 默认值 | 描述 |
|
||||
|------|------|--------|------|
|
||||
| `allowDiagonal` | `boolean` | `true` | 允许对角移动 |
|
||||
| `diagonalCost` | `number` | `√2` | 对角移动代价 |
|
||||
| `avoidCorners` | `boolean` | `true` | 避免穿角 |
|
||||
| `heuristic` | `HeuristicFunction` | `octileDistance` | 启发式函数 |
|
||||
|
||||
### 地图操作
|
||||
|
||||
```typescript
|
||||
// 检查/设置可通行性
|
||||
grid.isWalkable(x, y);
|
||||
grid.setWalkable(x, y, false);
|
||||
|
||||
// 设置移动代价(如沼泽、沙地)
|
||||
grid.setCost(x, y, 2); // 代价为 2(默认 1)
|
||||
|
||||
// 设置矩形区域
|
||||
grid.setRectWalkable(0, 0, 5, 5, false);
|
||||
|
||||
// 从数组加载(0=可通行,非0=障碍)
|
||||
grid.loadFromArray([
|
||||
[0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0]
|
||||
]);
|
||||
|
||||
// 从字符串加载(.=可通行,#=障碍)
|
||||
grid.loadFromString(`
|
||||
.....
|
||||
.#.#.
|
||||
.#...
|
||||
`);
|
||||
|
||||
// 导出为字符串
|
||||
console.log(grid.toString());
|
||||
|
||||
// 重置所有节点为可通行
|
||||
grid.reset();
|
||||
```
|
||||
|
||||
### 方向常量
|
||||
|
||||
```typescript
|
||||
import { DIRECTIONS_4, DIRECTIONS_8 } from '@esengine/pathfinding';
|
||||
|
||||
// 4方向(上下左右)
|
||||
DIRECTIONS_4 // [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, ...]
|
||||
|
||||
// 8方向(含对角线)
|
||||
DIRECTIONS_8 // [{ dx: 0, dy: -1 }, { dx: 1, dy: -1 }, ...]
|
||||
```
|
||||
|
||||
## A* 寻路器 API
|
||||
|
||||
### createAStarPathfinder
|
||||
|
||||
```typescript
|
||||
function createAStarPathfinder(map: IPathfindingMap): AStarPathfinder
|
||||
```
|
||||
|
||||
### findPath
|
||||
|
||||
```typescript
|
||||
const result = pathfinder.findPath(
|
||||
startX, startY,
|
||||
endX, endY,
|
||||
{
|
||||
maxNodes: 5000, // 限制搜索节点数
|
||||
heuristicWeight: 1.5 // 加速但可能非最优
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 重用寻路器
|
||||
|
||||
```typescript
|
||||
// 寻路器可重用,内部会自动清理状态
|
||||
pathfinder.findPath(0, 0, 10, 10);
|
||||
pathfinder.findPath(5, 5, 15, 15);
|
||||
|
||||
// 手动清理(可选)
|
||||
pathfinder.clear();
|
||||
```
|
||||
|
||||
## 导航网格 API
|
||||
|
||||
### createNavMesh
|
||||
|
||||
```typescript
|
||||
function createNavMesh(): NavMesh
|
||||
```
|
||||
|
||||
### 构建导航网格
|
||||
|
||||
```typescript
|
||||
const navmesh = createNavMesh();
|
||||
|
||||
// 添加凸多边形
|
||||
const id1 = navmesh.addPolygon([
|
||||
{ x: 0, y: 0 }, { x: 10, y: 0 },
|
||||
{ x: 10, y: 10 }, { x: 0, y: 10 }
|
||||
]);
|
||||
|
||||
const id2 = navmesh.addPolygon([
|
||||
{ x: 10, y: 0 }, { x: 20, y: 0 },
|
||||
{ x: 20, y: 10 }, { x: 10, y: 10 }
|
||||
]);
|
||||
|
||||
// 方式1:自动检测共享边并建立连接
|
||||
navmesh.build();
|
||||
|
||||
// 方式2:手动设置连接
|
||||
navmesh.setConnection(id1, id2, {
|
||||
left: { x: 10, y: 0 },
|
||||
right: { x: 10, y: 10 }
|
||||
});
|
||||
```
|
||||
|
||||
### 查询和寻路
|
||||
|
||||
```typescript
|
||||
// 查找包含点的多边形
|
||||
const polygon = navmesh.findPolygonAt(5, 5);
|
||||
|
||||
// 检查位置是否可通行
|
||||
navmesh.isWalkable(5, 5);
|
||||
|
||||
// 寻路(内部使用漏斗算法优化路径)
|
||||
const result = navmesh.findPath(1, 1, 18, 8);
|
||||
```
|
||||
|
||||
## 路径平滑 API
|
||||
|
||||
### 视线简化
|
||||
|
||||
移除不必要的中间点:
|
||||
|
||||
```typescript
|
||||
import { createLineOfSightSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createLineOfSightSmoother();
|
||||
const smoothedPath = smoother.smooth(result.path, grid);
|
||||
|
||||
// 原路径: [(0,0), (1,1), (2,2), (3,3), (4,4)]
|
||||
// 简化后: [(0,0), (4,4)]
|
||||
```
|
||||
|
||||
### 曲线平滑
|
||||
|
||||
使用 Catmull-Rom 样条曲线:
|
||||
|
||||
```typescript
|
||||
import { createCatmullRomSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createCatmullRomSmoother(
|
||||
5, // segments - 每段插值点数
|
||||
0.5 // tension - 张力 (0-1)
|
||||
);
|
||||
|
||||
const curvedPath = smoother.smooth(result.path, grid);
|
||||
```
|
||||
|
||||
### 组合平滑
|
||||
|
||||
先简化再曲线平滑:
|
||||
|
||||
```typescript
|
||||
import { createCombinedSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createCombinedSmoother(5, 0.5);
|
||||
const finalPath = smoother.smooth(result.path, grid);
|
||||
```
|
||||
|
||||
### 视线检测函数
|
||||
|
||||
```typescript
|
||||
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
|
||||
|
||||
// Bresenham 算法(快速,网格对齐)
|
||||
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
|
||||
|
||||
// 射线投射(精确,支持浮点坐标)
|
||||
const hasLOS = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 游戏角色移动
|
||||
|
||||
```typescript
|
||||
class MovementSystem {
|
||||
private grid: GridMap;
|
||||
private pathfinder: AStarPathfinder;
|
||||
private smoother: CombinedSmoother;
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
this.grid = createGridMap(width, height);
|
||||
this.pathfinder = createAStarPathfinder(this.grid);
|
||||
this.smoother = createCombinedSmoother();
|
||||
}
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPoint[] | null {
|
||||
const result = this.pathfinder.findPath(
|
||||
from.x, from.y,
|
||||
to.x, to.y
|
||||
);
|
||||
|
||||
if (!result.found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 平滑路径
|
||||
return this.smoother.smooth(result.path, this.grid);
|
||||
}
|
||||
|
||||
setObstacle(x: number, y: number): void {
|
||||
this.grid.setWalkable(x, y, false);
|
||||
}
|
||||
|
||||
setTerrain(x: number, y: number, cost: number): void {
|
||||
this.grid.setCost(x, y, cost);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动态障碍物
|
||||
|
||||
```typescript
|
||||
class DynamicPathfinding {
|
||||
private grid: GridMap;
|
||||
private pathfinder: AStarPathfinder;
|
||||
private dynamicObstacles: Set<string> = new Set();
|
||||
|
||||
addDynamicObstacle(x: number, y: number): void {
|
||||
const key = `${x},${y}`;
|
||||
if (!this.dynamicObstacles.has(key)) {
|
||||
this.dynamicObstacles.add(key);
|
||||
this.grid.setWalkable(x, y, false);
|
||||
}
|
||||
}
|
||||
|
||||
removeDynamicObstacle(x: number, y: number): void {
|
||||
const key = `${x},${y}`;
|
||||
if (this.dynamicObstacles.has(key)) {
|
||||
this.dynamicObstacles.delete(key);
|
||||
this.grid.setWalkable(x, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPathResult {
|
||||
return this.pathfinder.findPath(from.x, from.y, to.x, to.y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 不同地形代价
|
||||
|
||||
```typescript
|
||||
// 设置不同地形的移动代价
|
||||
const grid = createGridMap(50, 50);
|
||||
|
||||
// 普通地面 - 代价 1(默认)
|
||||
// 沙地 - 代价 2
|
||||
for (let y = 10; y < 20; y++) {
|
||||
for (let x = 0; x < 50; x++) {
|
||||
grid.setCost(x, y, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 沼泽 - 代价 4
|
||||
for (let y = 30; y < 35; y++) {
|
||||
for (let x = 20; x < 30; x++) {
|
||||
grid.setCost(x, y, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// 寻路时会自动考虑地形代价
|
||||
const result = pathfinder.findPath(0, 0, 49, 49);
|
||||
```
|
||||
|
||||
### 分层寻路
|
||||
|
||||
对于大型地图,使用层级化寻路:
|
||||
|
||||
```typescript
|
||||
class HierarchicalPathfinding {
|
||||
private coarseGrid: GridMap; // 粗粒度网格
|
||||
private fineGrid: GridMap; // 细粒度网格
|
||||
private coarsePathfinder: AStarPathfinder;
|
||||
private finePathfinder: AStarPathfinder;
|
||||
private cellSize = 10;
|
||||
|
||||
findPath(from: IPoint, to: IPoint): IPoint[] {
|
||||
// 1. 在粗粒度网格上寻路
|
||||
const coarseFrom = this.toCoarse(from);
|
||||
const coarseTo = this.toCoarse(to);
|
||||
const coarseResult = this.coarsePathfinder.findPath(
|
||||
coarseFrom.x, coarseFrom.y,
|
||||
coarseTo.x, coarseTo.y
|
||||
);
|
||||
|
||||
if (!coarseResult.found) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. 在每个粗粒度单元内进行细粒度寻路
|
||||
const finePath: IPoint[] = [];
|
||||
// ... 详细实现略
|
||||
return finePath;
|
||||
}
|
||||
|
||||
private toCoarse(p: IPoint): IPoint {
|
||||
return {
|
||||
x: Math.floor(p.x / this.cellSize),
|
||||
y: Math.floor(p.y / this.cellSize)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
Pathfinding 模块提供了可视化脚本支持的蓝图节点:
|
||||
|
||||
- `FindPath` - 查找路径
|
||||
- `FindPathSmooth` - 查找并平滑路径
|
||||
- `IsWalkable` - 检查位置是否可通行
|
||||
- `GetPathLength` - 获取路径点数
|
||||
- `GetPathDistance` - 获取路径总距离
|
||||
- `GetPathPoint` - 获取路径上的指定点
|
||||
- `MoveAlongPath` - 沿路径移动
|
||||
- `HasLineOfSight` - 检查视线
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **限制搜索范围**
|
||||
```typescript
|
||||
pathfinder.findPath(x1, y1, x2, y2, { maxNodes: 1000 });
|
||||
```
|
||||
|
||||
2. **使用启发式权重**
|
||||
```typescript
|
||||
// 权重 > 1 会更快但可能不是最优路径
|
||||
pathfinder.findPath(x1, y1, x2, y2, { heuristicWeight: 1.5 });
|
||||
```
|
||||
|
||||
3. **复用寻路器实例**
|
||||
```typescript
|
||||
// 创建一次,多次使用
|
||||
const pathfinder = createAStarPathfinder(grid);
|
||||
```
|
||||
|
||||
4. **使用导航网格**
|
||||
- 对于复杂地形,NavMesh 比网格寻路更高效
|
||||
- 多边形数量远少于网格单元格数量
|
||||
|
||||
5. **选择合适的启发式**
|
||||
- 4方向移动用 `manhattanDistance`
|
||||
- 8方向移动用 `octileDistance`(默认)
|
||||
|
||||
## 网格 vs 导航网格
|
||||
### 网格 vs 导航网格
|
||||
|
||||
| 特性 | GridMap | NavMesh |
|
||||
|------|---------|---------|
|
||||
@@ -501,4 +108,10 @@ Pathfinding 模块提供了可视化脚本支持的蓝图节点:
|
||||
| 内存占用 | 较高 (width × height) | 较低 (多边形数) |
|
||||
| 精度 | 网格对齐 | 连续坐标 |
|
||||
| 动态修改 | 容易 | 需要重建 |
|
||||
| 设置复杂度 | 简单 | 较复杂 |
|
||||
|
||||
## 文档导航
|
||||
|
||||
- [网格地图 API](./grid-map) - 网格操作和 A* 寻路
|
||||
- [导航网格 API](./navmesh) - NavMesh 构建和查询
|
||||
- [路径平滑](./smoothing) - 视线简化和曲线平滑
|
||||
- [实际示例](./examples) - 游戏移动、动态障碍物、分层寻路
|
||||
|
||||
67
docs/src/content/docs/modules/pathfinding/navmesh.md
Normal file
67
docs/src/content/docs/modules/pathfinding/navmesh.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "导航网格 API"
|
||||
description: "NavMesh 构建和查询"
|
||||
---
|
||||
|
||||
## createNavMesh
|
||||
|
||||
```typescript
|
||||
function createNavMesh(): NavMesh
|
||||
```
|
||||
|
||||
## 构建导航网格
|
||||
|
||||
```typescript
|
||||
const navmesh = createNavMesh();
|
||||
|
||||
// 添加凸多边形
|
||||
const id1 = navmesh.addPolygon([
|
||||
{ x: 0, y: 0 }, { x: 10, y: 0 },
|
||||
{ x: 10, y: 10 }, { x: 0, y: 10 }
|
||||
]);
|
||||
|
||||
const id2 = navmesh.addPolygon([
|
||||
{ x: 10, y: 0 }, { x: 20, y: 0 },
|
||||
{ x: 20, y: 10 }, { x: 10, y: 10 }
|
||||
]);
|
||||
|
||||
// 方式1:自动检测共享边并建立连接
|
||||
navmesh.build();
|
||||
|
||||
// 方式2:手动设置连接
|
||||
navmesh.setConnection(id1, id2, {
|
||||
left: { x: 10, y: 0 },
|
||||
right: { x: 10, y: 10 }
|
||||
});
|
||||
```
|
||||
|
||||
## 查询和寻路
|
||||
|
||||
```typescript
|
||||
// 查找包含点的多边形
|
||||
const polygon = navmesh.findPolygonAt(5, 5);
|
||||
|
||||
// 检查位置是否可通行
|
||||
navmesh.isWalkable(5, 5);
|
||||
|
||||
// 寻路(内部使用漏斗算法优化路径)
|
||||
const result = navmesh.findPath(1, 1, 18, 8);
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
导航网格适合:
|
||||
- 复杂不规则地形
|
||||
- 需要精确路径的场景
|
||||
- 多边形数量远少于网格单元格的大地图
|
||||
|
||||
```typescript
|
||||
// 从编辑器导出的导航网格数据
|
||||
const navData = await loadNavMeshData('level1.navmesh');
|
||||
|
||||
const navmesh = createNavMesh();
|
||||
for (const poly of navData.polygons) {
|
||||
navmesh.addPolygon(poly.vertices);
|
||||
}
|
||||
navmesh.build();
|
||||
```
|
||||
67
docs/src/content/docs/modules/pathfinding/smoothing.md
Normal file
67
docs/src/content/docs/modules/pathfinding/smoothing.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "路径平滑"
|
||||
description: "视线简化和曲线平滑"
|
||||
---
|
||||
|
||||
## 视线简化
|
||||
|
||||
移除不必要的中间点:
|
||||
|
||||
```typescript
|
||||
import { createLineOfSightSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createLineOfSightSmoother();
|
||||
const smoothedPath = smoother.smooth(result.path, grid);
|
||||
|
||||
// 原路径: [(0,0), (1,1), (2,2), (3,3), (4,4)]
|
||||
// 简化后: [(0,0), (4,4)]
|
||||
```
|
||||
|
||||
## 曲线平滑
|
||||
|
||||
使用 Catmull-Rom 样条曲线:
|
||||
|
||||
```typescript
|
||||
import { createCatmullRomSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createCatmullRomSmoother(
|
||||
5, // segments - 每段插值点数
|
||||
0.5 // tension - 张力 (0-1)
|
||||
);
|
||||
|
||||
const curvedPath = smoother.smooth(result.path, grid);
|
||||
```
|
||||
|
||||
## 组合平滑
|
||||
|
||||
先简化再曲线平滑:
|
||||
|
||||
```typescript
|
||||
import { createCombinedSmoother } from '@esengine/pathfinding';
|
||||
|
||||
const smoother = createCombinedSmoother(5, 0.5);
|
||||
const finalPath = smoother.smooth(result.path, grid);
|
||||
```
|
||||
|
||||
## 视线检测函数
|
||||
|
||||
```typescript
|
||||
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
|
||||
|
||||
// Bresenham 算法(快速,网格对齐)
|
||||
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
|
||||
|
||||
// 射线投射(精确,支持浮点坐标)
|
||||
const hasLOS = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
- `FindPath` - 查找路径
|
||||
- `FindPathSmooth` - 查找并平滑路径
|
||||
- `IsWalkable` - 检查位置是否可通行
|
||||
- `GetPathLength` - 获取路径点数
|
||||
- `GetPathDistance` - 获取路径总距离
|
||||
- `GetPathPoint` - 获取路径上的指定点
|
||||
- `MoveAlongPath` - 沿路径移动
|
||||
- `HasLineOfSight` - 检查视线
|
||||
230
docs/src/content/docs/modules/procgen/examples.md
Normal file
230
docs/src/content/docs/modules/procgen/examples.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
title: "实际示例"
|
||||
description: "地形、战利品、敌人和关卡生成"
|
||||
---
|
||||
|
||||
## 程序化地形生成
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||||
|
||||
class TerrainGenerator {
|
||||
private fbm: FBM;
|
||||
private moistureFbm: FBM;
|
||||
|
||||
constructor(seed: number) {
|
||||
const heightNoise = createPerlinNoise(seed);
|
||||
const moistureNoise = createPerlinNoise(seed + 1000);
|
||||
|
||||
this.fbm = createFBM(heightNoise, {
|
||||
octaves: 8,
|
||||
persistence: 0.5,
|
||||
frequency: 0.01
|
||||
});
|
||||
|
||||
this.moistureFbm = createFBM(moistureNoise, {
|
||||
octaves: 4,
|
||||
persistence: 0.6,
|
||||
frequency: 0.02
|
||||
});
|
||||
}
|
||||
|
||||
getHeight(x: number, y: number): number {
|
||||
// 基础高度
|
||||
let height = this.fbm.noise2D(x, y);
|
||||
|
||||
// 添加山脉
|
||||
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
|
||||
|
||||
return (height + 1) * 0.5; // 归一化到 [0, 1]
|
||||
}
|
||||
|
||||
getBiome(x: number, y: number): string {
|
||||
const height = this.getHeight(x, y);
|
||||
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
|
||||
|
||||
if (height < 0.3) return 'water';
|
||||
if (height < 0.4) return 'beach';
|
||||
if (height > 0.8) return 'mountain';
|
||||
|
||||
if (moisture < 0.3) return 'desert';
|
||||
if (moisture > 0.7) return 'forest';
|
||||
return 'grassland';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 战利品系统
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom, createWeightedRandom, pickOne } from '@esengine/procgen';
|
||||
|
||||
interface LootItem {
|
||||
id: string;
|
||||
rarity: string;
|
||||
}
|
||||
|
||||
class LootSystem {
|
||||
private rng: SeededRandom;
|
||||
private raritySelector: WeightedRandom<string>;
|
||||
private lootTables: Map<string, LootItem[]> = new Map();
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
|
||||
this.raritySelector = createWeightedRandom([
|
||||
{ value: 'common', weight: 60 },
|
||||
{ value: 'uncommon', weight: 25 },
|
||||
{ value: 'rare', weight: 10 },
|
||||
{ value: 'legendary', weight: 5 }
|
||||
]);
|
||||
|
||||
// 初始化战利品表
|
||||
this.lootTables.set('common', [/* ... */]);
|
||||
this.lootTables.set('rare', [/* ... */]);
|
||||
}
|
||||
|
||||
generateLoot(count: number): LootItem[] {
|
||||
const loot: LootItem[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const rarity = this.raritySelector.pick(this.rng);
|
||||
const table = this.lootTables.get(rarity)!;
|
||||
const item = pickOne(table, this.rng);
|
||||
loot.push(item);
|
||||
}
|
||||
|
||||
return loot;
|
||||
}
|
||||
|
||||
// 保证可重现
|
||||
setSeed(seed: number): void {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 程序化敌人放置
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom } from '@esengine/procgen';
|
||||
|
||||
class EnemySpawner {
|
||||
private rng: SeededRandom;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
|
||||
spawnEnemiesInArea(
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
radius: number,
|
||||
count: number
|
||||
): Array<{ x: number; y: number; type: string }> {
|
||||
const enemies: Array<{ x: number; y: number; type: string }> = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 在圆内生成位置
|
||||
const pos = this.rng.nextPointInCircle(radius);
|
||||
|
||||
// 随机选择敌人类型
|
||||
const type = this.rng.nextBool(0.2) ? 'elite' : 'normal';
|
||||
|
||||
enemies.push({
|
||||
x: centerX + pos.x,
|
||||
y: centerY + pos.y,
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
return enemies;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 程序化关卡布局
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom, shuffle } from '@esengine/procgen';
|
||||
|
||||
interface Room {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
type: 'start' | 'combat' | 'treasure' | 'boss';
|
||||
}
|
||||
|
||||
class DungeonGenerator {
|
||||
private rng: SeededRandom;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
|
||||
generate(roomCount: number): Room[] {
|
||||
const rooms: Room[] = [];
|
||||
|
||||
// 生成房间
|
||||
for (let i = 0; i < roomCount; i++) {
|
||||
rooms.push({
|
||||
x: this.rng.nextInt(0, 100),
|
||||
y: this.rng.nextInt(0, 100),
|
||||
width: this.rng.nextInt(5, 15),
|
||||
height: this.rng.nextInt(5, 15),
|
||||
type: 'combat'
|
||||
});
|
||||
}
|
||||
|
||||
// 随机分配特殊房间
|
||||
shuffle(rooms, this.rng);
|
||||
rooms[0].type = 'start';
|
||||
rooms[1].type = 'treasure';
|
||||
rooms[rooms.length - 1].type = 'boss';
|
||||
|
||||
return rooms;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
Procgen 模块提供了可视化脚本支持的蓝图节点:
|
||||
|
||||
### 噪声节点
|
||||
|
||||
- `SampleNoise2D` - 采样 2D 噪声
|
||||
- `SampleFBM` - 采样 FBM 噪声
|
||||
|
||||
### 随机节点
|
||||
|
||||
- `SeededRandom` - 生成随机浮点数
|
||||
- `SeededRandomInt` - 生成随机整数
|
||||
- `WeightedPick` - 加权随机选择
|
||||
- `ShuffleArray` - 洗牌数组
|
||||
- `PickRandom` - 随机选择元素
|
||||
- `SampleArray` - 采样数组
|
||||
- `RandomPointInCircle` - 圆内随机点
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用种子保证可重现性**
|
||||
```typescript
|
||||
const seed = Date.now();
|
||||
const rng = createSeededRandom(seed);
|
||||
saveSeed(seed);
|
||||
```
|
||||
|
||||
2. **预计算加权选择器** - 避免重复创建
|
||||
|
||||
3. **选择合适的噪声函数**
|
||||
- Perlin:平滑过渡的地形、云彩
|
||||
- Simplex:性能要求高的场景
|
||||
- Worley:细胞、石头纹理
|
||||
- FBM:需要多层细节的自然效果
|
||||
|
||||
4. **调整 FBM 参数**
|
||||
- `octaves`:越多细节越丰富,但性能开销越大
|
||||
- `persistence`:0.5 是常用值,越大高频细节越明显
|
||||
- `lacunarity`:通常为 2,控制频率增长速度
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: "程序化生成 (Procgen)"
|
||||
description: "噪声函数、种子随机数和随机工具"
|
||||
---
|
||||
|
||||
`@esengine/procgen` 提供了程序化内容生成的核心工具,包括噪声函数、种子随机数和各种随机工具。
|
||||
@@ -67,493 +68,9 @@ const drop = loot.pick(rng);
|
||||
console.log(drop); // 大概率是 'common'
|
||||
```
|
||||
|
||||
## 噪声函数
|
||||
## 文档导航
|
||||
|
||||
### Perlin 噪声
|
||||
|
||||
经典的梯度噪声,输出范围 [-1, 1]:
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise } from '@esengine/procgen';
|
||||
|
||||
const perlin = createPerlinNoise(seed);
|
||||
|
||||
// 2D 噪声
|
||||
const value2D = perlin.noise2D(x, y);
|
||||
|
||||
// 3D 噪声
|
||||
const value3D = perlin.noise3D(x, y, z);
|
||||
```
|
||||
|
||||
### Simplex 噪声
|
||||
|
||||
比 Perlin 更快、更少方向性偏差:
|
||||
|
||||
```typescript
|
||||
import { createSimplexNoise } from '@esengine/procgen';
|
||||
|
||||
const simplex = createSimplexNoise(seed);
|
||||
|
||||
const value = simplex.noise2D(x, y);
|
||||
```
|
||||
|
||||
### Worley 噪声
|
||||
|
||||
基于细胞的噪声,适合生成石头、细胞等纹理:
|
||||
|
||||
```typescript
|
||||
import { createWorleyNoise } from '@esengine/procgen';
|
||||
|
||||
const worley = createWorleyNoise(seed);
|
||||
|
||||
// 返回到最近点的距离
|
||||
const distance = worley.noise2D(x, y);
|
||||
```
|
||||
|
||||
### FBM (分形布朗运动)
|
||||
|
||||
叠加多层噪声创建更丰富的细节:
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||||
|
||||
const baseNoise = createPerlinNoise(seed);
|
||||
|
||||
const fbm = createFBM(baseNoise, {
|
||||
octaves: 6, // 层数(越多细节越丰富)
|
||||
lacunarity: 2.0, // 频率倍增因子
|
||||
persistence: 0.5, // 振幅衰减因子
|
||||
frequency: 1.0, // 初始频率
|
||||
amplitude: 1.0 // 初始振幅
|
||||
});
|
||||
|
||||
// 标准 FBM
|
||||
const value = fbm.noise2D(x, y);
|
||||
|
||||
// Ridged FBM(脊状,适合山脉)
|
||||
const ridged = fbm.ridged2D(x, y);
|
||||
|
||||
// Turbulence(湍流)
|
||||
const turb = fbm.turbulence2D(x, y);
|
||||
|
||||
// Billowed(膨胀,适合云朵)
|
||||
const cloud = fbm.billowed2D(x, y);
|
||||
```
|
||||
|
||||
## 种子随机数 API
|
||||
|
||||
### SeededRandom
|
||||
|
||||
基于 xorshift128+ 算法的确定性伪随机数生成器:
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom } from '@esengine/procgen';
|
||||
|
||||
const rng = createSeededRandom(42);
|
||||
```
|
||||
|
||||
### 基础方法
|
||||
|
||||
```typescript
|
||||
// [0, 1) 浮点数
|
||||
rng.next();
|
||||
|
||||
// [min, max] 整数
|
||||
rng.nextInt(1, 10);
|
||||
|
||||
// [min, max) 浮点数
|
||||
rng.nextFloat(0, 100);
|
||||
|
||||
// 布尔值(可指定概率)
|
||||
rng.nextBool(); // 50%
|
||||
rng.nextBool(0.3); // 30%
|
||||
|
||||
// 重置到初始状态
|
||||
rng.reset();
|
||||
```
|
||||
|
||||
### 分布方法
|
||||
|
||||
```typescript
|
||||
// 正态分布(高斯分布)
|
||||
rng.nextGaussian(); // 均值 0, 标准差 1
|
||||
rng.nextGaussian(100, 15); // 均值 100, 标准差 15
|
||||
|
||||
// 指数分布
|
||||
rng.nextExponential(); // λ = 1
|
||||
rng.nextExponential(0.5); // λ = 0.5
|
||||
```
|
||||
|
||||
### 几何方法
|
||||
|
||||
```typescript
|
||||
// 圆内均匀分布的点
|
||||
const point = rng.nextPointInCircle(50); // { x, y }
|
||||
|
||||
// 圆周上的点
|
||||
const edge = rng.nextPointOnCircle(50); // { x, y }
|
||||
|
||||
// 球内均匀分布的点
|
||||
const point3D = rng.nextPointInSphere(50); // { x, y, z }
|
||||
|
||||
// 随机方向向量
|
||||
const dir = rng.nextDirection2D(); // { x, y },长度为 1
|
||||
```
|
||||
|
||||
## 加权随机 API
|
||||
|
||||
### WeightedRandom
|
||||
|
||||
预计算累积权重,高效随机选择:
|
||||
|
||||
```typescript
|
||||
import { createWeightedRandom } from '@esengine/procgen';
|
||||
|
||||
const selector = createWeightedRandom([
|
||||
{ value: 'apple', weight: 5 },
|
||||
{ value: 'banana', weight: 3 },
|
||||
{ value: 'cherry', weight: 2 }
|
||||
]);
|
||||
|
||||
// 使用种子随机数
|
||||
const result = selector.pick(rng);
|
||||
|
||||
// 使用 Math.random
|
||||
const result2 = selector.pickRandom();
|
||||
|
||||
// 获取概率
|
||||
console.log(selector.getProbability(0)); // 0.5 (5/10)
|
||||
console.log(selector.size); // 3
|
||||
console.log(selector.totalWeight); // 10
|
||||
```
|
||||
|
||||
### 便捷函数
|
||||
|
||||
```typescript
|
||||
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
|
||||
|
||||
// 从数组选择
|
||||
const item = weightedPick([
|
||||
{ value: 'a', weight: 1 },
|
||||
{ value: 'b', weight: 2 }
|
||||
], rng);
|
||||
|
||||
// 从对象选择
|
||||
const item2 = weightedPickFromMap({
|
||||
'common': 60,
|
||||
'rare': 30,
|
||||
'epic': 10
|
||||
}, rng);
|
||||
```
|
||||
|
||||
## 洗牌和采样 API
|
||||
|
||||
### shuffle / shuffleCopy
|
||||
|
||||
Fisher-Yates 洗牌算法:
|
||||
|
||||
```typescript
|
||||
import { shuffle, shuffleCopy } from '@esengine/procgen';
|
||||
|
||||
const arr = [1, 2, 3, 4, 5];
|
||||
|
||||
// 原地洗牌
|
||||
shuffle(arr, rng);
|
||||
|
||||
// 创建洗牌副本(不修改原数组)
|
||||
const shuffled = shuffleCopy(arr, rng);
|
||||
```
|
||||
|
||||
### pickOne
|
||||
|
||||
随机选择一个元素:
|
||||
|
||||
```typescript
|
||||
import { pickOne } from '@esengine/procgen';
|
||||
|
||||
const items = ['a', 'b', 'c', 'd'];
|
||||
const item = pickOne(items, rng);
|
||||
```
|
||||
|
||||
### sample / sampleWithReplacement
|
||||
|
||||
采样:
|
||||
|
||||
```typescript
|
||||
import { sample, sampleWithReplacement } from '@esengine/procgen';
|
||||
|
||||
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
// 采样 3 个不重复元素
|
||||
const unique = sample(arr, 3, rng);
|
||||
|
||||
// 采样 5 个(可重复)
|
||||
const withRep = sampleWithReplacement(arr, 5, rng);
|
||||
```
|
||||
|
||||
### randomIntegers
|
||||
|
||||
生成范围内的随机整数数组:
|
||||
|
||||
```typescript
|
||||
import { randomIntegers } from '@esengine/procgen';
|
||||
|
||||
// 从 1-100 中随机选 5 个不重复的数
|
||||
const nums = randomIntegers(1, 100, 5, rng);
|
||||
```
|
||||
|
||||
### weightedSample
|
||||
|
||||
按权重采样(不重复):
|
||||
|
||||
```typescript
|
||||
import { weightedSample } from '@esengine/procgen';
|
||||
|
||||
const items = ['A', 'B', 'C', 'D', 'E'];
|
||||
const weights = [10, 8, 6, 4, 2];
|
||||
|
||||
// 按权重选 3 个
|
||||
const selected = weightedSample(items, weights, 3, rng);
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 程序化地形生成
|
||||
|
||||
```typescript
|
||||
import { createPerlinNoise, createFBM } from '@esengine/procgen';
|
||||
|
||||
class TerrainGenerator {
|
||||
private fbm: FBM;
|
||||
private moistureFbm: FBM;
|
||||
|
||||
constructor(seed: number) {
|
||||
const heightNoise = createPerlinNoise(seed);
|
||||
const moistureNoise = createPerlinNoise(seed + 1000);
|
||||
|
||||
this.fbm = createFBM(heightNoise, {
|
||||
octaves: 8,
|
||||
persistence: 0.5,
|
||||
frequency: 0.01
|
||||
});
|
||||
|
||||
this.moistureFbm = createFBM(moistureNoise, {
|
||||
octaves: 4,
|
||||
persistence: 0.6,
|
||||
frequency: 0.02
|
||||
});
|
||||
}
|
||||
|
||||
getHeight(x: number, y: number): number {
|
||||
// 基础高度
|
||||
let height = this.fbm.noise2D(x, y);
|
||||
|
||||
// 添加山脉
|
||||
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
|
||||
|
||||
return (height + 1) * 0.5; // 归一化到 [0, 1]
|
||||
}
|
||||
|
||||
getBiome(x: number, y: number): string {
|
||||
const height = this.getHeight(x, y);
|
||||
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
|
||||
|
||||
if (height < 0.3) return 'water';
|
||||
if (height < 0.4) return 'beach';
|
||||
if (height > 0.8) return 'mountain';
|
||||
|
||||
if (moisture < 0.3) return 'desert';
|
||||
if (moisture > 0.7) return 'forest';
|
||||
return 'grassland';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 战利品系统
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom, createWeightedRandom, sample } from '@esengine/procgen';
|
||||
|
||||
interface LootItem {
|
||||
id: string;
|
||||
rarity: string;
|
||||
}
|
||||
|
||||
class LootSystem {
|
||||
private rng: SeededRandom;
|
||||
private raritySelector: WeightedRandom<string>;
|
||||
private lootTables: Map<string, LootItem[]> = new Map();
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
|
||||
this.raritySelector = createWeightedRandom([
|
||||
{ value: 'common', weight: 60 },
|
||||
{ value: 'uncommon', weight: 25 },
|
||||
{ value: 'rare', weight: 10 },
|
||||
{ value: 'legendary', weight: 5 }
|
||||
]);
|
||||
|
||||
// 初始化战利品表
|
||||
this.lootTables.set('common', [/* ... */]);
|
||||
this.lootTables.set('rare', [/* ... */]);
|
||||
// ...
|
||||
}
|
||||
|
||||
generateLoot(count: number): LootItem[] {
|
||||
const loot: LootItem[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const rarity = this.raritySelector.pick(this.rng);
|
||||
const table = this.lootTables.get(rarity)!;
|
||||
const item = pickOne(table, this.rng);
|
||||
loot.push(item);
|
||||
}
|
||||
|
||||
return loot;
|
||||
}
|
||||
|
||||
// 保证可重现
|
||||
setSeed(seed: number): void {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 程序化敌人放置
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom } from '@esengine/procgen';
|
||||
|
||||
class EnemySpawner {
|
||||
private rng: SeededRandom;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
|
||||
spawnEnemiesInArea(
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
radius: number,
|
||||
count: number
|
||||
): Array<{ x: number; y: number; type: string }> {
|
||||
const enemies: Array<{ x: number; y: number; type: string }> = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 在圆内生成位置
|
||||
const pos = this.rng.nextPointInCircle(radius);
|
||||
|
||||
// 随机选择敌人类型
|
||||
const type = this.rng.nextBool(0.2) ? 'elite' : 'normal';
|
||||
|
||||
enemies.push({
|
||||
x: centerX + pos.x,
|
||||
y: centerY + pos.y,
|
||||
type
|
||||
});
|
||||
}
|
||||
|
||||
return enemies;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 程序化关卡布局
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom, shuffle } from '@esengine/procgen';
|
||||
|
||||
interface Room {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
type: 'start' | 'combat' | 'treasure' | 'boss';
|
||||
}
|
||||
|
||||
class DungeonGenerator {
|
||||
private rng: SeededRandom;
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rng = createSeededRandom(seed);
|
||||
}
|
||||
|
||||
generate(roomCount: number): Room[] {
|
||||
const rooms: Room[] = [];
|
||||
|
||||
// 生成房间
|
||||
for (let i = 0; i < roomCount; i++) {
|
||||
rooms.push({
|
||||
x: this.rng.nextInt(0, 100),
|
||||
y: this.rng.nextInt(0, 100),
|
||||
width: this.rng.nextInt(5, 15),
|
||||
height: this.rng.nextInt(5, 15),
|
||||
type: 'combat'
|
||||
});
|
||||
}
|
||||
|
||||
// 随机分配特殊房间
|
||||
shuffle(rooms, this.rng);
|
||||
rooms[0].type = 'start';
|
||||
rooms[1].type = 'treasure';
|
||||
rooms[rooms.length - 1].type = 'boss';
|
||||
|
||||
return rooms;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
Procgen 模块提供了可视化脚本支持的蓝图节点:
|
||||
|
||||
### 噪声节点
|
||||
|
||||
- `SampleNoise2D` - 采样 2D 噪声
|
||||
- `SampleFBM` - 采样 FBM 噪声
|
||||
|
||||
### 随机节点
|
||||
|
||||
- `SeededRandom` - 生成随机浮点数
|
||||
- `SeededRandomInt` - 生成随机整数
|
||||
- `WeightedPick` - 加权随机选择
|
||||
- `ShuffleArray` - 洗牌数组
|
||||
- `PickRandom` - 随机选择元素
|
||||
- `SampleArray` - 采样数组
|
||||
- `RandomPointInCircle` - 圆内随机点
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用种子保证可重现性**
|
||||
```typescript
|
||||
// 保存种子以便重现相同结果
|
||||
const seed = Date.now();
|
||||
const rng = createSeededRandom(seed);
|
||||
saveSeed(seed);
|
||||
```
|
||||
|
||||
2. **预计算加权选择器**
|
||||
```typescript
|
||||
// 好:创建一次,多次使用
|
||||
const selector = createWeightedRandom(items);
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
selector.pick(rng);
|
||||
}
|
||||
|
||||
// 不好:每次都创建
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
weightedPick(items, rng);
|
||||
}
|
||||
```
|
||||
|
||||
3. **选择合适的噪声函数**
|
||||
- Perlin:平滑过渡的地形、云彩
|
||||
- Simplex:性能要求高的场景
|
||||
- Worley:细胞、石头纹理
|
||||
- FBM:需要多层细节的自然效果
|
||||
|
||||
4. **调整 FBM 参数**
|
||||
- `octaves`:越多细节越丰富,但性能开销越大
|
||||
- `persistence`:0.5 是常用值,越大高频细节越明显
|
||||
- `lacunarity`:通常为 2,控制频率增长速度
|
||||
- [噪声函数](./noise) - Perlin、Simplex、Worley、FBM
|
||||
- [种子随机数](./random) - SeededRandom API 和分布方法
|
||||
- [采样工具](./sampling) - 加权随机、洗牌、采样
|
||||
- [实际示例](./examples) - 地形、战利品、关卡生成
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user