From 8605888f1136dd0ddf91f36c6bf485dc35b150bb Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Sat, 27 Dec 2025 20:35:54 +0800 Subject: [PATCH] docs: restructure documentation with modular sub-pages (#363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- docs/astro.config.mjs | 129 ++- .../content/docs/en/guide/plugin-system.md | 645 --------------- .../en/guide/plugin-system/best-practices.md | 151 ++++ .../en/guide/plugin-system/dependencies.md | 106 +++ .../en/guide/plugin-system/development.md | 139 ++++ .../docs/en/guide/plugin-system/examples.md | 188 +++++ .../docs/en/guide/plugin-system/index.md | 85 ++ .../docs/en/guide/plugin-system/management.md | 93 +++ .../guide/plugin-system/services-systems.md | 133 +++ docs/src/content/docs/en/guide/scene.md | 368 --------- .../docs/en/guide/scene/best-practices.md | 179 ++++ .../content/docs/en/guide/scene/debugging.md | 124 +++ .../docs/en/guide/scene/entity-management.md | 125 +++ .../src/content/docs/en/guide/scene/events.md | 122 +++ docs/src/content/docs/en/guide/scene/index.md | 91 ++ .../content/docs/en/guide/scene/lifecycle.md | 103 +++ .../docs/en/guide/scene/system-management.md | 115 +++ .../content/docs/en/guide/worker-system.md | 572 ------------- .../en/guide/worker-system/best-practices.md | 44 + .../en/guide/worker-system/configuration.md | 61 ++ .../docs/en/guide/worker-system/examples.md | 39 + .../docs/en/guide/worker-system/index.md | 104 +++ .../docs/en/guide/worker-system/wechat.md | 43 + .../modules/behavior-tree/advanced-usage.md | 121 +++ .../modules/behavior-tree/asset-management.md | 94 +++ .../modules/behavior-tree/best-practices.md | 174 ++++ .../behavior-tree/cocos-integration.md | 147 ++++ .../en/modules/behavior-tree/core-concepts.md | 179 ++++ .../modules/behavior-tree/custom-actions.md | 197 +++++ .../en/modules/behavior-tree/editor-guide.md | 73 ++ .../modules/behavior-tree/editor-workflow.md | 88 ++ .../modules/behavior-tree/getting-started.md | 105 +++ .../docs/en/modules/behavior-tree/index.md | 95 +++ .../modules/behavior-tree/laya-integration.md | 118 +++ .../en/modules/behavior-tree/nodejs-usage.md | 183 +++++ .../docs/en/modules/blueprint/composition.md | 133 +++ .../docs/en/modules/blueprint/custom-nodes.md | 128 +++ .../docs/en/modules/blueprint/examples.md | 149 ++++ .../docs/en/modules/blueprint/index.md | 8 + .../docs/en/modules/blueprint/nodes.md | 107 +++ .../content/docs/en/modules/blueprint/vm.md | 133 +++ docs/src/content/docs/en/modules/fsm/api.md | 158 ++++ .../content/docs/en/modules/fsm/examples.md | 253 ++++++ docs/src/content/docs/en/modules/fsm/index.md | 5 + .../docs/en/modules/pathfinding/examples.md | 164 ++++ .../docs/en/modules/pathfinding/grid-map.md | 112 +++ .../docs/en/modules/pathfinding/index.md | 7 + .../docs/en/modules/pathfinding/navmesh.md | 67 ++ .../docs/en/modules/pathfinding/smoothing.md | 67 ++ .../docs/en/modules/procgen/examples.md | 230 ++++++ .../content/docs/en/modules/procgen/index.md | 7 + .../content/docs/en/modules/procgen/noise.md | 97 +++ .../content/docs/en/modules/procgen/random.md | 92 +++ .../docs/en/modules/procgen/sampling.md | 135 +++ .../content/docs/en/modules/spatial/aoi.md | 166 ++++ .../docs/en/modules/spatial/examples.md | 184 +++++ .../content/docs/en/modules/spatial/index.md | 235 +----- .../docs/en/modules/spatial/spatial-index.md | 159 ++++ .../docs/en/modules/spatial/utilities.md | 149 ++++ docs/src/content/docs/en/modules/timer/api.md | 218 +++++ .../docs/en/modules/timer/best-practices.md | 223 +++++ .../content/docs/en/modules/timer/examples.md | 235 ++++++ .../content/docs/en/modules/timer/index.md | 268 +----- docs/src/content/docs/guide/plugin-system.md | 645 --------------- .../guide/plugin-system/best-practices.md | 151 ++++ .../docs/guide/plugin-system/dependencies.md | 106 +++ .../docs/guide/plugin-system/development.md | 139 ++++ .../docs/guide/plugin-system/examples.md | 188 +++++ .../content/docs/guide/plugin-system/index.md | 85 ++ .../docs/guide/plugin-system/management.md | 93 +++ .../guide/plugin-system/services-systems.md | 133 +++ docs/src/content/docs/guide/scene.md | 666 --------------- .../docs/guide/scene/best-practices.md | 179 ++++ .../src/content/docs/guide/scene/debugging.md | 124 +++ .../docs/guide/scene/entity-management.md | 125 +++ docs/src/content/docs/guide/scene/events.md | 122 +++ docs/src/content/docs/guide/scene/index.md | 91 ++ .../src/content/docs/guide/scene/lifecycle.md | 103 +++ .../docs/guide/scene/system-management.md | 115 +++ docs/src/content/docs/guide/worker-system.md | 774 ------------------ .../guide/worker-system/best-practices.md | 120 +++ .../docs/guide/worker-system/configuration.md | 130 +++ .../docs/guide/worker-system/examples.md | 190 +++++ .../content/docs/guide/worker-system/index.md | 105 +++ .../docs/guide/worker-system/wechat.md | 138 ++++ .../docs/modules/blueprint/composition.md | 133 +++ .../docs/modules/blueprint/custom-nodes.md | 128 +++ .../docs/modules/blueprint/examples.md | 168 ++++ .../content/docs/modules/blueprint/index.md | 409 +-------- .../content/docs/modules/blueprint/nodes.md | 107 +++ docs/src/content/docs/modules/blueprint/vm.md | 133 +++ docs/src/content/docs/modules/fsm/api.md | 135 +++ docs/src/content/docs/modules/fsm/examples.md | 230 ++++++ docs/src/content/docs/modules/fsm/index.md | 249 +----- .../docs/modules/pathfinding/examples.md | 164 ++++ .../docs/modules/pathfinding/grid-map.md | 112 +++ .../content/docs/modules/pathfinding/index.md | 417 +--------- .../docs/modules/pathfinding/navmesh.md | 67 ++ .../docs/modules/pathfinding/smoothing.md | 67 ++ .../content/docs/modules/procgen/examples.md | 230 ++++++ .../src/content/docs/modules/procgen/index.md | 495 +---------- .../src/content/docs/modules/procgen/noise.md | 97 +++ .../content/docs/modules/procgen/random.md | 92 +++ .../content/docs/modules/procgen/sampling.md | 135 +++ docs/src/content/docs/modules/spatial/aoi.md | 166 ++++ .../content/docs/modules/spatial/examples.md | 184 +++++ .../src/content/docs/modules/spatial/index.md | 513 +----------- .../docs/modules/spatial/spatial-index.md | 159 ++++ .../content/docs/modules/spatial/utilities.md | 149 ++++ docs/src/content/docs/modules/timer/api.md | 221 +++++ .../docs/modules/timer/best-practices.md | 223 +++++ .../content/docs/modules/timer/examples.md | 235 ++++++ docs/src/content/docs/modules/timer/index.md | 394 +-------- 113 files changed, 12958 insertions(+), 6598 deletions(-) delete mode 100644 docs/src/content/docs/en/guide/plugin-system.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/best-practices.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/dependencies.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/development.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/examples.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/index.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/management.md create mode 100644 docs/src/content/docs/en/guide/plugin-system/services-systems.md delete mode 100644 docs/src/content/docs/en/guide/scene.md create mode 100644 docs/src/content/docs/en/guide/scene/best-practices.md create mode 100644 docs/src/content/docs/en/guide/scene/debugging.md create mode 100644 docs/src/content/docs/en/guide/scene/entity-management.md create mode 100644 docs/src/content/docs/en/guide/scene/events.md create mode 100644 docs/src/content/docs/en/guide/scene/index.md create mode 100644 docs/src/content/docs/en/guide/scene/lifecycle.md create mode 100644 docs/src/content/docs/en/guide/scene/system-management.md delete mode 100644 docs/src/content/docs/en/guide/worker-system.md create mode 100644 docs/src/content/docs/en/guide/worker-system/best-practices.md create mode 100644 docs/src/content/docs/en/guide/worker-system/configuration.md create mode 100644 docs/src/content/docs/en/guide/worker-system/examples.md create mode 100644 docs/src/content/docs/en/guide/worker-system/index.md create mode 100644 docs/src/content/docs/en/guide/worker-system/wechat.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/advanced-usage.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/asset-management.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/best-practices.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/cocos-integration.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/core-concepts.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/custom-actions.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/editor-guide.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/editor-workflow.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/getting-started.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/index.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/laya-integration.md create mode 100644 docs/src/content/docs/en/modules/behavior-tree/nodejs-usage.md create mode 100644 docs/src/content/docs/en/modules/blueprint/composition.md create mode 100644 docs/src/content/docs/en/modules/blueprint/custom-nodes.md create mode 100644 docs/src/content/docs/en/modules/blueprint/examples.md create mode 100644 docs/src/content/docs/en/modules/blueprint/nodes.md create mode 100644 docs/src/content/docs/en/modules/blueprint/vm.md create mode 100644 docs/src/content/docs/en/modules/fsm/api.md create mode 100644 docs/src/content/docs/en/modules/fsm/examples.md create mode 100644 docs/src/content/docs/en/modules/pathfinding/examples.md create mode 100644 docs/src/content/docs/en/modules/pathfinding/grid-map.md create mode 100644 docs/src/content/docs/en/modules/pathfinding/navmesh.md create mode 100644 docs/src/content/docs/en/modules/pathfinding/smoothing.md create mode 100644 docs/src/content/docs/en/modules/procgen/examples.md create mode 100644 docs/src/content/docs/en/modules/procgen/noise.md create mode 100644 docs/src/content/docs/en/modules/procgen/random.md create mode 100644 docs/src/content/docs/en/modules/procgen/sampling.md create mode 100644 docs/src/content/docs/en/modules/spatial/aoi.md create mode 100644 docs/src/content/docs/en/modules/spatial/examples.md create mode 100644 docs/src/content/docs/en/modules/spatial/spatial-index.md create mode 100644 docs/src/content/docs/en/modules/spatial/utilities.md create mode 100644 docs/src/content/docs/en/modules/timer/api.md create mode 100644 docs/src/content/docs/en/modules/timer/best-practices.md create mode 100644 docs/src/content/docs/en/modules/timer/examples.md delete mode 100644 docs/src/content/docs/guide/plugin-system.md create mode 100644 docs/src/content/docs/guide/plugin-system/best-practices.md create mode 100644 docs/src/content/docs/guide/plugin-system/dependencies.md create mode 100644 docs/src/content/docs/guide/plugin-system/development.md create mode 100644 docs/src/content/docs/guide/plugin-system/examples.md create mode 100644 docs/src/content/docs/guide/plugin-system/index.md create mode 100644 docs/src/content/docs/guide/plugin-system/management.md create mode 100644 docs/src/content/docs/guide/plugin-system/services-systems.md delete mode 100644 docs/src/content/docs/guide/scene.md create mode 100644 docs/src/content/docs/guide/scene/best-practices.md create mode 100644 docs/src/content/docs/guide/scene/debugging.md create mode 100644 docs/src/content/docs/guide/scene/entity-management.md create mode 100644 docs/src/content/docs/guide/scene/events.md create mode 100644 docs/src/content/docs/guide/scene/index.md create mode 100644 docs/src/content/docs/guide/scene/lifecycle.md create mode 100644 docs/src/content/docs/guide/scene/system-management.md delete mode 100644 docs/src/content/docs/guide/worker-system.md create mode 100644 docs/src/content/docs/guide/worker-system/best-practices.md create mode 100644 docs/src/content/docs/guide/worker-system/configuration.md create mode 100644 docs/src/content/docs/guide/worker-system/examples.md create mode 100644 docs/src/content/docs/guide/worker-system/index.md create mode 100644 docs/src/content/docs/guide/worker-system/wechat.md create mode 100644 docs/src/content/docs/modules/blueprint/composition.md create mode 100644 docs/src/content/docs/modules/blueprint/custom-nodes.md create mode 100644 docs/src/content/docs/modules/blueprint/examples.md create mode 100644 docs/src/content/docs/modules/blueprint/nodes.md create mode 100644 docs/src/content/docs/modules/blueprint/vm.md create mode 100644 docs/src/content/docs/modules/fsm/api.md create mode 100644 docs/src/content/docs/modules/fsm/examples.md create mode 100644 docs/src/content/docs/modules/pathfinding/examples.md create mode 100644 docs/src/content/docs/modules/pathfinding/grid-map.md create mode 100644 docs/src/content/docs/modules/pathfinding/navmesh.md create mode 100644 docs/src/content/docs/modules/pathfinding/smoothing.md create mode 100644 docs/src/content/docs/modules/procgen/examples.md create mode 100644 docs/src/content/docs/modules/procgen/noise.md create mode 100644 docs/src/content/docs/modules/procgen/random.md create mode 100644 docs/src/content/docs/modules/procgen/sampling.md create mode 100644 docs/src/content/docs/modules/spatial/aoi.md create mode 100644 docs/src/content/docs/modules/spatial/examples.md create mode 100644 docs/src/content/docs/modules/spatial/spatial-index.md create mode 100644 docs/src/content/docs/modules/spatial/utilities.md create mode 100644 docs/src/content/docs/modules/timer/api.md create mode 100644 docs/src/content/docs/modules/timer/best-practices.md create mode 100644 docs/src/content/docs/modules/timer/examples.md diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index fb408046..382f20b7 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -83,7 +83,19 @@ export default defineConfig({ { 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: '序列化', translations: { en: 'Serialization' }, @@ -115,8 +127,30 @@ export default defineConfig({ { 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' }, items: [ { label: '模块总览', slug: 'modules', translations: { en: 'Modules Overview' } }, - { label: '行为树', slug: 'modules/behavior-tree', translations: { en: 'Behavior Tree' } }, - { label: '状态机', slug: 'modules/fsm', translations: { en: 'FSM' } }, - { label: '定时器', slug: 'modules/timer', translations: { en: 'Timer' } }, - { label: '空间索引', slug: 'modules/spatial', translations: { en: 'Spatial' } }, - { label: '寻路', slug: 'modules/pathfinding', translations: { en: 'Pathfinding' } }, - { label: '蓝图', slug: 'modules/blueprint', translations: { en: 'Blueprint' } }, - { label: '程序生成', slug: 'modules/procgen', translations: { en: 'Procgen' } }, + { + label: '行为树', + translations: { en: 'Behavior Tree' }, + items: [ + { label: '概述', slug: 'modules/behavior-tree', translations: { en: 'Overview' } }, + { label: '快速开始', slug: 'modules/behavior-tree/getting-started', translations: { en: 'Getting Started' } }, + { 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: '网络', translations: { en: 'Network' }, diff --git a/docs/src/content/docs/en/guide/plugin-system.md b/docs/src/content/docs/en/guide/plugin-system.md deleted file mode 100644 index aad0eccc..00000000 --- a/docs/src/content/docs/en/guide/plugin-system.md +++ /dev/null @@ -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; - - // Called when plugin is uninstalled - uninstall(): void | Promise; -} -``` - -### 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 { - // 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 { - // 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 { - 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 { - 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 diff --git a/docs/src/content/docs/en/guide/plugin-system/best-practices.md b/docs/src/content/docs/en/guide/plugin-system/best-practices.md new file mode 100644 index 00000000..7b85ec2f --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/best-practices.md @@ -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 { + 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 { + 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 | diff --git a/docs/src/content/docs/en/guide/plugin-system/dependencies.md b/docs/src/content/docs/en/guide/plugin-system/dependencies.md new file mode 100644 index 00000000..0a1b5981 --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/dependencies.md @@ -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 {} +} +``` diff --git a/docs/src/content/docs/en/guide/plugin-system/development.md b/docs/src/content/docs/en/guide/plugin-system/development.md new file mode 100644 index 00000000..1ac2d6f6 --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/development.md @@ -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; + + // Called when plugin is uninstalled + uninstall(): void | Promise; +} +``` + +## 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 { + // 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 { + // 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 +``` diff --git a/docs/src/content/docs/en/guide/plugin-system/examples.md b/docs/src/content/docs/en/guide/plugin-system/examples.md new file mode 100644 index 00000000..c5939682 --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/examples.md @@ -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; + } + } +} +``` diff --git a/docs/src/content/docs/en/guide/plugin-system/index.md b/docs/src/content/docs/en/guide/plugin-system/index.md new file mode 100644 index 00000000..1788a631 --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/index.md @@ -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 diff --git a/docs/src/content/docs/en/guide/plugin-system/management.md b/docs/src/content/docs/en/guide/plugin-system/management.md new file mode 100644 index 00000000..fc10d543 --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/management.md @@ -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; +} +``` diff --git a/docs/src/content/docs/en/guide/plugin-system/services-systems.md b/docs/src/content/docs/en/guide/plugin-system/services-systems.md new file mode 100644 index 00000000..686ff30c --- /dev/null +++ b/docs/src/content/docs/en/guide/plugin-system/services-systems.md @@ -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 = []; + } +} +``` diff --git a/docs/src/content/docs/en/guide/scene.md b/docs/src/content/docs/en/guide/scene.md deleted file mode 100644 index baa4b090..00000000 --- a/docs/src/content/docs/en/guide/scene.md +++ /dev/null @@ -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 = new Map(); - private sounds: Map = 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. diff --git a/docs/src/content/docs/en/guide/scene/best-practices.md b/docs/src/content/docs/en/guide/scene/best-practices.md new file mode 100644 index 00000000..97b272d1 --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/best-practices.md @@ -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 = new Map(); + private sounds: Map = 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 | diff --git a/docs/src/content/docs/en/guide/scene/debugging.md b/docs/src/content/docs/en/guide/scene/debugging.md new file mode 100644 index 00000000..af6a896c --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/debugging.md @@ -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 diff --git a/docs/src/content/docs/en/guide/scene/entity-management.md b/docs/src/content/docs/en/guide/scene/entity-management.md new file mode 100644 index 00000000..7298611d --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/entity-management.md @@ -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 | diff --git a/docs/src/content/docs/en/guide/scene/events.md b/docs/src/content/docs/en/guide/scene/events.md new file mode 100644 index 00000000..9439d492 --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/events.md @@ -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` | diff --git a/docs/src/content/docs/en/guide/scene/index.md b/docs/src/content/docs/en/guide/scene/index.md new file mode 100644 index 00000000..723f68fb --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/index.md @@ -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 diff --git a/docs/src/content/docs/en/guide/scene/lifecycle.md b/docs/src/content/docs/en/guide/scene/lifecycle.md new file mode 100644 index 00000000..cefa5029 --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/lifecycle.md @@ -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 diff --git a/docs/src/content/docs/en/guide/scene/system-management.md b/docs/src/content/docs/en/guide/scene/system-management.md new file mode 100644 index 00000000..3acec6e2 --- /dev/null +++ b/docs/src/content/docs/en/guide/scene/system-management.md @@ -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 | diff --git a/docs/src/content/docs/en/guide/worker-system.md b/docs/src/content/docs/en/guide/worker-system.md deleted file mode 100644 index 95ff1527..00000000 --- a/docs/src/content/docs/en/guide/worker-system.md +++ /dev/null @@ -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 { - 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 { - 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. diff --git a/docs/src/content/docs/en/guide/worker-system/best-practices.md b/docs/src/content/docs/en/guide/worker-system/best-practices.md new file mode 100644 index 00000000..a9d7c2a3 --- /dev/null +++ b/docs/src/content/docs/en/guide/worker-system/best-practices.md @@ -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 diff --git a/docs/src/content/docs/en/guide/worker-system/configuration.md b/docs/src/content/docs/en/guide/worker-system/configuration.md new file mode 100644 index 00000000..67ceb817 --- /dev/null +++ b/docs/src/content/docs/en/guide/worker-system/configuration.md @@ -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, ... } +``` diff --git a/docs/src/content/docs/en/guide/worker-system/examples.md b/docs/src/content/docs/en/guide/worker-system/examples.md new file mode 100644 index 00000000..ae57047f --- /dev/null +++ b/docs/src/content/docs/en/guide/worker-system/examples.md @@ -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 { + 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... +} +``` diff --git a/docs/src/content/docs/en/guide/worker-system/index.md b/docs/src/content/docs/en/guide/worker-system/index.md new file mode 100644 index 00000000..f7edbe94 --- /dev/null +++ b/docs/src/content/docs/en/guide/worker-system/index.md @@ -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 { + 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/) diff --git a/docs/src/content/docs/en/guide/worker-system/wechat.md b/docs/src/content/docs/en/guide/worker-system/wechat.md new file mode 100644 index 00000000..8310331f --- /dev/null +++ b/docs/src/content/docs/en/guide/worker-system/wechat.md @@ -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 { + 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` diff --git a/docs/src/content/docs/en/modules/behavior-tree/advanced-usage.md b/docs/src/content/docs/en/modules/behavior-tree/advanced-usage.md new file mode 100644 index 00000000..0240b0a7 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/advanced-usage.md @@ -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); + } +} +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/asset-management.md b/docs/src/content/docs/en/modules/behavior-tree/asset-management.md new file mode 100644 index 00000000..63d7f799 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/asset-management.md @@ -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('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 +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/best-practices.md b/docs/src/content/docs/en/modules/behavior-tree/best-practices.md new file mode 100644 index 00000000..3ca5228d --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/best-practices.md @@ -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() +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/cocos-integration.md b/docs/src/content/docs/en/modules/behavior-tree/cocos-integration.md new file mode 100644 index 00000000..525a93af --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/cocos-integration.md @@ -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('ccNode'); + const target = context.runtime.getBlackboardValue('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(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 { + 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 diff --git a/docs/src/content/docs/en/modules/behavior-tree/core-concepts.md b/docs/src/content/docs/en/modules/behavior-tree/core-concepts.md new file mode 100644 index 00000000..bee180bc --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/core-concepts.md @@ -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 +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md b/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md new file mode 100644 index 00000000..0c231556 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/custom-actions.md @@ -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(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(context, 'speed', 100); + + // Get bound value from blackboard + const target = BindingHelper.getBoundValue(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(context, 'duration', 1000); + + // Get or initialize state + let elapsed = context.runtime.getNodeState(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(context, 'threshold', 30); + const health = context.runtime.getBlackboardValue('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'); +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/editor-guide.md b/docs/src/content/docs/en/modules/behavior-tree/editor-guide.md new file mode 100644 index 00000000..7b0e67e0 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/editor-guide.md @@ -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 | diff --git a/docs/src/content/docs/en/modules/behavior-tree/editor-workflow.md b/docs/src/content/docs/en/modules/behavior-tree/editor-workflow.md new file mode 100644 index 00000000..ecf7bf74 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/editor-workflow.md @@ -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); +``` diff --git a/docs/src/content/docs/en/modules/behavior-tree/getting-started.md b/docs/src/content/docs/en/modules/behavior-tree/getting-started.md new file mode 100644 index 00000000..df4f3750 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/getting-started.md @@ -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 diff --git a/docs/src/content/docs/en/modules/behavior-tree/index.md b/docs/src/content/docs/en/modules/behavior-tree/index.md new file mode 100644 index 00000000..58dbfd33 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/index.md @@ -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 diff --git a/docs/src/content/docs/en/modules/behavior-tree/laya-integration.md b/docs/src/content/docs/en/modules/behavior-tree/laya-integration.md new file mode 100644 index 00000000..b839b6db --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/laya-integration.md @@ -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('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(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 diff --git a/docs/src/content/docs/en/modules/behavior-tree/nodejs-usage.md b/docs/src/content/docs/en/modules/behavior-tree/nodejs-usage.md new file mode 100644 index 00000000..f6a54428 --- /dev/null +++ b/docs/src/content/docs/en/modules/behavior-tree/nodejs-usage.md @@ -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 diff --git a/docs/src/content/docs/en/modules/blueprint/composition.md b/docs/src/content/docs/en/modules/blueprint/composition.md new file mode 100644 index 00000000..45cec33c --- /dev/null +++ b/docs/src/content/docs/en/modules/blueprint/composition.md @@ -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 diff --git a/docs/src/content/docs/en/modules/blueprint/custom-nodes.md b/docs/src/content/docs/en/modules/blueprint/custom-nodes.md new file mode 100644 index 00000000..fba9501b --- /dev/null +++ b/docs/src/content/docs/en/modules/blueprint/custom-nodes.md @@ -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(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 } }; + } +} +``` diff --git a/docs/src/content/docs/en/modules/blueprint/examples.md b/docs/src/content/docs/en/modules/blueprint/examples.md new file mode 100644 index 00000000..bb7d2274 --- /dev/null +++ b/docs/src/content/docs/en/modules/blueprint/examples.md @@ -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(node.id, 'target'); + const amount = context.getInput(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 +``` diff --git a/docs/src/content/docs/en/modules/blueprint/index.md b/docs/src/content/docs/en/modules/blueprint/index.md index e0a1e6b5..3210b7ca 100644 --- a/docs/src/content/docs/en/modules/blueprint/index.md +++ b/docs/src/content/docs/en/modules/blueprint/index.md @@ -404,3 +404,11 @@ async function loadBlueprint(path: string): Promise { 5. **Performance optimization** - Pure nodes (`isPure: true`) cache outputs - Avoid heavy computation in Tick + +## Documentation + +- [Virtual Machine API](./vm) - BlueprintVM execution and context +- [Custom Nodes](./custom-nodes) - Creating custom nodes +- [Built-in Nodes](./nodes) - Built-in node reference +- [Blueprint Composition](./composition) - Fragments and composer +- [Examples](./examples) - ECS integration and best practices diff --git a/docs/src/content/docs/en/modules/blueprint/nodes.md b/docs/src/content/docs/en/modules/blueprint/nodes.md new file mode 100644 index 00000000..af087b6e --- /dev/null +++ b/docs/src/content/docs/en/modules/blueprint/nodes.md @@ -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 | diff --git a/docs/src/content/docs/en/modules/blueprint/vm.md b/docs/src/content/docs/en/modules/blueprint/vm.md new file mode 100644 index 00000000..52545d27 --- /dev/null +++ b/docs/src/content/docs/en/modules/blueprint/vm.md @@ -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(nodeId: string, pinName: string): T; + + // Set output value + setOutput(nodeId: string, pinName: string, value: unknown): void; + + // Variable access + getVariable(name: string): T; + setVariable(name: string, value: unknown): void; +} +``` + +## Execution Result + +```typescript +interface ExecutionResult { + outputs?: Record; // 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 { + const json = await fs.readFile(path, 'utf-8'); + const asset = JSON.parse(json); + + if (!validateBlueprintAsset(asset)) { + throw new Error('Invalid blueprint file'); + } + + return asset; +} +``` diff --git a/docs/src/content/docs/en/modules/fsm/api.md b/docs/src/content/docs/en/modules/fsm/api.md new file mode 100644 index 00000000..26c77f68 --- /dev/null +++ b/docs/src/content/docs/en/modules/fsm/api.md @@ -0,0 +1,158 @@ +--- +title: "API Reference" +description: "Complete FSM API documentation" +--- + +## createStateMachine + +```typescript +function createStateMachine( + config: StateMachineConfig +): StateMachine +``` + +### Configuration + +```typescript +interface StateMachineConfig { + initial: string; + context?: TContext; + states: Record>; + 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 { + // 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; +} +``` + +## 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({ + 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})`); + } +}); +``` diff --git a/docs/src/content/docs/en/modules/fsm/examples.md b/docs/src/content/docs/en/modules/fsm/examples.md new file mode 100644 index 00000000..ce2961dd --- /dev/null +++ b/docs/src/content/docs/en/modules/fsm/examples.md @@ -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({ + 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; +} + +// 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({ + 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' + } + } + } +}); +``` diff --git a/docs/src/content/docs/en/modules/fsm/index.md b/docs/src/content/docs/en/modules/fsm/index.md index f6793896..264a18aa 100644 --- a/docs/src/content/docs/en/modules/fsm/index.md +++ b/docs/src/content/docs/en/modules/fsm/index.md @@ -316,3 +316,8 @@ The FSM module provides blueprint nodes for visual scripting: - `GetStateDuration` - Get state duration - `EvaluateTransitions` - Evaluate transition conditions - `ResetStateMachine` - Reset state machine + +## Documentation + +- [API Reference](./api) - Complete API documentation +- [Examples](./examples) - Character FSM, AI behavior, ECS integration diff --git a/docs/src/content/docs/en/modules/pathfinding/examples.md b/docs/src/content/docs/en/modules/pathfinding/examples.md new file mode 100644 index 00000000..8880c548 --- /dev/null +++ b/docs/src/content/docs/en/modules/pathfinding/examples.md @@ -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 = 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) diff --git a/docs/src/content/docs/en/modules/pathfinding/grid-map.md b/docs/src/content/docs/en/modules/pathfinding/grid-map.md new file mode 100644 index 00000000..2cbf381c --- /dev/null +++ b/docs/src/content/docs/en/modules/pathfinding/grid-map.md @@ -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 +}); +``` diff --git a/docs/src/content/docs/en/modules/pathfinding/index.md b/docs/src/content/docs/en/modules/pathfinding/index.md index a1422f4b..2b0b7514 100644 --- a/docs/src/content/docs/en/modules/pathfinding/index.md +++ b/docs/src/content/docs/en/modules/pathfinding/index.md @@ -299,3 +299,10 @@ for (let y = 30; y < 35; y++) { | Precision | Grid-aligned | Continuous coordinates | | Dynamic Updates | Easy | Requires rebuild | | Setup Complexity | Simple | More complex | + +## Documentation + +- [Grid Map API](./grid-map) - Grid operations and A* pathfinder +- [Navigation Mesh API](./navmesh) - NavMesh building and querying +- [Path Smoothing](./smoothing) - Line of sight and curve smoothing +- [Examples](./examples) - Game movement, dynamic obstacles, hierarchical pathfinding diff --git a/docs/src/content/docs/en/modules/pathfinding/navmesh.md b/docs/src/content/docs/en/modules/pathfinding/navmesh.md new file mode 100644 index 00000000..aaa66ced --- /dev/null +++ b/docs/src/content/docs/en/modules/pathfinding/navmesh.md @@ -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(); +``` diff --git a/docs/src/content/docs/en/modules/pathfinding/smoothing.md b/docs/src/content/docs/en/modules/pathfinding/smoothing.md new file mode 100644 index 00000000..8a56700f --- /dev/null +++ b/docs/src/content/docs/en/modules/pathfinding/smoothing.md @@ -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 diff --git a/docs/src/content/docs/en/modules/procgen/examples.md b/docs/src/content/docs/en/modules/procgen/examples.md new file mode 100644 index 00000000..ce62a8fb --- /dev/null +++ b/docs/src/content/docs/en/modules/procgen/examples.md @@ -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; + private lootTables: Map = 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 diff --git a/docs/src/content/docs/en/modules/procgen/index.md b/docs/src/content/docs/en/modules/procgen/index.md index 1b923a08..1bb91641 100644 --- a/docs/src/content/docs/en/modules/procgen/index.md +++ b/docs/src/content/docs/en/modules/procgen/index.md @@ -396,3 +396,10 @@ class LootSystem { - `octaves`: More = richer detail, higher cost - `persistence`: 0.5 is common, higher = more high-frequency detail - `lacunarity`: Usually 2, controls frequency growth + +## Documentation + +- [Noise Functions](./noise) - Perlin, Simplex, Worley, FBM +- [Seeded Random](./random) - SeededRandom API and distribution methods +- [Sampling Utilities](./sampling) - Weighted random, shuffle, sampling +- [Examples](./examples) - Terrain, loot, level generation diff --git a/docs/src/content/docs/en/modules/procgen/noise.md b/docs/src/content/docs/en/modules/procgen/noise.md new file mode 100644 index 00000000..069f52a8 --- /dev/null +++ b/docs/src/content/docs/en/modules/procgen/noise.md @@ -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 | diff --git a/docs/src/content/docs/en/modules/procgen/random.md b/docs/src/content/docs/en/modules/procgen/random.md new file mode 100644 index 00000000..a9a62666 --- /dev/null +++ b/docs/src/content/docs/en/modules/procgen/random.md @@ -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 +``` diff --git a/docs/src/content/docs/en/modules/procgen/sampling.md b/docs/src/content/docs/en/modules/procgen/sampling.md new file mode 100644 index 00000000..974f855a --- /dev/null +++ b/docs/src/content/docs/en/modules/procgen/sampling.md @@ -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); +} +``` diff --git a/docs/src/content/docs/en/modules/spatial/aoi.md b/docs/src/content/docs/en/modules/spatial/aoi.md new file mode 100644 index 00000000..013f8a2d --- /dev/null +++ b/docs/src/content/docs/en/modules/spatial/aoi.md @@ -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(cellSize?: number): GridAOI +``` + +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 { + 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); +``` diff --git a/docs/src/content/docs/en/modules/spatial/examples.md b/docs/src/content/docs/en/modules/spatial/examples.md new file mode 100644 index 00000000..cb3def19 --- /dev/null +++ b/docs/src/content/docs/en/modules/spatial/examples.md @@ -0,0 +1,184 @@ +--- +title: "Examples" +description: "Area attacks, MMO sync, AI perception and more" +--- + +## Area Attack Detection + +```typescript +class CombatSystem { + private spatialIndex: ISpatialIndex; + + 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; + + constructor() { + this.aoi = createGridAOI(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; + + constructor() { + this.aoi = createGridAOI(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; + + // 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; + + 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); + } +} +``` diff --git a/docs/src/content/docs/en/modules/spatial/index.md b/docs/src/content/docs/en/modules/spatial/index.md index 3df4752f..2edd156e 100644 --- a/docs/src/content/docs/en/modules/spatial/index.md +++ b/docs/src/content/docs/en/modules/spatial/index.md @@ -1,5 +1,6 @@ --- title: "Spatial Index System" +description: "Efficient spatial queries and AOI management" --- `@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management. @@ -76,7 +77,9 @@ const visible = aoi.getEntitiesInView(player); | Direction | One-way query | Two-way tracking | | Use Cases | Collision, range attacks | MMO sync, NPC AI perception | -### IBounds +### Core Interfaces + +#### IBounds ```typescript interface IBounds { @@ -87,7 +90,7 @@ interface IBounds { } ``` -### IRaycastHit +#### IRaycastHit ```typescript interface IRaycastHit { @@ -98,227 +101,9 @@ interface IRaycastHit { } ``` -## Spatial Index API +## Documentation -### createGridSpatialIndex - -```typescript -function createGridSpatialIndex(cellSize?: number): GridSpatialIndex -``` - -**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(cellSize?: number): GridAOI -``` - -### 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; - - 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; - - constructor() { - this.aoi = createGridAOI(100); - - this.aoi.addListener((event) => { - const packet = this.createSyncPacket(event); - this.sendToPlayer(event.observer, packet); - }); - } - - onPlayerMove(player: Player, newPosition: IVector2): void { - this.aoi.updatePosition(player, newPosition); - } -} -``` - -## Blueprint Nodes - -### Spatial Query Nodes -- `FindInRadius`, `FindInRect`, `FindNearest`, `FindKNearest` -- `Raycast`, `RaycastFirst` - -### AOI Nodes -- `GetEntitiesInView`, `GetObserversOf`, `CanSee` -- `OnEntityEnterView`, `OnEntityExitView` - -## Service Tokens - -```typescript -import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial'; - -services.register(SpatialIndexToken, createGridSpatialIndex(100)); -services.register(AOIManagerToken, createGridAOI(100)); -``` +- [Spatial Index API](./spatial-index) - Grid index, range queries, raycasting +- [AOI (Area of Interest)](./aoi) - View management, enter/exit events +- [Examples](./examples) - Area attacks, MMO sync, AI perception +- [Utilities & Optimization](./utilities) - Geometry detection, performance tips diff --git a/docs/src/content/docs/en/modules/spatial/spatial-index.md b/docs/src/content/docs/en/modules/spatial/spatial-index.md new file mode 100644 index 00000000..35f6224b --- /dev/null +++ b/docs/src/content/docs/en/modules/spatial/spatial-index.md @@ -0,0 +1,159 @@ +--- +title: "Spatial Index API" +description: "Grid index, range queries, raycasting" +--- + +## createGridSpatialIndex + +```typescript +function createGridSpatialIndex(cellSize?: number): GridSpatialIndex +``` + +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) diff --git a/docs/src/content/docs/en/modules/spatial/utilities.md b/docs/src/content/docs/en/modules/spatial/utilities.md new file mode 100644 index 00000000..b66f3182 --- /dev/null +++ b/docs/src/content/docs/en/modules/spatial/utilities.md @@ -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(); + 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(); +``` diff --git a/docs/src/content/docs/en/modules/timer/api.md b/docs/src/content/docs/en/modules/timer/api.md new file mode 100644 index 00000000..09aee3eb --- /dev/null +++ b/docs/src/content/docs/en/modules/timer/api.md @@ -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); +``` diff --git a/docs/src/content/docs/en/modules/timer/best-practices.md b/docs/src/content/docs/en/modules/timer/best-practices.md new file mode 100644 index 00000000..9f1241b6 --- /dev/null +++ b/docs/src/content/docs/en/modules/timer/best-practices.md @@ -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; +}); +``` diff --git a/docs/src/content/docs/en/modules/timer/examples.md b/docs/src/content/docs/en/modules/timer/examples.md new file mode 100644 index 00000000..4fcfe9f8 --- /dev/null +++ b/docs/src/content/docs/en/modules/timer/examples.md @@ -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 = 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 + } +} +``` diff --git a/docs/src/content/docs/en/modules/timer/index.md b/docs/src/content/docs/en/modules/timer/index.md index a3009407..9da505a4 100644 --- a/docs/src/content/docs/en/modules/timer/index.md +++ b/docs/src/content/docs/en/modules/timer/index.md @@ -1,5 +1,6 @@ --- title: "Timer System" +description: "Flexible timer and cooldown system" --- `@esengine/timer` provides a flexible timer and cooldown system for delayed execution, repeating tasks, skill cooldowns, and more. @@ -67,8 +68,6 @@ interface TimerHandle { ### TimerInfo -Timer information object: - ```typescript interface TimerInfo { readonly id: string; // Timer ID @@ -80,8 +79,6 @@ interface TimerInfo { ### CooldownInfo -Cooldown information object: - ```typescript interface CooldownInfo { readonly id: string; // Cooldown ID @@ -92,263 +89,8 @@ interface CooldownInfo { } ``` -## API Reference +## Documentation -### createTimerService - -```typescript -function createTimerService(config?: TimerServiceConfig): ITimerService -``` - -**Configuration:** - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `maxTimers` | `number` | `0` | Maximum timer count (0 = unlimited) | -| `maxCooldowns` | `number` | `0` | Maximum cooldown count (0 = unlimited) | - -### Timer API - -#### schedule - -Schedule a one-time timer: - -```typescript -const handle = timerService.schedule('explosion', 2000, () => { - createExplosion(); -}); - -// Cancel early -handle.cancel(); -``` - -#### scheduleRepeating - -Schedule a repeating timer: - -```typescript -// Execute every second -timerService.scheduleRepeating('regen', 1000, () => { - player.hp += 5; -}); - -// Execute immediately once, then repeat every second -timerService.scheduleRepeating('tick', 1000, () => { - console.log('Tick'); -}, true); // immediate = true -``` - -#### cancel / cancelById - -Cancel timers: - -```typescript -// Cancel by handle -handle.cancel(); -// or -timerService.cancel(handle); - -// Cancel by ID -timerService.cancelById('regen'); -``` - -#### hasTimer - -Check if timer exists: - -```typescript -if (timerService.hasTimer('explosion')) { - console.log('Explosion is pending'); -} -``` - -#### getTimerInfo - -Get timer information: - -```typescript -const info = timerService.getTimerInfo('explosion'); -if (info) { - console.log(`Remaining: ${info.remaining}ms`); - console.log(`Repeating: ${info.repeating}`); -} -``` - -### Cooldown API - -#### startCooldown - -Start a cooldown: - -```typescript -timerService.startCooldown('skill_fireball', 5000); -``` - -#### isCooldownReady / isOnCooldown - -Check cooldown status: - -```typescript -if (timerService.isCooldownReady('skill_fireball')) { - castFireball(); - timerService.startCooldown('skill_fireball', 5000); -} - -if (timerService.isOnCooldown('skill_fireball')) { - console.log('On cooldown...'); -} -``` - -#### getCooldownProgress / getCooldownRemaining - -Get cooldown progress: - -```typescript -// Progress 0-1 (0=started, 1=complete) -const progress = timerService.getCooldownProgress('skill_fireball'); -console.log(`Progress: ${(progress * 100).toFixed(0)}%`); - -// Remaining time (ms) -const remaining = timerService.getCooldownRemaining('skill_fireball'); -console.log(`Remaining: ${(remaining / 1000).toFixed(1)}s`); -``` - -#### getCooldownInfo - -Get complete cooldown info: - -```typescript -const info = timerService.getCooldownInfo('skill_fireball'); -if (info) { - console.log(`Duration: ${info.duration}ms`); - console.log(`Remaining: ${info.remaining}ms`); - console.log(`Progress: ${info.progress}`); - console.log(`Ready: ${info.isReady}`); -} -``` - -#### resetCooldown / clearAllCooldowns - -Reset cooldowns: - -```typescript -// Reset single cooldown -timerService.resetCooldown('skill_fireball'); - -// Clear all cooldowns (e.g., on respawn) -timerService.clearAllCooldowns(); -``` - -### Lifecycle - -#### update - -Update timer service (call every frame): - -```typescript -function gameLoop(deltaTime: number) { - timerService.update(deltaTime); // deltaTime in ms -} -``` - -#### clear - -Clear all timers and cooldowns: - -```typescript -timerService.clear(); -``` - -### Debug Properties - -```typescript -console.log(timerService.activeTimerCount); -console.log(timerService.activeCooldownCount); -const timerIds = timerService.getActiveTimerIds(); -const cooldownIds = timerService.getActiveCooldownIds(); -``` - -## Practical Examples - -### Skill Cooldown System - -```typescript -import { createTimerService, type ITimerService } from '@esengine/timer'; - -class SkillSystem { - private timerService: ITimerService; - private skills: Map = new Map(); - - constructor() { - this.timerService = createTimerService(); - } - - useSkill(skillId: string): boolean { - const skill = this.skills.get(skillId); - if (!skill) return false; - - if (!this.timerService.isCooldownReady(skillId)) { - const remaining = this.timerService.getCooldownRemaining(skillId); - console.log(`Skill ${skillId} on cooldown, ${remaining}ms remaining`); - return false; - } - - this.executeSkill(skill); - this.timerService.startCooldown(skillId, skill.cooldown); - return true; - } - - update(dt: number): void { - this.timerService.update(dt); - } -} -``` - -### DOT Effects - -```typescript -class EffectSystem { - private timerService: ITimerService; - - applyDOT(target: Entity, damage: number, duration: number): void { - const dotId = `dot_${target.id}_${Date.now()}`; - let elapsed = 0; - - this.timerService.scheduleRepeating(dotId, 1000, () => { - elapsed += 1000; - target.takeDamage(damage); - - if (elapsed >= duration) { - this.timerService.cancelById(dotId); - } - }); - } -} -``` - -## Blueprint Nodes - -### Cooldown Nodes - -- `StartCooldown` - Start cooldown -- `IsCooldownReady` - Check if cooldown is ready -- `GetCooldownProgress` - Get cooldown progress -- `GetCooldownInfo` - Get cooldown info -- `ResetCooldown` - Reset cooldown - -### Timer Nodes - -- `HasTimer` - Check if timer exists -- `CancelTimer` - Cancel timer -- `GetTimerRemaining` - Get timer remaining time - -## Service Token - -For dependency injection: - -```typescript -import { TimerServiceToken, createTimerService } from '@esengine/timer'; - -services.register(TimerServiceToken, createTimerService()); -const timerService = services.get(TimerServiceToken); -``` +- [API Reference](./api) - Complete timer and cooldown API +- [Examples](./examples) - Skill cooldowns, DOT effects, buff systems +- [Best Practices](./best-practices) - Usage tips and ECS integration diff --git a/docs/src/content/docs/guide/plugin-system.md b/docs/src/content/docs/guide/plugin-system.md deleted file mode 100644 index e559bfc8..00000000 --- a/docs/src/content/docs/guide/plugin-system.md +++ /dev/null @@ -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; - - // 卸载插件时调用 - uninstall(): void | Promise; -} -``` - -### 插件生命周期 - -#### 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 { - // 异步加载资源 - const config = await fetch('/plugin-config.json').then(r => r.json()); - - // 使用加载的配置初始化服务 - const service = new MyService(config); - services.registerInstance(MyService, service); - } - - async uninstall(): Promise { - // 异步清理 - 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 { - try { - // 可能失败的操作 - await this.loadConfig(); - } catch (error) { - console.error('Failed to load plugin config:', error); - throw error; // 重新抛出,让框架知道安装失败 - } - } - - async uninstall(): Promise { - 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 初始化和基础使用 diff --git a/docs/src/content/docs/guide/plugin-system/best-practices.md b/docs/src/content/docs/guide/plugin-system/best-practices.md new file mode 100644 index 00000000..8c902a98 --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/best-practices.md @@ -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 { + try { + await this.loadConfig(); + } catch (error) { + console.error('Failed to load plugin config:', error); + throw error; // 重新抛出,让框架知道安装失败 + } + } + + async uninstall(): Promise { + 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 连接 +- 系统引用 + +### 何时使用插件 + +| 适合使用插件 | 不适合使用插件 | +|-------------|---------------| +| 可选功能(调试工具、性能分析) | 核心游戏逻辑 | +| 第三方集成(网络库、物理引擎) | 简单的工具类 | +| 跨项目复用的功能模块 | 项目特定的功能 | diff --git a/docs/src/content/docs/guide/plugin-system/dependencies.md b/docs/src/content/docs/guide/plugin-system/dependencies.md new file mode 100644 index 00000000..8d1dc470 --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/dependencies.md @@ -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 {} +} +``` diff --git a/docs/src/content/docs/guide/plugin-system/development.md b/docs/src/content/docs/guide/plugin-system/development.md new file mode 100644 index 00000000..fe710c92 --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/development.md @@ -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; + + // 卸载插件时调用 + uninstall(): void | Promise; +} +``` + +## 生命周期方法 + +### 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 { + // 异步加载资源 + const config = await fetch('/plugin-config.json').then(r => r.json()); + + // 使用加载的配置初始化服务 + const service = new MyService(config); + services.registerInstance(MyService, service); + } + + async uninstall(): Promise { + // 异步清理 + await this.saveState(); + } + + private async saveState() { + // 保存插件状态 + } +} + +// 使用 +await Core.installPlugin(new AsyncPlugin()); +``` + +## 生命周期流程 + +``` +安装: Core.installPlugin(plugin) + ↓ +依赖检查: 检查 dependencies 是否满足 + ↓ +调用 install(): 注册服务、添加系统 + ↓ +状态更新: 标记为已安装 + +卸载: Core.uninstallPlugin(name) + ↓ +依赖检查: 检查是否被其他插件依赖 + ↓ +调用 uninstall(): 清理资源 + ↓ +状态更新: 从插件列表移除 +``` diff --git a/docs/src/content/docs/guide/plugin-system/examples.md b/docs/src/content/docs/guide/plugin-system/examples.md new file mode 100644 index 00000000..533af46c --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/examples.md @@ -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; + } + } +} +``` diff --git a/docs/src/content/docs/guide/plugin-system/index.md b/docs/src/content/docs/guide/plugin-system/index.md new file mode 100644 index 00000000..f2e82a9b --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/index.md @@ -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/) - 设计规范 diff --git a/docs/src/content/docs/guide/plugin-system/management.md b/docs/src/content/docs/guide/plugin-system/management.md new file mode 100644 index 00000000..b3a6d977 --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/management.md @@ -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; +} +``` diff --git a/docs/src/content/docs/guide/plugin-system/services-systems.md b/docs/src/content/docs/guide/plugin-system/services-systems.md new file mode 100644 index 00000000..c9d55dde --- /dev/null +++ b/docs/src/content/docs/guide/plugin-system/services-systems.md @@ -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 = []; + } +} +``` diff --git a/docs/src/content/docs/guide/scene.md b/docs/src/content/docs/guide/scene.md deleted file mode 100644 index d2483fed..00000000 --- a/docs/src/content/docs/guide/scene.md +++ /dev/null @@ -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 = new Map(); - private sounds: Map = 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 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。 diff --git a/docs/src/content/docs/guide/scene/best-practices.md b/docs/src/content/docs/guide/scene/best-practices.md new file mode 100644 index 00000000..7a376ce7 --- /dev/null +++ b/docs/src/content/docs/guide/scene/best-practices.md @@ -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 = new Map(); + private sounds: Map = 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()` 中清理所有资源 | +| 系统顺序 | 按输入→逻辑→渲染顺序添加系统 | +| 事件解耦 | 使用事件系统进行场景内通信 | +| 初始化分层 | 配置→系统→实体→事件的初始化顺序 | diff --git a/docs/src/content/docs/guide/scene/debugging.md b/docs/src/content/docs/guide/scene/debugging.md new file mode 100644 index 00000000..fab17d2c --- /dev/null +++ b/docs/src/content/docs/guide/scene/debugging.md @@ -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` 识别慢系统 diff --git a/docs/src/content/docs/guide/scene/entity-management.md b/docs/src/content/docs/guide/scene/entity-management.md new file mode 100644 index 00000000..1c7032ee --- /dev/null +++ b/docs/src/content/docs/guide/scene/entity-management.md @@ -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` | 销毁所有实体 | diff --git a/docs/src/content/docs/guide/scene/events.md b/docs/src/content/docs/guide/scene/events.md new file mode 100644 index 00000000..f4898069 --- /dev/null +++ b/docs/src/content/docs/guide/scene/events.md @@ -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` | diff --git a/docs/src/content/docs/guide/scene/index.md b/docs/src/content/docs/guide/scene/index.md new file mode 100644 index 00000000..8a0a29f4 --- /dev/null +++ b/docs/src/content/docs/guide/scene/index.md @@ -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/) - 场景设计模式 diff --git a/docs/src/content/docs/guide/scene/lifecycle.md b/docs/src/content/docs/guide/scene/lifecycle.md new file mode 100644 index 00000000..eb5ddb09 --- /dev/null +++ b/docs/src/content/docs/guide/scene/lifecycle.md @@ -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() 由框架管理** - 不需要手动调用 diff --git a/docs/src/content/docs/guide/scene/system-management.md b/docs/src/content/docs/guide/scene/system-management.md new file mode 100644 index 00000000..f13aee37 --- /dev/null +++ b/docs/src/content/docs/guide/scene/system-management.md @@ -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[]` | 获取所有系统 | diff --git a/docs/src/content/docs/guide/worker-system.md b/docs/src/content/docs/guide/worker-system.md deleted file mode 100644 index 1cded0a2..00000000 --- a/docs/src/content/docs/guide/worker-system.md +++ /dev/null @@ -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 { - 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 { - 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 { - 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框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。 \ No newline at end of file diff --git a/docs/src/content/docs/guide/worker-system/best-practices.md b/docs/src/content/docs/guide/worker-system/best-practices.md new file mode 100644 index 00000000..5d6d4ee2 --- /dev/null +++ b/docs/src/content/docs/guide/worker-system/best-practices.md @@ -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 +}); +``` diff --git a/docs/src/content/docs/guide/worker-system/configuration.md b/docs/src/content/docs/guide/worker-system/configuration.md new file mode 100644 index 00000000..5b3fe3b5 --- /dev/null +++ b/docs/src/content/docs/guide/worker-system/configuration.md @@ -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 +}); +``` diff --git a/docs/src/content/docs/guide/worker-system/examples.md b/docs/src/content/docs/guide/worker-system/examples.md new file mode 100644 index 00000000..951c0db2 --- /dev/null +++ b/docs/src/content/docs/guide/worker-system/examples.md @@ -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 { + 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 + }; +} +``` diff --git a/docs/src/content/docs/guide/worker-system/index.md b/docs/src/content/docs/guide/worker-system/index.md new file mode 100644 index 00000000..9655f38e --- /dev/null +++ b/docs/src/content/docs/guide/worker-system/index.md @@ -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 { + 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/) - 多线程物理计算、实时性能对比 diff --git a/docs/src/content/docs/guide/worker-system/wechat.md b/docs/src/content/docs/guide/worker-system/wechat.md new file mode 100644 index 00000000..9560bb4a --- /dev/null +++ b/docs/src/content/docs/guide/worker-system/wechat.md @@ -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 { + 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` diff --git a/docs/src/content/docs/modules/blueprint/composition.md b/docs/src/content/docs/modules/blueprint/composition.md new file mode 100644 index 00000000..c7c496a7 --- /dev/null +++ b/docs/src/content/docs/modules/blueprint/composition.md @@ -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. **版本控制** - 更新片段时注意向后兼容 diff --git a/docs/src/content/docs/modules/blueprint/custom-nodes.md b/docs/src/content/docs/modules/blueprint/custom-nodes.md new file mode 100644 index 00000000..c799afb8 --- /dev/null +++ b/docs/src/content/docs/modules/blueprint/custom-nodes.md @@ -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(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 } }; + } +} +``` diff --git a/docs/src/content/docs/modules/blueprint/examples.md b/docs/src/content/docs/modules/blueprint/examples.md new file mode 100644 index 00000000..646acd68 --- /dev/null +++ b/docs/src/content/docs/modules/blueprint/examples.md @@ -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(node.id, 'target'); + const amount = context.getInput(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 中执行重计算 +// 使用事件驱动而非轮询 +``` diff --git a/docs/src/content/docs/modules/blueprint/index.md b/docs/src/content/docs/modules/blueprint/index.md index 459c68a0..94c5b3e4 100644 --- a/docs/src/content/docs/modules/blueprint/index.md +++ b/docs/src/content/docs/modules/blueprint/index.md @@ -1,5 +1,6 @@ --- title: "蓝图可视化脚本 (Blueprint)" +description: "完整的可视化脚本系统" --- `@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。 @@ -104,406 +105,10 @@ type VariableScope = | 'global'; // 全局共享 ``` -## 虚拟机 API +## 文档导航 -### BlueprintVM - -蓝图虚拟机负责执行蓝图图: - -```typescript -import { BlueprintVM } from '@esengine/blueprint'; - -// 创建 VM -const vm = new BlueprintVM(blueprintAsset, entity, scene); - -// 启动(触发 BeginPlay) -vm.start(); - -// 每帧更新(触发 Tick) -vm.tick(deltaTime); - -// 停止(触发 EndPlay) -vm.stop(); - -// 暂停/恢复 -vm.pause(); -vm.resume(); - -// 触发事件 -vm.triggerEvent('EventCollision', { other: otherEntity }); -vm.triggerCustomEvent('OnDamage', { amount: 50 }); - -// 调试模式 -vm.debug = true; -``` - -### 执行上下文 - -```typescript -interface ExecutionContext { - blueprint: BlueprintAsset; // 蓝图资产 - entity: Entity; // 当前实体 - scene: IScene; // 当前场景 - deltaTime: number; // 帧间隔时间 - time: number; // 总运行时间 - - // 获取输入值 - getInput(nodeId: string, pinName: string): T; - - // 设置输出值 - setOutput(nodeId: string, pinName: string, value: unknown): void; - - // 变量访问 - getVariable(name: string): T; - setVariable(name: string, value: unknown): void; -} -``` - -### 执行结果 - -```typescript -interface ExecutionResult { - outputs?: Record; // 输出值 - 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(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 { - const json = await fs.readFile(path, 'utf-8'); - const asset = JSON.parse(json); - - if (!validateBlueprintAsset(asset)) { - throw new Error('Invalid blueprint file'); - } - - return asset; -} -``` - -## 最佳实践 - -1. **使用片段复用逻辑** - - 将通用逻辑封装为片段 - - 通过组合器构建复杂蓝图 - -2. **合理使用变量作用域** - - `local`: 临时计算结果 - - `instance`: 实体状态(如生命值) - - `global`: 游戏全局状态 - -3. **避免无限循环** - - VM 有每帧最大执行步数限制(默认 1000) - - 使用 Delay 节点打断长执行链 - -4. **调试技巧** - - 启用 `vm.debug = true` 查看执行日志 - - 使用 Print 节点输出中间值 - -5. **性能优化** - - 纯节点(`isPure: true`)的输出会被缓存 - - 避免在 Tick 中执行重计算 +- [虚拟机 API](./vm) - BlueprintVM 执行和上下文 +- [自定义节点](./custom-nodes) - 创建自定义节点 +- [内置节点](./nodes) - 内置节点参考 +- [蓝图组合](./composition) - 片段和组合器 +- [实际示例](./examples) - ECS 集成和最佳实践 diff --git a/docs/src/content/docs/modules/blueprint/nodes.md b/docs/src/content/docs/modules/blueprint/nodes.md new file mode 100644 index 00000000..40e37a89 --- /dev/null +++ b/docs/src/content/docs/modules/blueprint/nodes.md @@ -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` | 调试断点 | diff --git a/docs/src/content/docs/modules/blueprint/vm.md b/docs/src/content/docs/modules/blueprint/vm.md new file mode 100644 index 00000000..9e3139a5 --- /dev/null +++ b/docs/src/content/docs/modules/blueprint/vm.md @@ -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(nodeId: string, pinName: string): T; + + // 设置输出值 + setOutput(nodeId: string, pinName: string, value: unknown): void; + + // 变量访问 + getVariable(name: string): T; + setVariable(name: string, value: unknown): void; +} +``` + +## 执行结果 + +```typescript +interface ExecutionResult { + outputs?: Record; // 输出值 + 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 { + const json = await fs.readFile(path, 'utf-8'); + const asset = JSON.parse(json); + + if (!validateBlueprintAsset(asset)) { + throw new Error('Invalid blueprint file'); + } + + return asset; +} +``` diff --git a/docs/src/content/docs/modules/fsm/api.md b/docs/src/content/docs/modules/fsm/api.md new file mode 100644 index 00000000..8ad08ea8 --- /dev/null +++ b/docs/src/content/docs/modules/fsm/api.md @@ -0,0 +1,135 @@ +--- +title: "API 参考" +description: "状态机完整 API" +--- + +## createStateMachine + +```typescript +function createStateMachine( + initialState: TState, + options?: StateMachineOptions +): IStateMachine +``` + +**参数:** +- `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` - 重置状态机 diff --git a/docs/src/content/docs/modules/fsm/examples.md b/docs/src/content/docs/modules/fsm/examples.md new file mode 100644 index 00000000..98270c85 --- /dev/null +++ b/docs/src/content/docs/modules/fsm/examples.md @@ -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('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; + + 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('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('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') +}); + +// 设置转换(略) +``` diff --git a/docs/src/content/docs/modules/fsm/index.md b/docs/src/content/docs/modules/fsm/index.md index 1438023a..ca271d51 100644 --- a/docs/src/content/docs/modules/fsm/index.md +++ b/docs/src/content/docs/modules/fsm/index.md @@ -1,5 +1,6 @@ --- title: "状态机 (FSM)" +description: "类型安全的有限状态机实现" --- `@esengine/fsm` 提供了一个类型安全的有限状态机实现,用于角色、AI 或任何需要状态管理的场景。 @@ -91,249 +92,7 @@ fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1); // 如果同时满足,会先尝试 attack(优先级 10) ``` -## API 参考 +## 文档导航 -### createStateMachine - -```typescript -function createStateMachine( - initialState: TState, - options?: StateMachineOptions -): IStateMachine -``` - -**参数:** -- `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('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; - - constructor(initialState: string) { - super(); - this.fsm = createStateMachine(initialState); - } -} - -// 状态机系统 -class FSMSystem extends EntitySystem { - constructor() { - super(Matcher.all(FSMComponent)); - } - - protected processEntity(entity: Entity, dt: number): void { - const fsmComp = entity.getComponent(FSMComponent); - fsmComp.fsm.evaluateTransitions(); - fsmComp.fsm.update(dt); - } -} -``` - -## 蓝图节点 - -FSM 模块提供了可视化脚本支持的蓝图节点: - -- `GetCurrentState` - 获取当前状态 -- `TransitionTo` - 转换到指定状态 -- `CanTransition` - 检查是否可以转换 -- `IsInState` - 检查是否在指定状态 -- `WasInState` - 检查是否曾在指定状态 -- `GetStateDuration` - 获取状态持续时间 -- `EvaluateTransitions` - 评估转换条件 -- `ResetStateMachine` - 重置状态机 +- [API 参考](./api) - 完整的状态机 API +- [实际示例](./examples) - 角色状态机、ECS 集成 diff --git a/docs/src/content/docs/modules/pathfinding/examples.md b/docs/src/content/docs/modules/pathfinding/examples.md new file mode 100644 index 00000000..875e9422 --- /dev/null +++ b/docs/src/content/docs/modules/pathfinding/examples.md @@ -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 = 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`(默认) diff --git a/docs/src/content/docs/modules/pathfinding/grid-map.md b/docs/src/content/docs/modules/pathfinding/grid-map.md new file mode 100644 index 00000000..793ec24e --- /dev/null +++ b/docs/src/content/docs/modules/pathfinding/grid-map.md @@ -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 // 使用曼哈顿距离 +}); +``` diff --git a/docs/src/content/docs/modules/pathfinding/index.md b/docs/src/content/docs/modules/pathfinding/index.md index c5f74fc8..b437cecf 100644 --- a/docs/src/content/docs/modules/pathfinding/index.md +++ b/docs/src/content/docs/modules/pathfinding/index.md @@ -1,5 +1,6 @@ --- title: "寻路系统 (Pathfinding)" +description: "完整的 2D 寻路解决方案" --- `@esengine/pathfinding` 提供了完整的 2D 寻路解决方案,包括 A* 算法、网格地图、导航网格和路径平滑。 @@ -67,29 +68,21 @@ const result = navmesh.findPath(1, 1, 18, 8); ## 核心概念 -### IPoint - 坐标点 +### 核心接口 ```typescript interface IPoint { readonly x: number; readonly y: number; } -``` -### IPathResult - 寻路结果 - -```typescript interface IPathResult { readonly found: boolean; // 是否找到路径 readonly path: readonly IPoint[]; // 路径点列表 readonly cost: number; // 路径总代价 readonly nodesSearched: number; // 搜索的节点数 } -``` -### IPathfindingOptions - 寻路配置 - -```typescript interface IPathfindingOptions { maxNodes?: number; // 最大搜索节点数(默认 10000) heuristicWeight?: number; // 启发式权重(>1 更快但可能非最优) @@ -98,402 +91,16 @@ interface IPathfindingOptions { } ``` -## 启发式函数 - -模块提供了四种启发式函数: +### 启发式函数 | 函数 | 适用场景 | 说明 | |------|----------|------| -| `manhattanDistance` | 4方向移动 | 曼哈顿距离,只考虑水平/垂直 | -| `euclideanDistance` | 任意方向 | 欧几里得距离,直线距离 | -| `chebyshevDistance` | 8方向移动 | 切比雪夫距离,对角线代价为 1 | -| `octileDistance` | 8方向移动 | 八角距离,对角线代价为 √2(默认) | +| `manhattanDistance` | 4方向移动 | 曼哈顿距离 | +| `euclideanDistance` | 任意方向 | 欧几里得距离 | +| `chebyshevDistance` | 8方向移动 | 切比雪夫距离 | +| `octileDistance` | 8方向移动 | 八角距离(默认) | -```typescript -import { manhattanDistance, octileDistance } from '@esengine/pathfinding'; - -// 自定义启发式 -const grid = createGridMap(20, 20, { - heuristic: manhattanDistance // 使用曼哈顿距离 -}); -``` - -## 网格地图 API - -### createGridMap - -```typescript -function createGridMap( - width: number, - height: number, - options?: IGridMapOptions -): GridMap -``` - -**配置选项:** - -| 属性 | 类型 | 默认值 | 描述 | -|------|------|--------|------| -| `allowDiagonal` | `boolean` | `true` | 允许对角移动 | -| `diagonalCost` | `number` | `√2` | 对角移动代价 | -| `avoidCorners` | `boolean` | `true` | 避免穿角 | -| `heuristic` | `HeuristicFunction` | `octileDistance` | 启发式函数 | - -### 地图操作 - -```typescript -// 检查/设置可通行性 -grid.isWalkable(x, y); -grid.setWalkable(x, y, false); - -// 设置移动代价(如沼泽、沙地) -grid.setCost(x, y, 2); // 代价为 2(默认 1) - -// 设置矩形区域 -grid.setRectWalkable(0, 0, 5, 5, false); - -// 从数组加载(0=可通行,非0=障碍) -grid.loadFromArray([ - [0, 0, 0, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 0, 0, 0] -]); - -// 从字符串加载(.=可通行,#=障碍) -grid.loadFromString(` -..... -.#.#. -.#... -`); - -// 导出为字符串 -console.log(grid.toString()); - -// 重置所有节点为可通行 -grid.reset(); -``` - -### 方向常量 - -```typescript -import { DIRECTIONS_4, DIRECTIONS_8 } from '@esengine/pathfinding'; - -// 4方向(上下左右) -DIRECTIONS_4 // [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, ...] - -// 8方向(含对角线) -DIRECTIONS_8 // [{ dx: 0, dy: -1 }, { dx: 1, dy: -1 }, ...] -``` - -## A* 寻路器 API - -### createAStarPathfinder - -```typescript -function createAStarPathfinder(map: IPathfindingMap): AStarPathfinder -``` - -### findPath - -```typescript -const result = pathfinder.findPath( - startX, startY, - endX, endY, - { - maxNodes: 5000, // 限制搜索节点数 - heuristicWeight: 1.5 // 加速但可能非最优 - } -); -``` - -### 重用寻路器 - -```typescript -// 寻路器可重用,内部会自动清理状态 -pathfinder.findPath(0, 0, 10, 10); -pathfinder.findPath(5, 5, 15, 15); - -// 手动清理(可选) -pathfinder.clear(); -``` - -## 导航网格 API - -### createNavMesh - -```typescript -function createNavMesh(): NavMesh -``` - -### 构建导航网格 - -```typescript -const navmesh = createNavMesh(); - -// 添加凸多边形 -const id1 = navmesh.addPolygon([ - { x: 0, y: 0 }, { x: 10, y: 0 }, - { x: 10, y: 10 }, { x: 0, y: 10 } -]); - -const id2 = navmesh.addPolygon([ - { x: 10, y: 0 }, { x: 20, y: 0 }, - { x: 20, y: 10 }, { x: 10, y: 10 } -]); - -// 方式1:自动检测共享边并建立连接 -navmesh.build(); - -// 方式2:手动设置连接 -navmesh.setConnection(id1, id2, { - left: { x: 10, y: 0 }, - right: { x: 10, y: 10 } -}); -``` - -### 查询和寻路 - -```typescript -// 查找包含点的多边形 -const polygon = navmesh.findPolygonAt(5, 5); - -// 检查位置是否可通行 -navmesh.isWalkable(5, 5); - -// 寻路(内部使用漏斗算法优化路径) -const result = navmesh.findPath(1, 1, 18, 8); -``` - -## 路径平滑 API - -### 视线简化 - -移除不必要的中间点: - -```typescript -import { createLineOfSightSmoother } from '@esengine/pathfinding'; - -const smoother = createLineOfSightSmoother(); -const smoothedPath = smoother.smooth(result.path, grid); - -// 原路径: [(0,0), (1,1), (2,2), (3,3), (4,4)] -// 简化后: [(0,0), (4,4)] -``` - -### 曲线平滑 - -使用 Catmull-Rom 样条曲线: - -```typescript -import { createCatmullRomSmoother } from '@esengine/pathfinding'; - -const smoother = createCatmullRomSmoother( - 5, // segments - 每段插值点数 - 0.5 // tension - 张力 (0-1) -); - -const curvedPath = smoother.smooth(result.path, grid); -``` - -### 组合平滑 - -先简化再曲线平滑: - -```typescript -import { createCombinedSmoother } from '@esengine/pathfinding'; - -const smoother = createCombinedSmoother(5, 0.5); -const finalPath = smoother.smooth(result.path, grid); -``` - -### 视线检测函数 - -```typescript -import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding'; - -// Bresenham 算法(快速,网格对齐) -const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid); - -// 射线投射(精确,支持浮点坐标) -const hasLOS = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5); -``` - -## 实际示例 - -### 游戏角色移动 - -```typescript -class MovementSystem { - private grid: GridMap; - private pathfinder: AStarPathfinder; - private smoother: CombinedSmoother; - - constructor(width: number, height: number) { - this.grid = createGridMap(width, height); - this.pathfinder = createAStarPathfinder(this.grid); - this.smoother = createCombinedSmoother(); - } - - findPath(from: IPoint, to: IPoint): IPoint[] | null { - const result = this.pathfinder.findPath( - from.x, from.y, - to.x, to.y - ); - - if (!result.found) { - return null; - } - - // 平滑路径 - return this.smoother.smooth(result.path, this.grid); - } - - setObstacle(x: number, y: number): void { - this.grid.setWalkable(x, y, false); - } - - setTerrain(x: number, y: number, cost: number): void { - this.grid.setCost(x, y, cost); - } -} -``` - -### 动态障碍物 - -```typescript -class DynamicPathfinding { - private grid: GridMap; - private pathfinder: AStarPathfinder; - private dynamicObstacles: Set = new Set(); - - addDynamicObstacle(x: number, y: number): void { - const key = `${x},${y}`; - if (!this.dynamicObstacles.has(key)) { - this.dynamicObstacles.add(key); - this.grid.setWalkable(x, y, false); - } - } - - removeDynamicObstacle(x: number, y: number): void { - const key = `${x},${y}`; - if (this.dynamicObstacles.has(key)) { - this.dynamicObstacles.delete(key); - this.grid.setWalkable(x, y, true); - } - } - - findPath(from: IPoint, to: IPoint): IPathResult { - return this.pathfinder.findPath(from.x, from.y, to.x, to.y); - } -} -``` - -### 不同地形代价 - -```typescript -// 设置不同地形的移动代价 -const grid = createGridMap(50, 50); - -// 普通地面 - 代价 1(默认) -// 沙地 - 代价 2 -for (let y = 10; y < 20; y++) { - for (let x = 0; x < 50; x++) { - grid.setCost(x, y, 2); - } -} - -// 沼泽 - 代价 4 -for (let y = 30; y < 35; y++) { - for (let x = 20; x < 30; x++) { - grid.setCost(x, y, 4); - } -} - -// 寻路时会自动考虑地形代价 -const result = pathfinder.findPath(0, 0, 49, 49); -``` - -### 分层寻路 - -对于大型地图,使用层级化寻路: - -```typescript -class HierarchicalPathfinding { - private coarseGrid: GridMap; // 粗粒度网格 - private fineGrid: GridMap; // 细粒度网格 - private coarsePathfinder: AStarPathfinder; - private finePathfinder: AStarPathfinder; - private cellSize = 10; - - findPath(from: IPoint, to: IPoint): IPoint[] { - // 1. 在粗粒度网格上寻路 - const coarseFrom = this.toCoarse(from); - const coarseTo = this.toCoarse(to); - const coarseResult = this.coarsePathfinder.findPath( - coarseFrom.x, coarseFrom.y, - coarseTo.x, coarseTo.y - ); - - if (!coarseResult.found) { - return []; - } - - // 2. 在每个粗粒度单元内进行细粒度寻路 - const finePath: IPoint[] = []; - // ... 详细实现略 - return finePath; - } - - private toCoarse(p: IPoint): IPoint { - return { - x: Math.floor(p.x / this.cellSize), - y: Math.floor(p.y / this.cellSize) - }; - } -} -``` - -## 蓝图节点 - -Pathfinding 模块提供了可视化脚本支持的蓝图节点: - -- `FindPath` - 查找路径 -- `FindPathSmooth` - 查找并平滑路径 -- `IsWalkable` - 检查位置是否可通行 -- `GetPathLength` - 获取路径点数 -- `GetPathDistance` - 获取路径总距离 -- `GetPathPoint` - 获取路径上的指定点 -- `MoveAlongPath` - 沿路径移动 -- `HasLineOfSight` - 检查视线 - -## 性能优化 - -1. **限制搜索范围** - ```typescript - pathfinder.findPath(x1, y1, x2, y2, { maxNodes: 1000 }); - ``` - -2. **使用启发式权重** - ```typescript - // 权重 > 1 会更快但可能不是最优路径 - pathfinder.findPath(x1, y1, x2, y2, { heuristicWeight: 1.5 }); - ``` - -3. **复用寻路器实例** - ```typescript - // 创建一次,多次使用 - const pathfinder = createAStarPathfinder(grid); - ``` - -4. **使用导航网格** - - 对于复杂地形,NavMesh 比网格寻路更高效 - - 多边形数量远少于网格单元格数量 - -5. **选择合适的启发式** - - 4方向移动用 `manhattanDistance` - - 8方向移动用 `octileDistance`(默认) - -## 网格 vs 导航网格 +### 网格 vs 导航网格 | 特性 | GridMap | NavMesh | |------|---------|---------| @@ -501,4 +108,10 @@ Pathfinding 模块提供了可视化脚本支持的蓝图节点: | 内存占用 | 较高 (width × height) | 较低 (多边形数) | | 精度 | 网格对齐 | 连续坐标 | | 动态修改 | 容易 | 需要重建 | -| 设置复杂度 | 简单 | 较复杂 | + +## 文档导航 + +- [网格地图 API](./grid-map) - 网格操作和 A* 寻路 +- [导航网格 API](./navmesh) - NavMesh 构建和查询 +- [路径平滑](./smoothing) - 视线简化和曲线平滑 +- [实际示例](./examples) - 游戏移动、动态障碍物、分层寻路 diff --git a/docs/src/content/docs/modules/pathfinding/navmesh.md b/docs/src/content/docs/modules/pathfinding/navmesh.md new file mode 100644 index 00000000..6a4e7f99 --- /dev/null +++ b/docs/src/content/docs/modules/pathfinding/navmesh.md @@ -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(); +``` diff --git a/docs/src/content/docs/modules/pathfinding/smoothing.md b/docs/src/content/docs/modules/pathfinding/smoothing.md new file mode 100644 index 00000000..a83d7bb4 --- /dev/null +++ b/docs/src/content/docs/modules/pathfinding/smoothing.md @@ -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` - 检查视线 diff --git a/docs/src/content/docs/modules/procgen/examples.md b/docs/src/content/docs/modules/procgen/examples.md new file mode 100644 index 00000000..df54223a --- /dev/null +++ b/docs/src/content/docs/modules/procgen/examples.md @@ -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; + private lootTables: Map = 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,控制频率增长速度 diff --git a/docs/src/content/docs/modules/procgen/index.md b/docs/src/content/docs/modules/procgen/index.md index 60c85c49..e896d44f 100644 --- a/docs/src/content/docs/modules/procgen/index.md +++ b/docs/src/content/docs/modules/procgen/index.md @@ -1,5 +1,6 @@ --- title: "程序化生成 (Procgen)" +description: "噪声函数、种子随机数和随机工具" --- `@esengine/procgen` 提供了程序化内容生成的核心工具,包括噪声函数、种子随机数和各种随机工具。 @@ -67,493 +68,9 @@ const drop = loot.pick(rng); console.log(drop); // 大概率是 'common' ``` -## 噪声函数 +## 文档导航 -### Perlin 噪声 - -经典的梯度噪声,输出范围 [-1, 1]: - -```typescript -import { createPerlinNoise } from '@esengine/procgen'; - -const perlin = createPerlinNoise(seed); - -// 2D 噪声 -const value2D = perlin.noise2D(x, y); - -// 3D 噪声 -const value3D = perlin.noise3D(x, y, z); -``` - -### Simplex 噪声 - -比 Perlin 更快、更少方向性偏差: - -```typescript -import { createSimplexNoise } from '@esengine/procgen'; - -const simplex = createSimplexNoise(seed); - -const value = simplex.noise2D(x, y); -``` - -### Worley 噪声 - -基于细胞的噪声,适合生成石头、细胞等纹理: - -```typescript -import { createWorleyNoise } from '@esengine/procgen'; - -const worley = createWorleyNoise(seed); - -// 返回到最近点的距离 -const distance = worley.noise2D(x, y); -``` - -### FBM (分形布朗运动) - -叠加多层噪声创建更丰富的细节: - -```typescript -import { createPerlinNoise, createFBM } from '@esengine/procgen'; - -const baseNoise = createPerlinNoise(seed); - -const fbm = createFBM(baseNoise, { - octaves: 6, // 层数(越多细节越丰富) - lacunarity: 2.0, // 频率倍增因子 - persistence: 0.5, // 振幅衰减因子 - frequency: 1.0, // 初始频率 - amplitude: 1.0 // 初始振幅 -}); - -// 标准 FBM -const value = fbm.noise2D(x, y); - -// Ridged FBM(脊状,适合山脉) -const ridged = fbm.ridged2D(x, y); - -// Turbulence(湍流) -const turb = fbm.turbulence2D(x, y); - -// Billowed(膨胀,适合云朵) -const cloud = fbm.billowed2D(x, y); -``` - -## 种子随机数 API - -### SeededRandom - -基于 xorshift128+ 算法的确定性伪随机数生成器: - -```typescript -import { createSeededRandom } from '@esengine/procgen'; - -const rng = createSeededRandom(42); -``` - -### 基础方法 - -```typescript -// [0, 1) 浮点数 -rng.next(); - -// [min, max] 整数 -rng.nextInt(1, 10); - -// [min, max) 浮点数 -rng.nextFloat(0, 100); - -// 布尔值(可指定概率) -rng.nextBool(); // 50% -rng.nextBool(0.3); // 30% - -// 重置到初始状态 -rng.reset(); -``` - -### 分布方法 - -```typescript -// 正态分布(高斯分布) -rng.nextGaussian(); // 均值 0, 标准差 1 -rng.nextGaussian(100, 15); // 均值 100, 标准差 15 - -// 指数分布 -rng.nextExponential(); // λ = 1 -rng.nextExponential(0.5); // λ = 0.5 -``` - -### 几何方法 - -```typescript -// 圆内均匀分布的点 -const point = rng.nextPointInCircle(50); // { x, y } - -// 圆周上的点 -const edge = rng.nextPointOnCircle(50); // { x, y } - -// 球内均匀分布的点 -const point3D = rng.nextPointInSphere(50); // { x, y, z } - -// 随机方向向量 -const dir = rng.nextDirection2D(); // { x, y },长度为 1 -``` - -## 加权随机 API - -### WeightedRandom - -预计算累积权重,高效随机选择: - -```typescript -import { createWeightedRandom } from '@esengine/procgen'; - -const selector = createWeightedRandom([ - { value: 'apple', weight: 5 }, - { value: 'banana', weight: 3 }, - { value: 'cherry', weight: 2 } -]); - -// 使用种子随机数 -const result = selector.pick(rng); - -// 使用 Math.random -const result2 = selector.pickRandom(); - -// 获取概率 -console.log(selector.getProbability(0)); // 0.5 (5/10) -console.log(selector.size); // 3 -console.log(selector.totalWeight); // 10 -``` - -### 便捷函数 - -```typescript -import { weightedPick, weightedPickFromMap } from '@esengine/procgen'; - -// 从数组选择 -const item = weightedPick([ - { value: 'a', weight: 1 }, - { value: 'b', weight: 2 } -], rng); - -// 从对象选择 -const item2 = weightedPickFromMap({ - 'common': 60, - 'rare': 30, - 'epic': 10 -}, rng); -``` - -## 洗牌和采样 API - -### shuffle / shuffleCopy - -Fisher-Yates 洗牌算法: - -```typescript -import { shuffle, shuffleCopy } from '@esengine/procgen'; - -const arr = [1, 2, 3, 4, 5]; - -// 原地洗牌 -shuffle(arr, rng); - -// 创建洗牌副本(不修改原数组) -const shuffled = shuffleCopy(arr, rng); -``` - -### pickOne - -随机选择一个元素: - -```typescript -import { pickOne } from '@esengine/procgen'; - -const items = ['a', 'b', 'c', 'd']; -const item = pickOne(items, rng); -``` - -### sample / sampleWithReplacement - -采样: - -```typescript -import { sample, sampleWithReplacement } from '@esengine/procgen'; - -const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - -// 采样 3 个不重复元素 -const unique = sample(arr, 3, rng); - -// 采样 5 个(可重复) -const withRep = sampleWithReplacement(arr, 5, rng); -``` - -### randomIntegers - -生成范围内的随机整数数组: - -```typescript -import { randomIntegers } from '@esengine/procgen'; - -// 从 1-100 中随机选 5 个不重复的数 -const nums = randomIntegers(1, 100, 5, rng); -``` - -### weightedSample - -按权重采样(不重复): - -```typescript -import { weightedSample } from '@esengine/procgen'; - -const items = ['A', 'B', 'C', 'D', 'E']; -const weights = [10, 8, 6, 4, 2]; - -// 按权重选 3 个 -const selected = weightedSample(items, weights, 3, rng); -``` - -## 实际示例 - -### 程序化地形生成 - -```typescript -import { createPerlinNoise, createFBM } from '@esengine/procgen'; - -class TerrainGenerator { - private fbm: FBM; - private moistureFbm: FBM; - - constructor(seed: number) { - const heightNoise = createPerlinNoise(seed); - const moistureNoise = createPerlinNoise(seed + 1000); - - this.fbm = createFBM(heightNoise, { - octaves: 8, - persistence: 0.5, - frequency: 0.01 - }); - - this.moistureFbm = createFBM(moistureNoise, { - octaves: 4, - persistence: 0.6, - frequency: 0.02 - }); - } - - getHeight(x: number, y: number): number { - // 基础高度 - let height = this.fbm.noise2D(x, y); - - // 添加山脉 - height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3; - - return (height + 1) * 0.5; // 归一化到 [0, 1] - } - - getBiome(x: number, y: number): string { - const height = this.getHeight(x, y); - const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5; - - if (height < 0.3) return 'water'; - if (height < 0.4) return 'beach'; - if (height > 0.8) return 'mountain'; - - if (moisture < 0.3) return 'desert'; - if (moisture > 0.7) return 'forest'; - return 'grassland'; - } -} -``` - -### 战利品系统 - -```typescript -import { createSeededRandom, createWeightedRandom, sample } from '@esengine/procgen'; - -interface LootItem { - id: string; - rarity: string; -} - -class LootSystem { - private rng: SeededRandom; - private raritySelector: WeightedRandom; - private lootTables: Map = new Map(); - - constructor(seed: number) { - this.rng = createSeededRandom(seed); - - this.raritySelector = createWeightedRandom([ - { value: 'common', weight: 60 }, - { value: 'uncommon', weight: 25 }, - { value: 'rare', weight: 10 }, - { value: 'legendary', weight: 5 } - ]); - - // 初始化战利品表 - this.lootTables.set('common', [/* ... */]); - this.lootTables.set('rare', [/* ... */]); - // ... - } - - generateLoot(count: number): LootItem[] { - const loot: LootItem[] = []; - - for (let i = 0; i < count; i++) { - const rarity = this.raritySelector.pick(this.rng); - const table = this.lootTables.get(rarity)!; - const item = pickOne(table, this.rng); - loot.push(item); - } - - return loot; - } - - // 保证可重现 - setSeed(seed: number): void { - this.rng = createSeededRandom(seed); - } -} -``` - -### 程序化敌人放置 - -```typescript -import { createSeededRandom } from '@esengine/procgen'; - -class EnemySpawner { - private rng: SeededRandom; - - constructor(seed: number) { - this.rng = createSeededRandom(seed); - } - - spawnEnemiesInArea( - centerX: number, - centerY: number, - radius: number, - count: number - ): Array<{ x: number; y: number; type: string }> { - const enemies: Array<{ x: number; y: number; type: string }> = []; - - for (let i = 0; i < count; i++) { - // 在圆内生成位置 - const pos = this.rng.nextPointInCircle(radius); - - // 随机选择敌人类型 - const type = this.rng.nextBool(0.2) ? 'elite' : 'normal'; - - enemies.push({ - x: centerX + pos.x, - y: centerY + pos.y, - type - }); - } - - return enemies; - } -} -``` - -### 程序化关卡布局 - -```typescript -import { createSeededRandom, shuffle } from '@esengine/procgen'; - -interface Room { - x: number; - y: number; - width: number; - height: number; - type: 'start' | 'combat' | 'treasure' | 'boss'; -} - -class DungeonGenerator { - private rng: SeededRandom; - - constructor(seed: number) { - this.rng = createSeededRandom(seed); - } - - generate(roomCount: number): Room[] { - const rooms: Room[] = []; - - // 生成房间 - for (let i = 0; i < roomCount; i++) { - rooms.push({ - x: this.rng.nextInt(0, 100), - y: this.rng.nextInt(0, 100), - width: this.rng.nextInt(5, 15), - height: this.rng.nextInt(5, 15), - type: 'combat' - }); - } - - // 随机分配特殊房间 - shuffle(rooms, this.rng); - rooms[0].type = 'start'; - rooms[1].type = 'treasure'; - rooms[rooms.length - 1].type = 'boss'; - - return rooms; - } -} -``` - -## 蓝图节点 - -Procgen 模块提供了可视化脚本支持的蓝图节点: - -### 噪声节点 - -- `SampleNoise2D` - 采样 2D 噪声 -- `SampleFBM` - 采样 FBM 噪声 - -### 随机节点 - -- `SeededRandom` - 生成随机浮点数 -- `SeededRandomInt` - 生成随机整数 -- `WeightedPick` - 加权随机选择 -- `ShuffleArray` - 洗牌数组 -- `PickRandom` - 随机选择元素 -- `SampleArray` - 采样数组 -- `RandomPointInCircle` - 圆内随机点 - -## 最佳实践 - -1. **使用种子保证可重现性** - ```typescript - // 保存种子以便重现相同结果 - const seed = Date.now(); - const rng = createSeededRandom(seed); - saveSeed(seed); - ``` - -2. **预计算加权选择器** - ```typescript - // 好:创建一次,多次使用 - const selector = createWeightedRandom(items); - for (let i = 0; i < 1000; i++) { - selector.pick(rng); - } - - // 不好:每次都创建 - for (let i = 0; i < 1000; i++) { - weightedPick(items, rng); - } - ``` - -3. **选择合适的噪声函数** - - Perlin:平滑过渡的地形、云彩 - - Simplex:性能要求高的场景 - - Worley:细胞、石头纹理 - - FBM:需要多层细节的自然效果 - -4. **调整 FBM 参数** - - `octaves`:越多细节越丰富,但性能开销越大 - - `persistence`:0.5 是常用值,越大高频细节越明显 - - `lacunarity`:通常为 2,控制频率增长速度 +- [噪声函数](./noise) - Perlin、Simplex、Worley、FBM +- [种子随机数](./random) - SeededRandom API 和分布方法 +- [采样工具](./sampling) - 加权随机、洗牌、采样 +- [实际示例](./examples) - 地形、战利品、关卡生成 diff --git a/docs/src/content/docs/modules/procgen/noise.md b/docs/src/content/docs/modules/procgen/noise.md new file mode 100644 index 00000000..fc0487e7 --- /dev/null +++ b/docs/src/content/docs/modules/procgen/noise.md @@ -0,0 +1,97 @@ +--- +title: "噪声函数" +description: "Perlin、Simplex、Worley 和 FBM" +--- + +## Perlin 噪声 + +经典的梯度噪声,输出范围 [-1, 1]: + +```typescript +import { createPerlinNoise } from '@esengine/procgen'; + +const perlin = createPerlinNoise(seed); + +// 2D 噪声 +const value2D = perlin.noise2D(x, y); + +// 3D 噪声 +const value3D = perlin.noise3D(x, y, z); +``` + +## Simplex 噪声 + +比 Perlin 更快、更少方向性偏差: + +```typescript +import { createSimplexNoise } from '@esengine/procgen'; + +const simplex = createSimplexNoise(seed); + +const value = simplex.noise2D(x, y); +``` + +## Worley 噪声 + +基于细胞的噪声,适合生成石头、细胞等纹理: + +```typescript +import { createWorleyNoise } from '@esengine/procgen'; + +const worley = createWorleyNoise(seed); + +// 返回到最近点的距离 +const distance = worley.noise2D(x, y); +``` + +## FBM (分形布朗运动) + +叠加多层噪声创建更丰富的细节: + +```typescript +import { createPerlinNoise, createFBM } from '@esengine/procgen'; + +const baseNoise = createPerlinNoise(seed); + +const fbm = createFBM(baseNoise, { + octaves: 6, // 层数(越多细节越丰富) + lacunarity: 2.0, // 频率倍增因子 + persistence: 0.5, // 振幅衰减因子 + frequency: 1.0, // 初始频率 + amplitude: 1.0 // 初始振幅 +}); + +// 标准 FBM +const value = fbm.noise2D(x, y); + +// Ridged FBM(脊状,适合山脉) +const ridged = fbm.ridged2D(x, y); + +// Turbulence(湍流) +const turb = fbm.turbulence2D(x, y); + +// Billowed(膨胀,适合云朵) +const cloud = fbm.billowed2D(x, y); +``` + +## FBM 参数说明 + +| 参数 | 说明 | 推荐值 | +|------|------|--------| +| `octaves` | 层数,越多细节越丰富 | 4-8 | +| `lacunarity` | 频率倍增因子 | 2.0 | +| `persistence` | 振幅衰减因子 | 0.5 | +| `frequency` | 初始频率 | 0.01-0.1 | +| `amplitude` | 初始振幅 | 1.0 | + +## 选择合适的噪声函数 + +| 噪声类型 | 适用场景 | +|----------|----------| +| Perlin | 平滑过渡的地形、云彩 | +| Simplex | 性能要求高的场景 | +| Worley | 细胞、石头、裂纹纹理 | +| FBM | 需要多层细节的自然效果 | +| Ridged FBM | 山脉、脊状地形 | +| Turbulence | 火焰、烟雾效果 | +| Billowed FBM | 云朵、软膨胀效果 | diff --git a/docs/src/content/docs/modules/procgen/random.md b/docs/src/content/docs/modules/procgen/random.md new file mode 100644 index 00000000..9eab3060 --- /dev/null +++ b/docs/src/content/docs/modules/procgen/random.md @@ -0,0 +1,92 @@ +--- +title: "种子随机数" +description: "SeededRandom API 和分布方法" +--- + +## SeededRandom + +基于 xorshift128+ 算法的确定性伪随机数生成器: + +```typescript +import { createSeededRandom } from '@esengine/procgen'; + +const rng = createSeededRandom(42); +``` + +## 基础方法 + +```typescript +// [0, 1) 浮点数 +rng.next(); + +// [min, max] 整数 +rng.nextInt(1, 10); + +// [min, max) 浮点数 +rng.nextFloat(0, 100); + +// 布尔值(可指定概率) +rng.nextBool(); // 50% +rng.nextBool(0.3); // 30% + +// 重置到初始状态 +rng.reset(); +``` + +## 分布方法 + +```typescript +// 正态分布(高斯分布) +rng.nextGaussian(); // 均值 0, 标准差 1 +rng.nextGaussian(100, 15); // 均值 100, 标准差 15 + +// 指数分布 +rng.nextExponential(); // λ = 1 +rng.nextExponential(0.5); // λ = 0.5 +``` + +## 几何方法 + +```typescript +// 圆内均匀分布的点 +const point = rng.nextPointInCircle(50); // { x, y } + +// 圆周上的点 +const edge = rng.nextPointOnCircle(50); // { x, y } + +// 球内均匀分布的点 +const point3D = rng.nextPointInSphere(50); // { x, y, z } + +// 随机方向向量 +const dir = rng.nextDirection2D(); // { x, y },长度为 1 +``` + +## 确定性保证 + +相同种子总是产生相同序列: + +```typescript +const rng1 = createSeededRandom(12345); +const rng2 = createSeededRandom(12345); + +// 这两个序列完全相同 +console.log(rng1.next()); // 0.xxx +console.log(rng2.next()); // 0.xxx (相同) + +console.log(rng1.nextInt(1, 100)); // N +console.log(rng2.nextInt(1, 100)); // N (相同) +``` + +## 保存和恢复状态 + +```typescript +// 保存种子以便重现 +const seed = Date.now(); +const rng = createSeededRandom(seed); +saveSeed(seed); + +// 之后可以使用相同种子重现 +const savedSeed = loadSeed(); +const rng2 = createSeededRandom(savedSeed); +// 将产生相同序列 +``` diff --git a/docs/src/content/docs/modules/procgen/sampling.md b/docs/src/content/docs/modules/procgen/sampling.md new file mode 100644 index 00000000..0fc3d4fc --- /dev/null +++ b/docs/src/content/docs/modules/procgen/sampling.md @@ -0,0 +1,135 @@ +--- +title: "采样工具" +description: "加权随机、洗牌和采样函数" +--- + +## 加权随机 API + +### WeightedRandom + +预计算累积权重,高效随机选择: + +```typescript +import { createWeightedRandom } from '@esengine/procgen'; + +const selector = createWeightedRandom([ + { value: 'apple', weight: 5 }, + { value: 'banana', weight: 3 }, + { value: 'cherry', weight: 2 } +]); + +// 使用种子随机数 +const result = selector.pick(rng); + +// 使用 Math.random +const result2 = selector.pickRandom(); + +// 获取概率 +console.log(selector.getProbability(0)); // 0.5 (5/10) +console.log(selector.size); // 3 +console.log(selector.totalWeight); // 10 +``` + +### 便捷函数 + +```typescript +import { weightedPick, weightedPickFromMap } from '@esengine/procgen'; + +// 从数组选择 +const item = weightedPick([ + { value: 'a', weight: 1 }, + { value: 'b', weight: 2 } +], rng); + +// 从对象选择 +const item2 = weightedPickFromMap({ + 'common': 60, + 'rare': 30, + 'epic': 10 +}, rng); +``` + +## 洗牌 API + +### shuffle / shuffleCopy + +Fisher-Yates 洗牌算法: + +```typescript +import { shuffle, shuffleCopy } from '@esengine/procgen'; + +const arr = [1, 2, 3, 4, 5]; + +// 原地洗牌 +shuffle(arr, rng); + +// 创建洗牌副本(不修改原数组) +const shuffled = shuffleCopy(arr, rng); +``` + +### pickOne + +随机选择一个元素: + +```typescript +import { pickOne } from '@esengine/procgen'; + +const items = ['a', 'b', 'c', 'd']; +const item = pickOne(items, rng); +``` + +## 采样 API + +### sample / sampleWithReplacement + +```typescript +import { sample, sampleWithReplacement } from '@esengine/procgen'; + +const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + +// 采样 3 个不重复元素 +const unique = sample(arr, 3, rng); + +// 采样 5 个(可重复) +const withRep = sampleWithReplacement(arr, 5, rng); +``` + +### randomIntegers + +生成范围内的随机整数数组: + +```typescript +import { randomIntegers } from '@esengine/procgen'; + +// 从 1-100 中随机选 5 个不重复的数 +const nums = randomIntegers(1, 100, 5, rng); +``` + +### weightedSample + +按权重采样(不重复): + +```typescript +import { weightedSample } from '@esengine/procgen'; + +const items = ['A', 'B', 'C', 'D', 'E']; +const weights = [10, 8, 6, 4, 2]; + +// 按权重选 3 个 +const selected = weightedSample(items, weights, 3, rng); +``` + +## 性能建议 + +```typescript +// 好:创建一次,多次使用 +const selector = createWeightedRandom(items); +for (let i = 0; i < 1000; i++) { + selector.pick(rng); +} + +// 不好:每次都创建 +for (let i = 0; i < 1000; i++) { + weightedPick(items, rng); +} +``` diff --git a/docs/src/content/docs/modules/spatial/aoi.md b/docs/src/content/docs/modules/spatial/aoi.md new file mode 100644 index 00000000..f9fb0f35 --- /dev/null +++ b/docs/src/content/docs/modules/spatial/aoi.md @@ -0,0 +1,166 @@ +--- +title: "AOI 兴趣区域" +description: "视野管理与进入/离开事件" +--- + +AOI (Area of Interest) 用于追踪实体之间的可见性关系,常用于 MMO 同步和 NPC AI 感知。 + +## createGridAOI + +```typescript +function createGridAOI(cellSize?: number): GridAOI +``` + +创建基于网格的 AOI 管理器。 + +**参数:** +- `cellSize` - 网格单元格大小(建议为平均视野范围的 1-2 倍) + +## 观察者管理 + +### addObserver + +添加观察者: + +```typescript +aoi.addObserver(player, position, { + viewRange: 200, // 视野范围 + observable: true // 是否可被其他观察者看到(默认 true) +}); + +// NPC 只观察不被观察 +aoi.addObserver(camera, position, { + viewRange: 500, + observable: false +}); +``` + +### removeObserver + +移除观察者: + +```typescript +aoi.removeObserver(player); +``` + +### updatePosition + +更新位置(自动触发进入/离开事件): + +```typescript +aoi.updatePosition(player, newPosition); +``` + +### updateViewRange + +更新视野范围: + +```typescript +// 获得增益后视野扩大 +aoi.updateViewRange(player, 300); +``` + +## 查询方法 + +### getEntitiesInView + +获取观察者视野内的所有实体: + +```typescript +const visible = aoi.getEntitiesInView(player); +for (const entity of visible) { + updateEntityForPlayer(player, entity); +} +``` + +### getObserversOf + +获取能看到指定实体的所有观察者: + +```typescript +const observers = aoi.getObserversOf(monster); +for (const observer of observers) { + notifyMonsterMoved(observer, monster); +} +``` + +### canSee + +检查是否可见: + +```typescript +if (aoi.canSee(player, enemy)) { + enemy.showHealthBar(); +} +``` + +## 事件系统 + +### 全局事件监听 + +```typescript +aoi.addListener((event) => { + switch (event.type) { + case 'enter': + console.log(`${event.observer} 看到了 ${event.target}`); + break; + case 'exit': + console.log(`${event.target} 离开了 ${event.observer} 的视野`); + break; + } +}); +``` + +### 实体特定事件监听 + +```typescript +// 只监听特定玩家的视野事件 +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); + } +}); +``` + +### 事件类型 + +```typescript +interface IAOIEvent { + type: 'enter' | 'exit' | 'update'; + observer: T; // 观察者(谁看到了变化) + target: T; // 目标(发生变化的对象) + position: IVector2; // 目标位置 +} +``` + +## 蓝图节点 + +- `GetEntitiesInView` - 获取视野内实体 +- `GetObserversOf` - 获取观察者 +- `CanSee` - 检查可见性 +- `OnEntityEnterView` - 进入视野事件 +- `OnEntityExitView` - 离开视野事件 + +## 服务令牌 + +在依赖注入场景中使用: + +```typescript +import { + SpatialIndexToken, + SpatialQueryToken, + AOIManagerToken, + createGridSpatialIndex, + createGridAOI +} from '@esengine/spatial'; + +// 注册服务 +services.register(SpatialIndexToken, createGridSpatialIndex(100)); +services.register(AOIManagerToken, createGridAOI(100)); + +// 获取服务 +const spatialIndex = services.get(SpatialIndexToken); +const aoiManager = services.get(AOIManagerToken); +``` diff --git a/docs/src/content/docs/modules/spatial/examples.md b/docs/src/content/docs/modules/spatial/examples.md new file mode 100644 index 00000000..c07d0146 --- /dev/null +++ b/docs/src/content/docs/modules/spatial/examples.md @@ -0,0 +1,184 @@ +--- +title: "实际示例" +description: "范围攻击、MMO 同步、AI 感知等场景" +--- + +## 范围攻击检测 + +```typescript +class CombatSystem { + private spatialIndex: ISpatialIndex; + + 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, // 无距离限制 + (entity) => { + const teamComp = entity.getComponent(TeamComponent); + return teamComp && teamComp.team !== team; + } + ); + } +} +``` + +## MMO 同步系统 + +```typescript +class SyncSystem { + private aoi: IAOIManager; + + constructor() { + this.aoi = createGridAOI(100); + + // 监听进入/离开事件 + 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); + } + + // 广播给所有能看到某玩家的其他玩家 + broadcastToObservers(player: Player, packet: Packet): void { + const observers = this.aoi.getObserversOf(player); + for (const observer of observers) { + this.sendToPlayer(observer, packet); + } + } +} +``` + +## NPC AI 感知 + +```typescript +class AIPerceptionSystem { + private aoi: IAOIManager; + + constructor() { + this.aoi = createGridAOI(50); + } + + setupNPC(npc: Entity): void { + const perception = npc.getComponent(PerceptionComponent); + + this.aoi.addObserver(npc, npc.position, { + viewRange: perception.range + }); + + // 监听该 NPC 的感知事件 + 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 { + // 更新所有 NPC 位置 + for (const npc of this.npcs) { + this.aoi.updatePosition(npc, npc.position); + } + } +} +``` + +## 技能目标选择 + +```typescript +class TargetingSystem { + private spatialIndex: ISpatialIndex; + + // 扇形范围技能 + findTargetsInCone( + origin: IVector2, + direction: IVector2, + range: number, + angle: number + ): Entity[] { + // 先用圆形范围粗筛 + const candidates = this.spatialIndex.findInRadius(origin, range); + + // 再精确筛选扇形内的目标 + 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; + }); + } + + // 射线穿透技能 + 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); + } +} +``` + +## 动态障碍物避让 + +```typescript +class ObstacleAvoidanceSystem { + private spatialIndex: ISpatialIndex; + + calculateAvoidanceForce(entity: Entity, velocity: IVector2): IVector2 { + const position = entity.position; + const lookAhead = 50; // 前方检测距离 + + // 检测前方障碍物 + const hit = this.spatialIndex.raycastFirst( + position, + normalize(velocity), + lookAhead, + (e) => e.hasComponent(ObstacleComponent) + ); + + if (!hit) return { x: 0, y: 0 }; + + // 计算避让力 + const avoidDirection = normalize({ + x: hit.normal.y, + y: -hit.normal.x + }); + + const urgency = 1 - (hit.distance / lookAhead); + return scale(avoidDirection, urgency * 100); + } +} +``` diff --git a/docs/src/content/docs/modules/spatial/index.md b/docs/src/content/docs/modules/spatial/index.md index 7b4c2518..3388d0de 100644 --- a/docs/src/content/docs/modules/spatial/index.md +++ b/docs/src/content/docs/modules/spatial/index.md @@ -1,5 +1,6 @@ --- title: "空间索引系统 (Spatial)" +description: "高效的空间查询和 AOI 管理" --- `@esengine/spatial` 提供了高效的空间查询和索引功能,包括范围查询、最近邻查询、射线检测和 AOI(兴趣区域)管理。 @@ -76,7 +77,9 @@ const visible = aoi.getEntitiesInView(player); | 方向 | 单向查询 | 双向追踪(谁看到谁) | | 场景 | 碰撞检测、范围攻击 | MMO 同步、NPC AI 感知 | -### IBounds 边界框 +### 核心接口 + +#### IBounds 边界框 ```typescript interface IBounds { @@ -87,7 +90,7 @@ interface IBounds { } ``` -### IRaycastHit 射线检测结果 +#### IRaycastHit 射线检测结果 ```typescript interface IRaycastHit { @@ -98,505 +101,9 @@ interface IRaycastHit { } ``` -## 空间索引 API +## 文档导航 -### createGridSpatialIndex - -```typescript -function createGridSpatialIndex(cellSize?: number): GridSpatialIndex -``` - -创建基于均匀网格的空间索引。 - -**参数:** -- `cellSize` - 网格单元格大小(默认 100) - -**选择合适的 cellSize:** -- 太小:内存占用高,查询效率降低 -- 太大:单元格内对象过多,遍历耗时 -- 建议:设置为对象平均分布间距的 1-2 倍 - -### 管理方法 - -#### insert - -插入对象到索引: - -```typescript -spatialIndex.insert(enemy, { x: 100, y: 200 }); -``` - -#### remove - -移除对象: - -```typescript -spatialIndex.remove(enemy); -``` - -#### update - -更新对象位置: - -```typescript -spatialIndex.update(enemy, { x: 150, y: 250 }); -``` - -#### clear - -清空索引: - -```typescript -spatialIndex.clear(); -``` - -### 查询方法 - -#### findInRadius - -查找圆形范围内的所有对象: - -```typescript -// 查找中心点 (100, 200) 半径 50 内的所有敌人 -const enemies = spatialIndex.findInRadius( - { x: 100, y: 200 }, - 50, - (entity) => entity.type === 'enemy' // 可选过滤器 -); -``` - -#### findInRect - -查找矩形区域内的所有对象: - -```typescript -import { createBounds } from '@esengine/spatial'; - -const bounds = createBounds(0, 0, 200, 200); -const entities = spatialIndex.findInRect(bounds); -``` - -#### findNearest - -查找最近的对象: - -```typescript -// 查找最近的敌人(最大搜索距离 500) -const nearest = spatialIndex.findNearest( - playerPosition, - 500, // maxDistance - (entity) => entity.type === 'enemy' -); - -if (nearest) { - attackTarget(nearest); -} -``` - -#### findKNearest - -查找最近的 K 个对象: - -```typescript -// 查找最近的 5 个敌人 -const nearestEnemies = spatialIndex.findKNearest( - playerPosition, - 5, // k - 500, // maxDistance - (entity) => entity.type === 'enemy' -); -``` - -#### raycast - -射线检测(返回所有命中): - -```typescript -const hits = spatialIndex.raycast( - origin, // 射线起点 - direction, // 射线方向(应归一化) - maxDistance, // 最大检测距离 - filter // 可选过滤器 -); - -// hits 按距离排序 -for (const hit of hits) { - console.log(`命中 ${hit.target} at ${hit.point}, 距离 ${hit.distance}`); -} -``` - -#### raycastFirst - -射线检测(仅返回第一个命中): - -```typescript -const hit = spatialIndex.raycastFirst(origin, direction, 1000); -if (hit) { - dealDamage(hit.target, calculateDamage(hit.distance)); -} -``` - -### 属性 - -```typescript -// 获取索引中的对象数量 -console.log(spatialIndex.count); - -// 获取所有对象 -const all = spatialIndex.getAll(); -``` - -## AOI 兴趣区域 API - -### createGridAOI - -```typescript -function createGridAOI(cellSize?: number): GridAOI -``` - -创建基于网格的 AOI 管理器。 - -**参数:** -- `cellSize` - 网格单元格大小(建议为平均视野范围的 1-2 倍) - -### 观察者管理 - -#### addObserver - -添加观察者: - -```typescript -aoi.addObserver(player, position, { - viewRange: 200, // 视野范围 - observable: true // 是否可被其他观察者看到(默认 true) -}); - -// NPC 只观察不被观察 -aoi.addObserver(camera, position, { - viewRange: 500, - observable: false -}); -``` - -#### removeObserver - -移除观察者: - -```typescript -aoi.removeObserver(player); -``` - -#### updatePosition - -更新位置(自动触发进入/离开事件): - -```typescript -aoi.updatePosition(player, newPosition); -``` - -#### updateViewRange - -更新视野范围: - -```typescript -// 获得增益后视野扩大 -aoi.updateViewRange(player, 300); -``` - -### 查询方法 - -#### getEntitiesInView - -获取观察者视野内的所有实体: - -```typescript -const visible = aoi.getEntitiesInView(player); -for (const entity of visible) { - updateEntityForPlayer(player, entity); -} -``` - -#### getObserversOf - -获取能看到指定实体的所有观察者: - -```typescript -const observers = aoi.getObserversOf(monster); -for (const observer of observers) { - notifyMonsterMoved(observer, monster); -} -``` - -#### canSee - -检查是否可见: - -```typescript -if (aoi.canSee(player, enemy)) { - enemy.showHealthBar(); -} -``` - -### 事件系统 - -#### 全局事件监听 - -```typescript -aoi.addListener((event) => { - switch (event.type) { - case 'enter': - console.log(`${event.observer} 看到了 ${event.target}`); - break; - case 'exit': - console.log(`${event.target} 离开了 ${event.observer} 的视野`); - break; - } -}); -``` - -#### 实体特定事件监听 - -```typescript -// 只监听特定玩家的视野事件 -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); - } -}); -``` - -#### 事件类型 - -```typescript -interface IAOIEvent { - type: 'enter' | 'exit' | 'update'; - observer: T; // 观察者(谁看到了变化) - target: T; // 目标(发生变化的对象) - position: IVector2; // 目标位置 -} -``` - -## 工具函数 - -### 边界框创建 - -```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); -``` - -### 几何检测 - -```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); // 更快,避免 sqrt -``` - -## 实际示例 - -### 范围攻击检测 - -```typescript -class CombatSystem { - private spatialIndex: ISpatialIndex; - - 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, // 无距离限制 - (entity) => { - const teamComp = entity.getComponent(TeamComponent); - return teamComp && teamComp.team !== team; - } - ); - } -} -``` - -### MMO 同步系统 - -```typescript -class SyncSystem { - private aoi: IAOIManager; - - constructor() { - this.aoi = createGridAOI(100); - - // 监听进入/离开事件 - 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); - } - - // 广播给所有能看到某玩家的其他玩家 - broadcastToObservers(player: Player, packet: Packet): void { - const observers = this.aoi.getObserversOf(player); - for (const observer of observers) { - this.sendToPlayer(observer, packet); - } - } -} -``` - -### NPC AI 感知 - -```typescript -class AIPerceptionSystem { - private aoi: IAOIManager; - - constructor() { - this.aoi = createGridAOI(50); - } - - setupNPC(npc: Entity): void { - const perception = npc.getComponent(PerceptionComponent); - - this.aoi.addObserver(npc, npc.position, { - viewRange: perception.range - }); - - // 监听该 NPC 的感知事件 - 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 { - // 更新所有 NPC 位置 - for (const npc of this.npcs) { - this.aoi.updatePosition(npc, npc.position); - } - } -} -``` - -## 蓝图节点 - -### 空间查询节点 - -- `FindInRadius` - 查找半径内的对象 -- `FindInRect` - 查找矩形内的对象 -- `FindNearest` - 查找最近的对象 -- `FindKNearest` - 查找最近的 K 个对象 -- `Raycast` - 射线检测 -- `RaycastFirst` - 射线检测(仅第一个) - -### AOI 节点 - -- `GetEntitiesInView` - 获取视野内实体 -- `GetObserversOf` - 获取观察者 -- `CanSee` - 检查可见性 -- `OnEntityEnterView` - 进入视野事件 -- `OnEntityExitView` - 离开视野事件 - -## 服务令牌 - -在依赖注入场景中使用: - -```typescript -import { - SpatialIndexToken, - SpatialQueryToken, - AOIManagerToken, - createGridSpatialIndex, - createGridAOI -} from '@esengine/spatial'; - -// 注册服务 -services.register(SpatialIndexToken, createGridSpatialIndex(100)); -services.register(AOIManagerToken, createGridAOI(100)); - -// 获取服务 -const spatialIndex = services.get(SpatialIndexToken); -const aoiManager = services.get(AOIManagerToken); -``` - -## 性能优化 - -1. **选择合适的 cellSize** - - 太小:内存占用高,单元格数量多 - - 太大:单元格内对象多,遍历慢 - - 经验法则:对象平均间距的 1-2 倍 - -2. **使用过滤器减少结果** - ```typescript - // 在空间查询阶段就过滤,而不是事后过滤 - spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy'); - ``` - -3. **使用 distanceSquared 代替 distance** - ```typescript - // 避免 sqrt 计算 - if (distanceSquared(a, b) < threshold * threshold) { ... } - ``` - -4. **批量更新优化** - ```typescript - // 如果有大量对象同时移动,考虑禁用事件后批量更新 - ``` +- [空间索引 API](./spatial-index) - 网格索引、范围查询、射线检测 +- [AOI 兴趣区域](./aoi) - 视野管理、进入/离开事件 +- [实际示例](./examples) - 范围攻击、MMO 同步、AI 感知 +- [工具与优化](./utilities) - 几何检测、性能优化技巧 diff --git a/docs/src/content/docs/modules/spatial/spatial-index.md b/docs/src/content/docs/modules/spatial/spatial-index.md new file mode 100644 index 00000000..3d08032e --- /dev/null +++ b/docs/src/content/docs/modules/spatial/spatial-index.md @@ -0,0 +1,159 @@ +--- +title: "空间索引 API" +description: "网格索引、范围查询、射线检测" +--- + +## createGridSpatialIndex + +```typescript +function createGridSpatialIndex(cellSize?: number): GridSpatialIndex +``` + +创建基于均匀网格的空间索引。 + +**参数:** +- `cellSize` - 网格单元格大小(默认 100) + +**选择合适的 cellSize:** +- 太小:内存占用高,查询效率降低 +- 太大:单元格内对象过多,遍历耗时 +- 建议:设置为对象平均分布间距的 1-2 倍 + +## 管理方法 + +### insert + +插入对象到索引: + +```typescript +spatialIndex.insert(enemy, { x: 100, y: 200 }); +``` + +### remove + +移除对象: + +```typescript +spatialIndex.remove(enemy); +``` + +### update + +更新对象位置: + +```typescript +spatialIndex.update(enemy, { x: 150, y: 250 }); +``` + +### clear + +清空索引: + +```typescript +spatialIndex.clear(); +``` + +## 查询方法 + +### findInRadius + +查找圆形范围内的所有对象: + +```typescript +// 查找中心点 (100, 200) 半径 50 内的所有敌人 +const enemies = spatialIndex.findInRadius( + { x: 100, y: 200 }, + 50, + (entity) => entity.type === 'enemy' // 可选过滤器 +); +``` + +### findInRect + +查找矩形区域内的所有对象: + +```typescript +import { createBounds } from '@esengine/spatial'; + +const bounds = createBounds(0, 0, 200, 200); +const entities = spatialIndex.findInRect(bounds); +``` + +### findNearest + +查找最近的对象: + +```typescript +// 查找最近的敌人(最大搜索距离 500) +const nearest = spatialIndex.findNearest( + playerPosition, + 500, // maxDistance + (entity) => entity.type === 'enemy' +); + +if (nearest) { + attackTarget(nearest); +} +``` + +### findKNearest + +查找最近的 K 个对象: + +```typescript +// 查找最近的 5 个敌人 +const nearestEnemies = spatialIndex.findKNearest( + playerPosition, + 5, // k + 500, // maxDistance + (entity) => entity.type === 'enemy' +); +``` + +### raycast + +射线检测(返回所有命中): + +```typescript +const hits = spatialIndex.raycast( + origin, // 射线起点 + direction, // 射线方向(应归一化) + maxDistance, // 最大检测距离 + filter // 可选过滤器 +); + +// hits 按距离排序 +for (const hit of hits) { + console.log(`命中 ${hit.target} at ${hit.point}, 距离 ${hit.distance}`); +} +``` + +### raycastFirst + +射线检测(仅返回第一个命中): + +```typescript +const hit = spatialIndex.raycastFirst(origin, direction, 1000); +if (hit) { + dealDamage(hit.target, calculateDamage(hit.distance)); +} +``` + +## 属性 + +```typescript +// 获取索引中的对象数量 +console.log(spatialIndex.count); + +// 获取所有对象 +const all = spatialIndex.getAll(); +``` + +## 蓝图节点 + +- `FindInRadius` - 查找半径内的对象 +- `FindInRect` - 查找矩形内的对象 +- `FindNearest` - 查找最近的对象 +- `FindKNearest` - 查找最近的 K 个对象 +- `Raycast` - 射线检测 +- `RaycastFirst` - 射线检测(仅第一个) diff --git a/docs/src/content/docs/modules/spatial/utilities.md b/docs/src/content/docs/modules/spatial/utilities.md new file mode 100644 index 00000000..1b68554f --- /dev/null +++ b/docs/src/content/docs/modules/spatial/utilities.md @@ -0,0 +1,149 @@ +--- +title: "工具与优化" +description: "几何检测函数与性能优化技巧" +--- + +## 边界框创建 + +```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); +``` + +## 几何检测 + +```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); // 更快,避免 sqrt +``` + +## 性能优化 + +### 1. 选择合适的 cellSize + +- **太小**:内存占用高,单元格数量多 +- **太大**:单元格内对象多,遍历慢 +- **经验法则**:对象平均间距的 1-2 倍 + +```typescript +// 场景中对象平均间距约 50 单位 +const spatialIndex = createGridSpatialIndex(75); // 1.5 倍 +``` + +### 2. 使用过滤器减少结果 + +```typescript +// 在空间查询阶段就过滤,而不是事后过滤 +spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy'); + +// 避免这样做 +const all = spatialIndex.findInRadius(center, radius); +const enemies = all.filter(e => e.type === 'enemy'); // 多余的遍历 +``` + +### 3. 使用 distanceSquared 代替 distance + +```typescript +// 避免 sqrt 计算 +const thresholdSq = threshold * threshold; + +if (distanceSquared(a, b) < thresholdSq) { + // 在范围内 +} +``` + +### 4. 批量更新优化 + +```typescript +// 如果需要同时更新大量对象 +// 考虑在批量更新前后禁用/启用事件 +aoi.disableEvents(); +for (const entity of entities) { + aoi.updatePosition(entity, entity.position); +} +aoi.enableEvents(); +aoi.flushEvents(); // 一次性发送所有事件 +``` + +### 5. 分层索引 + +对于超大场景,可以使用多个空间索引: + +```typescript +// 静态物体使用大网格(查询少) +const staticIndex = createGridSpatialIndex(500); + +// 动态物体使用小网格(更新频繁) +const dynamicIndex = createGridSpatialIndex(50); + +// 查询时合并结果 +function findInRadius(center: IVector2, radius: number): Entity[] { + return [ + ...staticIndex.findInRadius(center, radius), + ...dynamicIndex.findInRadius(center, radius) + ]; +} +``` + +### 6. 减少查询频率 + +```typescript +class AISystem { + private lastQueryTime = new Map(); + private queryInterval = 100; // 每 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); + } + } + } +} +``` + +## 内存管理 + +```typescript +// 及时清理不需要的索引 +spatialIndex.remove(destroyedEntity); + +// 场景切换时完全清空 +spatialIndex.clear(); +aoi.clear(); +``` diff --git a/docs/src/content/docs/modules/timer/api.md b/docs/src/content/docs/modules/timer/api.md new file mode 100644 index 00000000..de5741fc --- /dev/null +++ b/docs/src/content/docs/modules/timer/api.md @@ -0,0 +1,221 @@ +--- +title: "API 参考" +description: "定时器和冷却系统完整 API" +--- + +## createTimerService + +```typescript +function createTimerService(config?: TimerServiceConfig): ITimerService +``` + +**配置选项:** + +| 属性 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `maxTimers` | `number` | `0` | 最大定时器数量(0 表示无限制) | +| `maxCooldowns` | `number` | `0` | 最大冷却数量(0 表示无限制) | + +## 定时器 API + +### schedule + +调度一次性定时器: + +```typescript +const handle = timerService.schedule('explosion', 2000, () => { + createExplosion(); +}); + +// 提前取消 +handle.cancel(); +``` + +### scheduleRepeating + +调度重复定时器: + +```typescript +// 每秒执行 +timerService.scheduleRepeating('regen', 1000, () => { + player.hp += 5; +}); + +// 立即执行一次,然后每秒重复 +timerService.scheduleRepeating('tick', 1000, () => { + console.log('Tick'); +}, true); // immediate = true +``` + +### cancel / cancelById + +取消定时器: + +```typescript +// 通过句柄取消 +handle.cancel(); +// 或 +timerService.cancel(handle); + +// 通过 ID 取消 +timerService.cancelById('regen'); +``` + +### hasTimer + +检查定时器是否存在: + +```typescript +if (timerService.hasTimer('explosion')) { + console.log('Explosion is pending'); +} +``` + +### getTimerInfo + +获取定时器信息: + +```typescript +const info = timerService.getTimerInfo('explosion'); +if (info) { + console.log(`剩余时间: ${info.remaining}ms`); + console.log(`是否重复: ${info.repeating}`); +} +``` + +## 冷却 API + +### startCooldown + +开始冷却: + +```typescript +// 5秒冷却 +timerService.startCooldown('skill_fireball', 5000); +``` + +### isCooldownReady / isOnCooldown + +检查冷却状态: + +```typescript +if (timerService.isCooldownReady('skill_fireball')) { + // 可以使用技能 + castFireball(); + timerService.startCooldown('skill_fireball', 5000); +} else { + console.log('技能还在冷却中'); +} + +// 或使用 isOnCooldown +if (timerService.isOnCooldown('skill_fireball')) { + console.log('冷却中...'); +} +``` + +### getCooldownProgress / getCooldownRemaining + +获取冷却进度: + +```typescript +// 进度 0-1(0=刚开始,1=完成) +const progress = timerService.getCooldownProgress('skill_fireball'); +console.log(`冷却进度: ${(progress * 100).toFixed(0)}%`); + +// 剩余时间(毫秒) +const remaining = timerService.getCooldownRemaining('skill_fireball'); +console.log(`剩余时间: ${(remaining / 1000).toFixed(1)}s`); +``` + +### getCooldownInfo + +获取完整冷却信息: + +```typescript +const info = timerService.getCooldownInfo('skill_fireball'); +if (info) { + console.log(`总时长: ${info.duration}ms`); + console.log(`剩余: ${info.remaining}ms`); + console.log(`进度: ${info.progress}`); + console.log(`就绪: ${info.isReady}`); +} +``` + +### resetCooldown / clearAllCooldowns + +重置冷却: + +```typescript +// 重置单个冷却 +timerService.resetCooldown('skill_fireball'); + +// 清除所有冷却(例如角色复活时) +timerService.clearAllCooldowns(); +``` + +## 生命周期 + +### update + +更新定时器服务(需要每帧调用): + +```typescript +function gameLoop(deltaTime: number) { + // deltaTime 单位是毫秒 + timerService.update(deltaTime); +} +``` + +### clear + +清除所有定时器和冷却: + +```typescript +timerService.clear(); +``` + +## 调试属性 + +```typescript +// 获取活跃定时器数量 +console.log(timerService.activeTimerCount); + +// 获取活跃冷却数量 +console.log(timerService.activeCooldownCount); + +// 获取所有活跃定时器 ID +const timerIds = timerService.getActiveTimerIds(); + +// 获取所有活跃冷却 ID +const cooldownIds = timerService.getActiveCooldownIds(); +``` + +## 蓝图节点 + +### 冷却节点 + +- `StartCooldown` - 开始冷却 +- `IsCooldownReady` - 检查冷却是否就绪 +- `GetCooldownProgress` - 获取冷却进度 +- `GetCooldownInfo` - 获取详细冷却信息 +- `ResetCooldown` - 重置冷却 + +### 定时器节点 + +- `HasTimer` - 检查定时器是否存在 +- `CancelTimer` - 取消定时器 +- `GetTimerRemaining` - 获取定时器剩余时间 + +## 服务令牌 + +在依赖注入场景中使用: + +```typescript +import { TimerServiceToken, createTimerService } from '@esengine/timer'; + +// 注册服务 +services.register(TimerServiceToken, createTimerService()); + +// 获取服务 +const timerService = services.get(TimerServiceToken); +``` diff --git a/docs/src/content/docs/modules/timer/best-practices.md b/docs/src/content/docs/modules/timer/best-practices.md new file mode 100644 index 00000000..8988ab67 --- /dev/null +++ b/docs/src/content/docs/modules/timer/best-practices.md @@ -0,0 +1,223 @@ +--- +title: "最佳实践" +description: "使用建议和 ECS 集成" +--- + +## 使用建议 + +### 1. 使用有意义的 ID + +使用描述性的 ID 便于调试和管理: + +```typescript +// 好 +timerService.startCooldown('skill_fireball', 5000); +timerService.schedule('explosion_wave_1', 1000, callback); + +// 不好 +timerService.startCooldown('cd1', 5000); +timerService.schedule('t1', 1000, callback); +``` + +### 2. 避免重复 ID + +相同 ID 的定时器会覆盖之前的,使用唯一 ID: + +```typescript +// 使用唯一 ID +const uniqueId = `explosion_${entity.id}_${Date.now()}`; +timerService.schedule(uniqueId, 1000, callback); + +// 或使用计数器 +let timerCounter = 0; +const timerId = `timer_${++timerCounter}`; +``` + +### 3. 及时清理 + +在适当时机清理不需要的定时器和冷却: + +```typescript +class Entity { + private timerId: string; + + onDestroy(): void { + // 实体销毁时清理定时器 + this.timerService.cancelById(this.timerId); + } +} + +class Scene { + onUnload(): void { + // 场景卸载时清除所有 + this.timerService.clear(); + } +} +``` + +### 4. 配置限制 + +在生产环境考虑设置最大数量限制: + +```typescript +const timerService = createTimerService({ + maxTimers: 1000, + maxCooldowns: 500 +}); +``` + +### 5. 批量管理 + +使用前缀管理相关定时器: + +```typescript +// 为某个实体的所有定时器使用统一前缀 +const prefix = `entity_${entityId}_`; + +timerService.schedule(`${prefix}explosion`, 1000, callback1); +timerService.schedule(`${prefix}effect`, 2000, callback2); + +// 清理时可以遍历查找 +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 集成 + +### 定时器组件 + +```typescript +import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework'; +import { createTimerService, type ITimerService } from '@esengine/timer'; + +// 定时器组件 +class TimerComponent extends Component { + timerService: ITimerService; + + constructor() { + super(); + this.timerService = createTimerService(); + } +} + +// 定时器系统 +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); + } +} +``` + +### 冷却组件(共享冷却) + +```typescript +// 冷却组件(用于共享冷却) +class CooldownComponent extends Component { + constructor(public timerService: ITimerService) { + super(); + } +} + +// 多个实体共享同一个冷却服务 +const sharedCooldowns = createTimerService(); + +entity1.addComponent(new CooldownComponent(sharedCooldowns)); +entity2.addComponent(new CooldownComponent(sharedCooldowns)); +``` + +### 全局定时器服务 + +```typescript +// 使用服务容器管理全局定时器 +import { TimerServiceToken, createTimerService } from '@esengine/timer'; + +// 注册全局服务 +services.register(TimerServiceToken, createTimerService()); + +// 在系统中使用 +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); + }); + } +} +``` + +## 性能优化 + +### 合并更新 + +如果有多个独立的定时器服务,考虑合并为一个: + +```typescript +// 不推荐:每个实体有自己的定时器服务 +class BadEntity { + private timerService = createTimerService(); // 内存浪费 +} + +// 推荐:共享定时器服务 +class GoodSystem { + private timerService = createTimerService(); + + addTimer(entityId: number, callback: () => void): void { + this.timerService.schedule(`entity_${entityId}`, 1000, callback); + } +} +``` + +### 避免频繁创建 + +重用定时器 ID 而不是创建新的: + +```typescript +// 不推荐:每次都创建新定时器 +function onHit(): void { + timerService.schedule(`hit_${Date.now()}`, 100, showHitEffect); +} + +// 推荐:取消旧定时器后复用 ID +function onHit(): void { + timerService.cancelById('hit_effect'); + timerService.schedule('hit_effect', 100, showHitEffect); +} +``` + +### 使用冷却而不是定时器 + +对于不需要回调的场景,使用冷却更高效: + +```typescript +// 使用冷却限制攻击频率 +if (timerService.isCooldownReady('attack')) { + attack(); + timerService.startCooldown('attack', 1000); +} + +// 而不是 +timerService.schedule('attack_cooldown', 1000, () => { + canAttack = true; +}); +``` diff --git a/docs/src/content/docs/modules/timer/examples.md b/docs/src/content/docs/modules/timer/examples.md new file mode 100644 index 00000000..d07dabc8 --- /dev/null +++ b/docs/src/content/docs/modules/timer/examples.md @@ -0,0 +1,235 @@ +--- +title: "实际示例" +description: "技能冷却、DOT 效果、BUFF 系统等场景" +--- + +## 技能冷却系统 + +```typescript +import { createTimerService, type ITimerService } from '@esengine/timer'; + +class SkillSystem { + private timerService: ITimerService; + private skills: Map = 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; + + // 检查冷却 + if (!this.timerService.isCooldownReady(skillId)) { + const remaining = this.timerService.getCooldownRemaining(skillId); + console.log(`技能 ${skillId} 冷却中,剩余 ${remaining}ms`); + return false; + } + + // 使用技能 + this.executeSkill(skill); + + // 开始冷却 + 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 +} +``` + +## 延迟和定时效果 + +```typescript +class EffectSystem { + private timerService: ITimerService; + + constructor(timerService: ITimerService) { + this.timerService = timerService; + } + + // 延迟爆炸 + scheduleExplosion(position: { x: number; y: number }, delay: number): void { + this.timerService.schedule(`explosion_${Date.now()}`, delay, () => { + this.createExplosion(position); + }); + } + + // DOT 伤害(每秒造成伤害) + 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 效果(持续一段时间) + applyBuff(target: Entity, buffId: string, duration: number): void { + target.addBuff(buffId); + + this.timerService.schedule(`buff_expire_${buffId}`, duration, () => { + target.removeBuff(buffId); + }); + } +} +``` + +## 技能连击系统 + +```typescript +class ComboSystem { + private timerService: ITimerService; + private comboCount = 0; + private comboWindowId = 'combo_window'; + + constructor(timerService: ITimerService) { + this.timerService = timerService; + } + + onAttack(): void { + // 增加连击计数 + this.comboCount++; + + // 取消之前的连击窗口 + this.timerService.cancelById(this.comboWindowId); + + // 开启新的连击窗口(2秒内无操作则重置) + 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; + } +} +``` + +## 自动保存系统 + +```typescript +class AutoSaveSystem { + private timerService: ITimerService; + + constructor(timerService: ITimerService) { + this.timerService = timerService; + this.startAutoSave(); + } + + private startAutoSave(): void { + // 每 5 分钟自动保存 + this.timerService.scheduleRepeating('autosave', 5 * 60 * 1000, () => { + this.saveGame(); + console.log('Game auto-saved'); + }); + } + + private saveGame(): void { + // 保存逻辑 + } + + stopAutoSave(): void { + this.timerService.cancelById('autosave'); + } +} +``` + +## 技能蓄力系统 + +```typescript +class ChargeSkillSystem { + private timerService: ITimerService; + private chargeStartTime = 0; + private maxChargeTime = 3000; // 3秒最大蓄力 + + constructor(timerService: ITimerService) { + this.timerService = timerService; + } + + startCharge(): void { + this.chargeStartTime = performance.now(); + + // 蓄力满时自动释放 + 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 伤害 + console.log(`Release skill with ${damage} damage (${(chargePercent * 100).toFixed(0)}% charge)`); + } +} +``` + +## 任务计时器 + +```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 更新 + 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 { + // 更新 UI + } +} +``` diff --git a/docs/src/content/docs/modules/timer/index.md b/docs/src/content/docs/modules/timer/index.md index ce6820a6..c8c5a8fd 100644 --- a/docs/src/content/docs/modules/timer/index.md +++ b/docs/src/content/docs/modules/timer/index.md @@ -1,5 +1,6 @@ --- title: "定时器系统 (Timer)" +description: "灵活的定时器和冷却系统" --- `@esengine/timer` 提供了一个灵活的定时器和冷却系统,用于游戏中的延迟执行、重复任务、技能冷却等场景。 @@ -68,8 +69,6 @@ interface TimerHandle { ### TimerInfo -定时器信息对象: - ```typescript interface TimerInfo { readonly id: string; // 定时器 ID @@ -81,8 +80,6 @@ interface TimerInfo { ### CooldownInfo -冷却信息对象: - ```typescript interface CooldownInfo { readonly id: string; // 冷却 ID @@ -93,389 +90,8 @@ interface CooldownInfo { } ``` -## API 参考 +## 文档导航 -### createTimerService - -```typescript -function createTimerService(config?: TimerServiceConfig): ITimerService -``` - -**配置选项:** - -| 属性 | 类型 | 默认值 | 描述 | -|------|------|--------|------| -| `maxTimers` | `number` | `0` | 最大定时器数量(0 表示无限制) | -| `maxCooldowns` | `number` | `0` | 最大冷却数量(0 表示无限制) | - -### 定时器 API - -#### schedule - -调度一次性定时器: - -```typescript -const handle = timerService.schedule('explosion', 2000, () => { - createExplosion(); -}); - -// 提前取消 -handle.cancel(); -``` - -#### scheduleRepeating - -调度重复定时器: - -```typescript -// 每秒执行 -timerService.scheduleRepeating('regen', 1000, () => { - player.hp += 5; -}); - -// 立即执行一次,然后每秒重复 -timerService.scheduleRepeating('tick', 1000, () => { - console.log('Tick'); -}, true); // immediate = true -``` - -#### cancel / cancelById - -取消定时器: - -```typescript -// 通过句柄取消 -handle.cancel(); -// 或 -timerService.cancel(handle); - -// 通过 ID 取消 -timerService.cancelById('regen'); -``` - -#### hasTimer - -检查定时器是否存在: - -```typescript -if (timerService.hasTimer('explosion')) { - console.log('Explosion is pending'); -} -``` - -#### getTimerInfo - -获取定时器信息: - -```typescript -const info = timerService.getTimerInfo('explosion'); -if (info) { - console.log(`剩余时间: ${info.remaining}ms`); - console.log(`是否重复: ${info.repeating}`); -} -``` - -### 冷却 API - -#### startCooldown - -开始冷却: - -```typescript -// 5秒冷却 -timerService.startCooldown('skill_fireball', 5000); -``` - -#### isCooldownReady / isOnCooldown - -检查冷却状态: - -```typescript -if (timerService.isCooldownReady('skill_fireball')) { - // 可以使用技能 - castFireball(); - timerService.startCooldown('skill_fireball', 5000); -} else { - console.log('技能还在冷却中'); -} - -// 或使用 isOnCooldown -if (timerService.isOnCooldown('skill_fireball')) { - console.log('冷却中...'); -} -``` - -#### getCooldownProgress / getCooldownRemaining - -获取冷却进度: - -```typescript -// 进度 0-1(0=刚开始,1=完成) -const progress = timerService.getCooldownProgress('skill_fireball'); -console.log(`冷却进度: ${(progress * 100).toFixed(0)}%`); - -// 剩余时间(毫秒) -const remaining = timerService.getCooldownRemaining('skill_fireball'); -console.log(`剩余时间: ${(remaining / 1000).toFixed(1)}s`); -``` - -#### getCooldownInfo - -获取完整冷却信息: - -```typescript -const info = timerService.getCooldownInfo('skill_fireball'); -if (info) { - console.log(`总时长: ${info.duration}ms`); - console.log(`剩余: ${info.remaining}ms`); - console.log(`进度: ${info.progress}`); - console.log(`就绪: ${info.isReady}`); -} -``` - -#### resetCooldown / clearAllCooldowns - -重置冷却: - -```typescript -// 重置单个冷却 -timerService.resetCooldown('skill_fireball'); - -// 清除所有冷却(例如角色复活时) -timerService.clearAllCooldowns(); -``` - -### 生命周期 - -#### update - -更新定时器服务(需要每帧调用): - -```typescript -function gameLoop(deltaTime: number) { - // deltaTime 单位是毫秒 - timerService.update(deltaTime); -} -``` - -#### clear - -清除所有定时器和冷却: - -```typescript -timerService.clear(); -``` - -### 调试属性 - -```typescript -// 获取活跃定时器数量 -console.log(timerService.activeTimerCount); - -// 获取活跃冷却数量 -console.log(timerService.activeCooldownCount); - -// 获取所有活跃定时器 ID -const timerIds = timerService.getActiveTimerIds(); - -// 获取所有活跃冷却 ID -const cooldownIds = timerService.getActiveCooldownIds(); -``` - -## 实际示例 - -### 技能冷却系统 - -```typescript -import { createTimerService, type ITimerService } from '@esengine/timer'; - -class SkillSystem { - private timerService: ITimerService; - private skills: Map = 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; - - // 检查冷却 - if (!this.timerService.isCooldownReady(skillId)) { - const remaining = this.timerService.getCooldownRemaining(skillId); - console.log(`技能 ${skillId} 冷却中,剩余 ${remaining}ms`); - return false; - } - - // 使用技能 - this.executeSkill(skill); - - // 开始冷却 - 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 -} -``` - -### 延迟和定时效果 - -```typescript -class EffectSystem { - private timerService: ITimerService; - - constructor(timerService: ITimerService) { - this.timerService = timerService; - } - - // 延迟爆炸 - scheduleExplosion(position: { x: number; y: number }, delay: number): void { - this.timerService.schedule(`explosion_${Date.now()}`, delay, () => { - this.createExplosion(position); - }); - } - - // DOT 伤害(每秒造成伤害) - 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 效果(持续一段时间) - applyBuff(target: Entity, buffId: string, duration: number): void { - target.addBuff(buffId); - - this.timerService.schedule(`buff_expire_${buffId}`, duration, () => { - target.removeBuff(buffId); - }); - } -} -``` - -### 与 ECS 集成 - -```typescript -import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework'; -import { createTimerService, type ITimerService } from '@esengine/timer'; - -// 定时器组件 -class TimerComponent extends Component { - timerService: ITimerService; - - constructor() { - super(); - this.timerService = createTimerService(); - } -} - -// 定时器系统 -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); - } -} - -// 冷却组件(用于共享冷却) -class CooldownComponent extends Component { - constructor(public timerService: ITimerService) { - super(); - } -} -``` - -## 蓝图节点 - -Timer 模块提供了可视化脚本支持的蓝图节点: - -### 冷却节点 - -- `StartCooldown` - 开始冷却 -- `IsCooldownReady` - 检查冷却是否就绪 -- `GetCooldownProgress` - 获取冷却进度 -- `GetCooldownInfo` - 获取详细冷却信息 -- `ResetCooldown` - 重置冷却 - -### 定时器节点 - -- `HasTimer` - 检查定时器是否存在 -- `CancelTimer` - 取消定时器 -- `GetTimerRemaining` - 获取定时器剩余时间 - -## 服务令牌 - -在依赖注入场景中使用: - -```typescript -import { TimerServiceToken, createTimerService } from '@esengine/timer'; - -// 注册服务 -services.register(TimerServiceToken, createTimerService()); - -// 获取服务 -const timerService = services.get(TimerServiceToken); -``` - -## 最佳实践 - -1. **使用有意义的 ID**:使用描述性的 ID 便于调试和管理 - ```typescript - // 好 - timerService.startCooldown('skill_fireball', 5000); - - // 不好 - timerService.startCooldown('cd1', 5000); - ``` - -2. **避免重复 ID**:相同 ID 的定时器会覆盖之前的 - ```typescript - // 使用唯一 ID - const uniqueId = `explosion_${entity.id}_${Date.now()}`; - timerService.schedule(uniqueId, 1000, callback); - ``` - -3. **及时清理**:在适当时机清理不需要的定时器和冷却 - ```typescript - // 实体销毁时 - onDestroy() { - this.timerService.cancelById(this.timerId); - } - ``` - -4. **配置限制**:在生产环境考虑设置最大数量限制 - ```typescript - const timerService = createTimerService({ - maxTimers: 1000, - maxCooldowns: 500 - }); - ``` +- [API 参考](./api) - 完整的定时器和冷却 API +- [实际示例](./examples) - 技能冷却、DOT 效果、BUFF 系统 +- [最佳实践](./best-practices) - 使用建议和 ECS 集成