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:
@@ -83,7 +83,19 @@ export default defineConfig({
|
|||||||
{ label: '最佳实践', slug: 'guide/system/best-practices', translations: { en: 'Best Practices' } },
|
{ label: '最佳实践', slug: 'guide/system/best-practices', translations: { en: 'Best Practices' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ label: '场景', slug: 'guide/scene', translations: { en: 'Scene' } },
|
{
|
||||||
|
label: '场景',
|
||||||
|
translations: { en: 'Scene' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'guide/scene', translations: { en: 'Overview' } },
|
||||||
|
{ label: '生命周期', slug: 'guide/scene/lifecycle', translations: { en: 'Lifecycle' } },
|
||||||
|
{ label: '实体管理', slug: 'guide/scene/entity-management', translations: { en: 'Entity Management' } },
|
||||||
|
{ label: '系统管理', slug: 'guide/scene/system-management', translations: { en: 'System Management' } },
|
||||||
|
{ label: '事件系统', slug: 'guide/scene/events', translations: { en: 'Events' } },
|
||||||
|
{ label: '调试与监控', slug: 'guide/scene/debugging', translations: { en: 'Debugging' } },
|
||||||
|
{ label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '序列化',
|
label: '序列化',
|
||||||
translations: { en: 'Serialization' },
|
translations: { en: 'Serialization' },
|
||||||
@@ -115,8 +127,30 @@ export default defineConfig({
|
|||||||
{ label: '高级用法', slug: 'guide/service-container/advanced', translations: { en: 'Advanced' } },
|
{ label: '高级用法', slug: 'guide/service-container/advanced', translations: { en: 'Advanced' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ label: '插件系统', slug: 'guide/plugin-system', translations: { en: 'Plugin System' } },
|
{
|
||||||
{ label: 'Worker 系统', slug: 'guide/worker-system', translations: { en: 'Worker System' } },
|
label: '插件系统',
|
||||||
|
translations: { en: 'Plugin System' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'guide/plugin-system', translations: { en: 'Overview' } },
|
||||||
|
{ label: '插件开发', slug: 'guide/plugin-system/development', translations: { en: 'Development' } },
|
||||||
|
{ label: '服务与系统', slug: 'guide/plugin-system/services-systems', translations: { en: 'Services & Systems' } },
|
||||||
|
{ label: '依赖管理', slug: 'guide/plugin-system/dependencies', translations: { en: 'Dependencies' } },
|
||||||
|
{ label: '插件管理', slug: 'guide/plugin-system/management', translations: { en: 'Management' } },
|
||||||
|
{ label: '示例插件', slug: 'guide/plugin-system/examples', translations: { en: 'Examples' } },
|
||||||
|
{ label: '最佳实践', slug: 'guide/plugin-system/best-practices', translations: { en: 'Best Practices' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Worker 系统',
|
||||||
|
translations: { en: 'Worker System' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'guide/worker-system', translations: { en: 'Overview' } },
|
||||||
|
{ label: '配置选项', slug: 'guide/worker-system/configuration', translations: { en: 'Configuration' } },
|
||||||
|
{ label: '完整示例', slug: 'guide/worker-system/examples', translations: { en: 'Examples' } },
|
||||||
|
{ label: '微信小游戏', slug: 'guide/worker-system/wechat', translations: { en: 'WeChat' } },
|
||||||
|
{ label: '最佳实践', slug: 'guide/worker-system/best-practices', translations: { en: 'Best Practices' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -134,13 +168,88 @@ export default defineConfig({
|
|||||||
translations: { en: 'Modules' },
|
translations: { en: 'Modules' },
|
||||||
items: [
|
items: [
|
||||||
{ label: '模块总览', slug: 'modules', translations: { en: 'Modules Overview' } },
|
{ label: '模块总览', slug: 'modules', translations: { en: 'Modules Overview' } },
|
||||||
{ label: '行为树', slug: 'modules/behavior-tree', translations: { en: 'Behavior Tree' } },
|
{
|
||||||
{ label: '状态机', slug: 'modules/fsm', translations: { en: 'FSM' } },
|
label: '行为树',
|
||||||
{ label: '定时器', slug: 'modules/timer', translations: { en: 'Timer' } },
|
translations: { en: 'Behavior Tree' },
|
||||||
{ label: '空间索引', slug: 'modules/spatial', translations: { en: 'Spatial' } },
|
items: [
|
||||||
{ label: '寻路', slug: 'modules/pathfinding', translations: { en: 'Pathfinding' } },
|
{ label: '概述', slug: 'modules/behavior-tree', translations: { en: 'Overview' } },
|
||||||
{ label: '蓝图', slug: 'modules/blueprint', translations: { en: 'Blueprint' } },
|
{ label: '快速开始', slug: 'modules/behavior-tree/getting-started', translations: { en: 'Getting Started' } },
|
||||||
{ label: '程序生成', slug: 'modules/procgen', translations: { en: 'Procgen' } },
|
{ label: '核心概念', slug: 'modules/behavior-tree/core-concepts', translations: { en: 'Core Concepts' } },
|
||||||
|
{ label: '编辑器指南', slug: 'modules/behavior-tree/editor-guide', translations: { en: 'Editor Guide' } },
|
||||||
|
{ label: '编辑器工作流', slug: 'modules/behavior-tree/editor-workflow', translations: { en: 'Editor Workflow' } },
|
||||||
|
{ label: '资产管理', slug: 'modules/behavior-tree/asset-management', translations: { en: 'Asset Management' } },
|
||||||
|
{ label: '自定义节点', slug: 'modules/behavior-tree/custom-actions', translations: { en: 'Custom Actions' } },
|
||||||
|
{ label: '高级用法', slug: 'modules/behavior-tree/advanced-usage', translations: { en: 'Advanced Usage' } },
|
||||||
|
{ label: '最佳实践', slug: 'modules/behavior-tree/best-practices', translations: { en: 'Best Practices' } },
|
||||||
|
{ label: 'Cocos 集成', slug: 'modules/behavior-tree/cocos-integration', translations: { en: 'Cocos Integration' } },
|
||||||
|
{ label: 'Laya 集成', slug: 'modules/behavior-tree/laya-integration', translations: { en: 'Laya Integration' } },
|
||||||
|
{ label: 'Node.js 使用', slug: 'modules/behavior-tree/nodejs-usage', translations: { en: 'Node.js Usage' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态机',
|
||||||
|
translations: { en: 'FSM' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/fsm', translations: { en: 'Overview' } },
|
||||||
|
{ label: 'API 参考', slug: 'modules/fsm/api', translations: { en: 'API Reference' } },
|
||||||
|
{ label: '实际示例', slug: 'modules/fsm/examples', translations: { en: 'Examples' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '定时器',
|
||||||
|
translations: { en: 'Timer' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/timer', translations: { en: 'Overview' } },
|
||||||
|
{ label: 'API 参考', slug: 'modules/timer/api', translations: { en: 'API Reference' } },
|
||||||
|
{ label: '实际示例', slug: 'modules/timer/examples', translations: { en: 'Examples' } },
|
||||||
|
{ label: '最佳实践', slug: 'modules/timer/best-practices', translations: { en: 'Best Practices' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '空间索引',
|
||||||
|
translations: { en: 'Spatial' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/spatial', translations: { en: 'Overview' } },
|
||||||
|
{ label: '空间索引 API', slug: 'modules/spatial/spatial-index', translations: { en: 'Spatial Index API' } },
|
||||||
|
{ label: 'AOI 兴趣区域', slug: 'modules/spatial/aoi', translations: { en: 'AOI' } },
|
||||||
|
{ label: '实际示例', slug: 'modules/spatial/examples', translations: { en: 'Examples' } },
|
||||||
|
{ label: '工具与优化', slug: 'modules/spatial/utilities', translations: { en: 'Utilities' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '寻路',
|
||||||
|
translations: { en: 'Pathfinding' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/pathfinding', translations: { en: 'Overview' } },
|
||||||
|
{ label: '网格地图 API', slug: 'modules/pathfinding/grid-map', translations: { en: 'Grid Map API' } },
|
||||||
|
{ label: '导航网格 API', slug: 'modules/pathfinding/navmesh', translations: { en: 'NavMesh API' } },
|
||||||
|
{ label: '路径平滑', slug: 'modules/pathfinding/smoothing', translations: { en: 'Path Smoothing' } },
|
||||||
|
{ label: '实际示例', slug: 'modules/pathfinding/examples', translations: { en: 'Examples' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '蓝图',
|
||||||
|
translations: { en: 'Blueprint' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
|
||||||
|
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
|
||||||
|
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
|
||||||
|
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
|
||||||
|
{ label: '蓝图组合', slug: 'modules/blueprint/composition', translations: { en: 'Composition' } },
|
||||||
|
{ label: '实际示例', slug: 'modules/blueprint/examples', translations: { en: 'Examples' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '程序生成',
|
||||||
|
translations: { en: 'Procgen' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/procgen', translations: { en: 'Overview' } },
|
||||||
|
{ label: '噪声函数', slug: 'modules/procgen/noise', translations: { en: 'Noise Functions' } },
|
||||||
|
{ label: '种子随机数', slug: 'modules/procgen/random', translations: { en: 'Seeded Random' } },
|
||||||
|
{ label: '采样工具', slug: 'modules/procgen/sampling', translations: { en: 'Sampling' } },
|
||||||
|
{ label: '实际示例', slug: 'modules/procgen/examples', translations: { en: 'Examples' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '网络',
|
label: '网络',
|
||||||
translations: { en: 'Network' },
|
translations: { en: 'Network' },
|
||||||
|
|||||||
@@ -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**
|
5. **Performance optimization**
|
||||||
- Pure nodes (`isPure: true`) cache outputs
|
- Pure nodes (`isPure: true`) cache outputs
|
||||||
- Avoid heavy computation in Tick
|
- 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
|
- `GetStateDuration` - Get state duration
|
||||||
- `EvaluateTransitions` - Evaluate transition conditions
|
- `EvaluateTransitions` - Evaluate transition conditions
|
||||||
- `ResetStateMachine` - Reset state machine
|
- `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 |
|
| Precision | Grid-aligned | Continuous coordinates |
|
||||||
| Dynamic Updates | Easy | Requires rebuild |
|
| Dynamic Updates | Easy | Requires rebuild |
|
||||||
| Setup Complexity | Simple | More complex |
|
| 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
|
- `octaves`: More = richer detail, higher cost
|
||||||
- `persistence`: 0.5 is common, higher = more high-frequency detail
|
- `persistence`: 0.5 is common, higher = more high-frequency detail
|
||||||
- `lacunarity`: Usually 2, controls frequency growth
|
- `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"
|
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.
|
`@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 |
|
| Direction | One-way query | Two-way tracking |
|
||||||
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
|
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
|
||||||
|
|
||||||
### IBounds
|
### Core Interfaces
|
||||||
|
|
||||||
|
#### IBounds
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface IBounds {
|
interface IBounds {
|
||||||
@@ -87,7 +90,7 @@ interface IBounds {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### IRaycastHit
|
#### IRaycastHit
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface IRaycastHit<T> {
|
interface IRaycastHit<T> {
|
||||||
@@ -98,227 +101,9 @@ interface IRaycastHit<T> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Spatial Index API
|
## Documentation
|
||||||
|
|
||||||
### createGridSpatialIndex
|
- [Spatial Index API](./spatial-index) - Grid index, range queries, raycasting
|
||||||
|
- [AOI (Area of Interest)](./aoi) - View management, enter/exit events
|
||||||
```typescript
|
- [Examples](./examples) - Area attacks, MMO sync, AI perception
|
||||||
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
|
- [Utilities & Optimization](./utilities) - Geometry detection, performance tips
|
||||||
```
|
|
||||||
|
|
||||||
**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));
|
|
||||||
```
|
|
||||||
|
|||||||
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"
|
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.
|
`@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
|
### TimerInfo
|
||||||
|
|
||||||
Timer information object:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface TimerInfo {
|
interface TimerInfo {
|
||||||
readonly id: string; // Timer ID
|
readonly id: string; // Timer ID
|
||||||
@@ -80,8 +79,6 @@ interface TimerInfo {
|
|||||||
|
|
||||||
### CooldownInfo
|
### CooldownInfo
|
||||||
|
|
||||||
Cooldown information object:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface CooldownInfo {
|
interface CooldownInfo {
|
||||||
readonly id: string; // Cooldown ID
|
readonly id: string; // Cooldown ID
|
||||||
@@ -92,263 +89,8 @@ interface CooldownInfo {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Reference
|
## Documentation
|
||||||
|
|
||||||
### createTimerService
|
- [API Reference](./api) - Complete timer and cooldown API
|
||||||
|
- [Examples](./examples) - Skill cooldowns, DOT effects, buff systems
|
||||||
```typescript
|
- [Best Practices](./best-practices) - Usage tips and ECS integration
|
||||||
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);
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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)"
|
title: "蓝图可视化脚本 (Blueprint)"
|
||||||
|
description: "完整的可视化脚本系统"
|
||||||
---
|
---
|
||||||
|
|
||||||
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。
|
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。
|
||||||
@@ -104,406 +105,10 @@ type VariableScope =
|
|||||||
| 'global'; // 全局共享
|
| 'global'; // 全局共享
|
||||||
```
|
```
|
||||||
|
|
||||||
## 虚拟机 API
|
## 文档导航
|
||||||
|
|
||||||
### BlueprintVM
|
- [虚拟机 API](./vm) - BlueprintVM 执行和上下文
|
||||||
|
- [自定义节点](./custom-nodes) - 创建自定义节点
|
||||||
蓝图虚拟机负责执行蓝图图:
|
- [内置节点](./nodes) - 内置节点参考
|
||||||
|
- [蓝图组合](./composition) - 片段和组合器
|
||||||
```typescript
|
- [实际示例](./examples) - ECS 集成和最佳实践
|
||||||
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 中执行重计算
|
|
||||||
|
|||||||
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)"
|
title: "状态机 (FSM)"
|
||||||
|
description: "类型安全的有限状态机实现"
|
||||||
---
|
---
|
||||||
|
|
||||||
`@esengine/fsm` 提供了一个类型安全的有限状态机实现,用于角色、AI 或任何需要状态管理的场景。
|
`@esengine/fsm` 提供了一个类型安全的有限状态机实现,用于角色、AI 或任何需要状态管理的场景。
|
||||||
@@ -91,249 +92,7 @@ fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
|
|||||||
// 如果同时满足,会先尝试 attack(优先级 10)
|
// 如果同时满足,会先尝试 attack(优先级 10)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 参考
|
## 文档导航
|
||||||
|
|
||||||
### createStateMachine
|
- [API 参考](./api) - 完整的状态机 API
|
||||||
|
- [实际示例](./examples) - 角色状态机、ECS 集成
|
||||||
```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` - 重置状态机
|
|
||||||
|
|||||||
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)"
|
title: "寻路系统 (Pathfinding)"
|
||||||
|
description: "完整的 2D 寻路解决方案"
|
||||||
---
|
---
|
||||||
|
|
||||||
`@esengine/pathfinding` 提供了完整的 2D 寻路解决方案,包括 A* 算法、网格地图、导航网格和路径平滑。
|
`@esengine/pathfinding` 提供了完整的 2D 寻路解决方案,包括 A* 算法、网格地图、导航网格和路径平滑。
|
||||||
@@ -67,29 +68,21 @@ const result = navmesh.findPath(1, 1, 18, 8);
|
|||||||
|
|
||||||
## 核心概念
|
## 核心概念
|
||||||
|
|
||||||
### IPoint - 坐标点
|
### 核心接口
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface IPoint {
|
interface IPoint {
|
||||||
readonly x: number;
|
readonly x: number;
|
||||||
readonly y: number;
|
readonly y: number;
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
### IPathResult - 寻路结果
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface IPathResult {
|
interface IPathResult {
|
||||||
readonly found: boolean; // 是否找到路径
|
readonly found: boolean; // 是否找到路径
|
||||||
readonly path: readonly IPoint[]; // 路径点列表
|
readonly path: readonly IPoint[]; // 路径点列表
|
||||||
readonly cost: number; // 路径总代价
|
readonly cost: number; // 路径总代价
|
||||||
readonly nodesSearched: number; // 搜索的节点数
|
readonly nodesSearched: number; // 搜索的节点数
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
### IPathfindingOptions - 寻路配置
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface IPathfindingOptions {
|
interface IPathfindingOptions {
|
||||||
maxNodes?: number; // 最大搜索节点数(默认 10000)
|
maxNodes?: number; // 最大搜索节点数(默认 10000)
|
||||||
heuristicWeight?: number; // 启发式权重(>1 更快但可能非最优)
|
heuristicWeight?: number; // 启发式权重(>1 更快但可能非最优)
|
||||||
@@ -98,402 +91,16 @@ interface IPathfindingOptions {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 启发式函数
|
### 启发式函数
|
||||||
|
|
||||||
模块提供了四种启发式函数:
|
|
||||||
|
|
||||||
| 函数 | 适用场景 | 说明 |
|
| 函数 | 适用场景 | 说明 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| `manhattanDistance` | 4方向移动 | 曼哈顿距离,只考虑水平/垂直 |
|
| `manhattanDistance` | 4方向移动 | 曼哈顿距离 |
|
||||||
| `euclideanDistance` | 任意方向 | 欧几里得距离,直线距离 |
|
| `euclideanDistance` | 任意方向 | 欧几里得距离 |
|
||||||
| `chebyshevDistance` | 8方向移动 | 切比雪夫距离,对角线代价为 1 |
|
| `chebyshevDistance` | 8方向移动 | 切比雪夫距离 |
|
||||||
| `octileDistance` | 8方向移动 | 八角距离,对角线代价为 √2(默认) |
|
| `octileDistance` | 8方向移动 | 八角距离(默认) |
|
||||||
|
|
||||||
```typescript
|
### 网格 vs 导航网格
|
||||||
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 导航网格
|
|
||||||
|
|
||||||
| 特性 | GridMap | NavMesh |
|
| 特性 | GridMap | NavMesh |
|
||||||
|------|---------|---------|
|
|------|---------|---------|
|
||||||
@@ -501,4 +108,10 @@ Pathfinding 模块提供了可视化脚本支持的蓝图节点:
|
|||||||
| 内存占用 | 较高 (width × height) | 较低 (多边形数) |
|
| 内存占用 | 较高 (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,控制频率增长速度
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user