docs: migrate documentation from VitePress to Starlight (#361)
- Replace VitePress with Astro Starlight for documentation - Reorganize document structure with better hierarchy - Split large documents into sub-modules for better readability - Add new sections: component, entity-query, serialization, system - Support both Chinese (default) and English documentation - Add responsive design and improved navigation
This commit is contained in:
21
docs/.gitignore
vendored
Normal file
21
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
@@ -1,329 +0,0 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const corePackageJson = JSON.parse(
|
||||
readFileSync(join(__dirname, '../../packages/framework/core/package.json'), 'utf-8')
|
||||
)
|
||||
|
||||
// Import i18n messages
|
||||
import en from './i18n/en.json' with { type: 'json' }
|
||||
import zh from './i18n/zh.json' with { type: 'json' }
|
||||
|
||||
// 创建侧边栏配置 | Create sidebar config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createSidebar(t, prefix = '') {
|
||||
return {
|
||||
[`${prefix}/guide/`]: [
|
||||
{
|
||||
text: t.sidebar.gettingStarted,
|
||||
items: [
|
||||
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.coreConcepts,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
|
||||
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
|
||||
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
|
||||
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
|
||||
{
|
||||
text: t.sidebar.system,
|
||||
link: `${prefix}/guide/system`,
|
||||
items: [
|
||||
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.scene,
|
||||
link: `${prefix}/guide/scene`,
|
||||
items: [
|
||||
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
|
||||
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` },
|
||||
{ text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` }
|
||||
]
|
||||
},
|
||||
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
|
||||
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
|
||||
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
|
||||
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.advancedFeatures,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
|
||||
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.platformAdapters,
|
||||
link: `${prefix}/guide/platform-adapter`,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
|
||||
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
|
||||
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
|
||||
]
|
||||
}
|
||||
],
|
||||
// 模块总览侧边栏 | Modules overview sidebar
|
||||
[`${prefix}/modules/`]: [
|
||||
{
|
||||
text: t.sidebar.modulesOverview,
|
||||
link: `${prefix}/modules/`,
|
||||
items: [
|
||||
{
|
||||
text: t.sidebar.aiModules,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.behaviorTree, link: `${prefix}/modules/behavior-tree/` },
|
||||
{ text: t.sidebar.fsm, link: `${prefix}/modules/fsm/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.gameplayModules,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.timer, link: `${prefix}/modules/timer/` },
|
||||
{ text: t.sidebar.spatial, link: `${prefix}/modules/spatial/` },
|
||||
{ text: t.sidebar.pathfinding, link: `${prefix}/modules/pathfinding/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.toolModules,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.blueprint, link: `${prefix}/modules/blueprint/` },
|
||||
{ text: t.sidebar.procgen, link: `${prefix}/modules/procgen/` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.networkModules,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: t.sidebar.network, link: `${prefix}/modules/network/` }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
// 行为树模块侧边栏 | Behavior tree module sidebar
|
||||
[`${prefix}/modules/behavior-tree/`]: [
|
||||
{
|
||||
text: t.sidebar.behaviorTree,
|
||||
items: [
|
||||
{ text: t.sidebar.btGettingStarted, link: `${prefix}/modules/behavior-tree/getting-started` },
|
||||
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/modules/behavior-tree/core-concepts` },
|
||||
{ text: t.sidebar.btEditorGuide, link: `${prefix}/modules/behavior-tree/editor-guide` },
|
||||
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/modules/behavior-tree/editor-workflow` },
|
||||
{ text: t.sidebar.btCustomActions, link: `${prefix}/modules/behavior-tree/custom-actions` },
|
||||
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/modules/behavior-tree/cocos-integration` },
|
||||
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/modules/behavior-tree/laya-integration` },
|
||||
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/modules/behavior-tree/advanced-usage` },
|
||||
{ text: t.sidebar.btBestPractices, link: `${prefix}/modules/behavior-tree/best-practices` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/examples/`]: [
|
||||
{
|
||||
text: t.sidebar.examples,
|
||||
items: [
|
||||
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
|
||||
]
|
||||
}
|
||||
],
|
||||
[`${prefix}/api/`]: [
|
||||
{
|
||||
text: t.sidebar.apiReference,
|
||||
items: [
|
||||
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.sidebar.coreClasses,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Core', link: `${prefix}/api/classes/Core` },
|
||||
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
|
||||
{ text: 'World', link: `${prefix}/api/classes/World` },
|
||||
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
|
||||
{ text: 'Component', link: `${prefix}/api/classes/Component` },
|
||||
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.systemClasses,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
|
||||
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
|
||||
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.utilities,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
|
||||
{ text: 'Time', link: `${prefix}/api/classes/Time` },
|
||||
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
|
||||
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.interfaces,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
|
||||
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
|
||||
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
|
||||
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.decorators,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
|
||||
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: t.sidebar.enums,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
|
||||
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建导航配置 | Create nav config
|
||||
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
|
||||
function createNav(t, prefix = '') {
|
||||
return [
|
||||
{ text: t.nav.home, link: `${prefix}/` },
|
||||
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
|
||||
{ text: t.nav.guide, link: `${prefix}/guide/` },
|
||||
{ text: t.nav.modules, link: `${prefix}/modules/` },
|
||||
{ text: t.nav.api, link: `${prefix}/api/README` },
|
||||
{
|
||||
text: t.nav.examples,
|
||||
items: [
|
||||
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
|
||||
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{ text: t.nav.changelog, link: `${prefix}/changelog` },
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/esengine/releases'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
autoInstall: true
|
||||
})
|
||||
],
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
},
|
||||
middlewareMode: false,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin'
|
||||
}
|
||||
}
|
||||
},
|
||||
title: 'ESEngine',
|
||||
appearance: 'force-dark',
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
description: '高性能 TypeScript ECS 框架 - 为游戏开发而生',
|
||||
themeConfig: {
|
||||
nav: createNav(zh, ''),
|
||||
sidebar: createSidebar(zh, ''),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
|
||||
text: zh.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: zh.common.onThisPage
|
||||
}
|
||||
}
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
link: '/en/',
|
||||
description: 'High-performance TypeScript ECS Framework for Game Development',
|
||||
themeConfig: {
|
||||
nav: createNav(en, '/en'),
|
||||
sidebar: createSidebar(en, '/en'),
|
||||
editLink: {
|
||||
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
|
||||
text: en.common.editOnGithub
|
||||
},
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: en.common.onThisPage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
siteTitle: 'ESEngine',
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: 'Copyright © 2025 ECS Framework'
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
}
|
||||
},
|
||||
|
||||
head: [
|
||||
['meta', { name: 'theme-color', content: '#646cff' }],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
|
||||
base: '/',
|
||||
cleanUrls: true,
|
||||
ignoreDeadLinks: true,
|
||||
|
||||
markdown: {
|
||||
lineNumbers: true,
|
||||
theme: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"quickStart": "Quick Start",
|
||||
"guide": "Guide",
|
||||
"modules": "Modules",
|
||||
"api": "API",
|
||||
"examples": "Examples",
|
||||
"workerDemo": "Worker System Demo",
|
||||
"lawnMowerDemo": "Lawn Mower Demo",
|
||||
"changelog": "Changelog"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "Getting Started",
|
||||
"quickStart": "Quick Start",
|
||||
"guideOverview": "Guide Overview",
|
||||
"coreConcepts": "Core Concepts",
|
||||
"entity": "Entity",
|
||||
"hierarchy": "Hierarchy",
|
||||
"component": "Component",
|
||||
"entityQuery": "Entity Query",
|
||||
"system": "System",
|
||||
"workerSystem": "Worker System (Multithreading)",
|
||||
"scene": "Scene",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"persistentEntity": "Persistent Entity",
|
||||
"behaviorTree": "Behavior Tree",
|
||||
"btGettingStarted": "Getting Started",
|
||||
"btCoreConcepts": "Core Concepts",
|
||||
"btEditorGuide": "Editor Guide",
|
||||
"btEditorWorkflow": "Editor Workflow",
|
||||
"btCustomActions": "Custom Actions",
|
||||
"btCocosIntegration": "Cocos Creator Integration",
|
||||
"btLayaIntegration": "Laya Engine Integration",
|
||||
"btAdvancedUsage": "Advanced Usage",
|
||||
"btBestPractices": "Best Practices",
|
||||
"serialization": "Serialization",
|
||||
"eventSystem": "Event System",
|
||||
"timeAndTimers": "Time and Timers",
|
||||
"logging": "Logging",
|
||||
"advancedFeatures": "Advanced Features",
|
||||
"serviceContainer": "Service Container",
|
||||
"pluginSystem": "Plugin System",
|
||||
"platformAdapters": "Platform Adapters",
|
||||
"browserAdapter": "Browser Adapter",
|
||||
"wechatAdapter": "WeChat Mini Game Adapter",
|
||||
"nodejsAdapter": "Node.js Adapter",
|
||||
"examples": "Examples",
|
||||
"examplesOverview": "Examples Overview",
|
||||
"apiReference": "API Reference",
|
||||
"overview": "Overview",
|
||||
"coreClasses": "Core Classes",
|
||||
"systemClasses": "System Classes",
|
||||
"utilities": "Utilities",
|
||||
"interfaces": "Interfaces",
|
||||
"decorators": "Decorators",
|
||||
"enums": "Enums",
|
||||
"modulesOverview": "Modules Overview",
|
||||
"aiModules": "AI Modules",
|
||||
"gameplayModules": "Gameplay",
|
||||
"toolModules": "Tools",
|
||||
"networkModules": "Network",
|
||||
"fsm": "State Machine (FSM)",
|
||||
"fsmOverview": "Overview",
|
||||
"timer": "Timer System",
|
||||
"timerOverview": "Overview",
|
||||
"spatial": "Spatial Index",
|
||||
"spatialOverview": "Overview",
|
||||
"pathfinding": "Pathfinding",
|
||||
"pathfindingOverview": "Overview",
|
||||
"blueprint": "Visual Scripting",
|
||||
"blueprintOverview": "Overview",
|
||||
"procgen": "Procedural Generation",
|
||||
"procgenOverview": "Overview",
|
||||
"network": "Network Sync",
|
||||
"networkOverview": "Overview"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - High-performance TypeScript ECS Framework",
|
||||
"quickLinks": "Quick Links",
|
||||
"viewDocs": "View Docs",
|
||||
"getStarted": "Get Started",
|
||||
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
|
||||
"aiSystem": "AI System",
|
||||
"behaviorTreeEditor": "Visual Behavior Tree Editor",
|
||||
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
|
||||
"coreFeatures": "Core Features",
|
||||
"ecsArchitecture": "High-performance ECS Architecture",
|
||||
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
|
||||
"typeSupport": "Full Type Support",
|
||||
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
|
||||
"visualBehaviorTree": "Visual Behavior Tree",
|
||||
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
|
||||
"multiPlatform": "Multi-Platform Support",
|
||||
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
|
||||
"modularDesign": "Modular Design",
|
||||
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
|
||||
"devTools": "Developer Tools",
|
||||
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
|
||||
"learnMore": "Learn more →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "Edit this page on GitHub",
|
||||
"onThisPage": "On this page"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
|
||||
export const messages = { en, zh }
|
||||
|
||||
export type Locale = 'en' | 'zh'
|
||||
|
||||
export function getLocaleMessages(locale: Locale) {
|
||||
return messages[locale] || messages.en
|
||||
}
|
||||
|
||||
// Helper to get nested key value
|
||||
export function t(messages: typeof en, key: string): string {
|
||||
const keys = key.split('.')
|
||||
let result: any = messages
|
||||
for (const k of keys) {
|
||||
result = result?.[k]
|
||||
if (result === undefined) return key
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"quickStart": "快速开始",
|
||||
"guide": "指南",
|
||||
"modules": "模块",
|
||||
"api": "API",
|
||||
"examples": "示例",
|
||||
"workerDemo": "Worker系统演示",
|
||||
"lawnMowerDemo": "割草机演示",
|
||||
"changelog": "更新日志"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "开始使用",
|
||||
"quickStart": "快速开始",
|
||||
"guideOverview": "指南概览",
|
||||
"coreConcepts": "核心概念",
|
||||
"entity": "实体类 (Entity)",
|
||||
"hierarchy": "层级系统 (Hierarchy)",
|
||||
"component": "组件系统 (Component)",
|
||||
"entityQuery": "实体查询系统",
|
||||
"system": "系统架构 (System)",
|
||||
"workerSystem": "Worker系统 (多线程)",
|
||||
"scene": "场景管理 (Scene)",
|
||||
"sceneManager": "SceneManager",
|
||||
"worldManager": "WorldManager",
|
||||
"persistentEntity": "持久化实体 (Persistent Entity)",
|
||||
"behaviorTree": "行为树系统 (Behavior Tree)",
|
||||
"btGettingStarted": "快速开始",
|
||||
"btCoreConcepts": "核心概念",
|
||||
"btEditorGuide": "编辑器指南",
|
||||
"btEditorWorkflow": "编辑器工作流",
|
||||
"btCustomActions": "自定义动作组件",
|
||||
"btCocosIntegration": "Cocos Creator集成",
|
||||
"btLayaIntegration": "Laya引擎集成",
|
||||
"btAdvancedUsage": "高级用法",
|
||||
"btBestPractices": "最佳实践",
|
||||
"serialization": "序列化系统 (Serialization)",
|
||||
"eventSystem": "事件系统 (Event)",
|
||||
"timeAndTimers": "时间和定时器 (Time)",
|
||||
"logging": "日志系统 (Logger)",
|
||||
"advancedFeatures": "高级特性",
|
||||
"serviceContainer": "服务容器 (Service Container)",
|
||||
"pluginSystem": "插件系统 (Plugin System)",
|
||||
"platformAdapters": "平台适配器",
|
||||
"browserAdapter": "浏览器适配器",
|
||||
"wechatAdapter": "微信小游戏适配器",
|
||||
"nodejsAdapter": "Node.js适配器",
|
||||
"examples": "示例",
|
||||
"examplesOverview": "示例概览",
|
||||
"apiReference": "API 参考",
|
||||
"overview": "概述",
|
||||
"coreClasses": "核心类",
|
||||
"systemClasses": "系统类",
|
||||
"utilities": "工具类",
|
||||
"interfaces": "接口",
|
||||
"decorators": "装饰器",
|
||||
"enums": "枚举",
|
||||
"modulesOverview": "模块总览",
|
||||
"aiModules": "AI 模块",
|
||||
"gameplayModules": "游戏逻辑",
|
||||
"toolModules": "工具模块",
|
||||
"networkModules": "网络模块",
|
||||
"fsm": "状态机 (FSM)",
|
||||
"fsmOverview": "概述",
|
||||
"timer": "定时器系统",
|
||||
"timerOverview": "概述",
|
||||
"spatial": "空间索引",
|
||||
"spatialOverview": "概述",
|
||||
"pathfinding": "寻路系统",
|
||||
"pathfindingOverview": "概述",
|
||||
"blueprint": "可视化脚本",
|
||||
"blueprintOverview": "概述",
|
||||
"procgen": "程序化生成",
|
||||
"procgenOverview": "概述",
|
||||
"network": "网络同步",
|
||||
"networkOverview": "概述"
|
||||
},
|
||||
"home": {
|
||||
"title": "ESEngine - 高性能 TypeScript ECS 框架",
|
||||
"quickLinks": "快速入口",
|
||||
"viewDocs": "查看文档",
|
||||
"getStarted": "快速开始",
|
||||
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
|
||||
"aiSystem": "AI 系统",
|
||||
"behaviorTreeEditor": "行为树可视化编辑器",
|
||||
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
|
||||
"coreFeatures": "核心特性",
|
||||
"ecsArchitecture": "高性能 ECS 架构",
|
||||
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
|
||||
"typeSupport": "完整类型支持",
|
||||
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
|
||||
"visualBehaviorTree": "可视化行为树",
|
||||
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
|
||||
"multiPlatform": "多平台支持",
|
||||
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。",
|
||||
"modularDesign": "模块化设计",
|
||||
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
|
||||
"devTools": "开发者工具",
|
||||
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
|
||||
"learnMore": "了解更多 →"
|
||||
},
|
||||
"common": {
|
||||
"editOnGithub": "在 GitHub 上编辑此页",
|
||||
"onThisPage": "在这个页面上"
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
link: String,
|
||||
image: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="link" class="feature-card">
|
||||
<div class="card-image" v-if="image">
|
||||
<img :src="image" :alt="title" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-icon" v-if="icon && !image">{{ icon }}</div>
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="card-description">{{ description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feature-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--es-bg-elevated, #252526);
|
||||
border: 1px solid var(--es-border-default, #3e3e42);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: var(--es-primary, #007acc);
|
||||
background: var(--es-bg-overlay, #2d2d2d);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 12px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--es-bg-input, #3c3c3c);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse, #ffffff);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 12px;
|
||||
color: var(--es-text-secondary, #9d9d9d);
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,422 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine 粒子颜色 - VS Code 风格配色(与编辑器统一)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// 透明背景
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 计算发光进度
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- 左侧文字区域 -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
我们构建框架。<br/>
|
||||
而你将创造游戏。
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine 是一个高性能的 TypeScript ECS 框架,为游戏开发者提供现代化的实体组件系统。
|
||||
无论是 2D 还是 3D 游戏,都能帮助你快速构建可扩展的游戏架构。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/guide/getting-started" class="btn-primary">开始使用</a>
|
||||
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">了解更多</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧粒子动画区域 -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 左侧文字 */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,422 +0,0 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref(null)
|
||||
let animationId = null
|
||||
let particles = []
|
||||
let animationStartTime = null
|
||||
let glowStartTime = null
|
||||
|
||||
// ESEngine particle colors - VS Code style colors (unified with editor)
|
||||
const colors = ['#569CD6', '#4EC9B0', '#9CDCFE', '#C586C0', '#DCDCAA']
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, targetX, targetY) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.targetX = targetX
|
||||
this.targetY = targetY
|
||||
this.size = Math.random() * 2 + 1.5
|
||||
this.alpha = Math.random() * 0.5 + 0.5
|
||||
this.color = colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles(canvas, text, fontSize) {
|
||||
const tempCanvas = document.createElement('canvas')
|
||||
const tempCtx = tempCanvas.getContext('2d')
|
||||
if (!tempCtx) return []
|
||||
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
const textMetrics = tempCtx.measureText(text)
|
||||
const textWidth = textMetrics.width
|
||||
const textHeight = fontSize
|
||||
|
||||
tempCanvas.width = textWidth + 40
|
||||
tempCanvas.height = textHeight + 40
|
||||
tempCtx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
tempCtx.textAlign = 'center'
|
||||
tempCtx.textBaseline = 'middle'
|
||||
tempCtx.fillStyle = '#ffffff'
|
||||
tempCtx.fillText(text, tempCanvas.width / 2, tempCanvas.height / 2)
|
||||
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height)
|
||||
const pixels = imageData.data
|
||||
const newParticles = []
|
||||
const gap = 3
|
||||
|
||||
const width = canvas.width / (window.devicePixelRatio || 1)
|
||||
const height = canvas.height / (window.devicePixelRatio || 1)
|
||||
const offsetX = (width - tempCanvas.width) / 2
|
||||
const offsetY = (height - tempCanvas.height) / 2
|
||||
|
||||
for (let y = 0; y < tempCanvas.height; y += gap) {
|
||||
for (let x = 0; x < tempCanvas.width; x += gap) {
|
||||
const index = (y * tempCanvas.width + x) * 4
|
||||
const alpha = pixels[index + 3] || 0
|
||||
|
||||
if (alpha > 128) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.random() * Math.max(width, height)
|
||||
|
||||
newParticles.push(new Particle(
|
||||
width / 2 + Math.cos(angle) * distance,
|
||||
height / 2 + Math.sin(angle) * distance,
|
||||
offsetX + x,
|
||||
offsetY + y
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newParticles
|
||||
}
|
||||
|
||||
function easeOutQuart(t) {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
function animate(canvas, ctx) {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const width = canvas.width / dpr
|
||||
const height = canvas.height / dpr
|
||||
|
||||
const currentTime = performance.now()
|
||||
const duration = 2500
|
||||
const glowDuration = 600
|
||||
|
||||
const elapsed = currentTime - animationStartTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
// Transparent background
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// Calculate glow progress
|
||||
let glowProgress = 0
|
||||
if (progress >= 1) {
|
||||
if (glowStartTime === null) {
|
||||
glowStartTime = currentTime
|
||||
}
|
||||
glowProgress = Math.min((currentTime - glowStartTime) / glowDuration, 1)
|
||||
glowProgress = easeOutCubic(glowProgress)
|
||||
}
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
const textY = height / 2
|
||||
|
||||
for (const particle of particles) {
|
||||
const moveProgress = Math.min(easedProgress * 1.2, 1)
|
||||
const currentX = particle.x + (particle.targetX - particle.x) * moveProgress
|
||||
const currentY = particle.y + (particle.targetY - particle.y) * moveProgress
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(currentX, currentY, particle.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = particle.color
|
||||
ctx.globalAlpha = particle.alpha * (1 - glowProgress * 0.3)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
if (glowProgress > 0) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 30 * glowProgress
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${glowProgress})`
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
if (glowProgress >= 1) {
|
||||
const breathe = 0.8 + Math.sin(currentTime / 1000) * 0.2
|
||||
ctx.save()
|
||||
ctx.shadowColor = '#3b9eff'
|
||||
ctx.shadowBlur = 20 * breathe
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = `bold ${fontSize}px "Segoe UI", Arial, sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(text, width / 2, textY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => animate(canvas, ctx))
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const container = canvas.parentElement
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const text = 'ESEngine'
|
||||
const fontSize = Math.min(width / 4, height / 3, 80)
|
||||
|
||||
particles = createParticles(canvas, text, fontSize)
|
||||
animationStartTime = performance.now()
|
||||
glowStartTime = null
|
||||
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
|
||||
animate(canvas, ctx)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
window.addEventListener('resize', initCanvas)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
}
|
||||
window.removeEventListener('resize', initCanvas)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="hero-section">
|
||||
<div class="hero-container">
|
||||
<!-- Left text area -->
|
||||
<div class="hero-text">
|
||||
<div class="hero-logo">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="14" stroke="#9147ff" stroke-width="2"/>
|
||||
<path d="M10 10h8v2h-6v3h5v2h-5v3h6v2h-8v-12z" fill="#9147ff"/>
|
||||
</svg>
|
||||
<span>ESENGINE</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
We build the framework.<br/>
|
||||
You create the game.
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
ESEngine is a high-performance TypeScript ECS framework for game developers.
|
||||
Whether 2D or 3D games, it helps you build scalable game architecture quickly.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/en/guide/getting-started" class="btn-primary">Get Started</a>
|
||||
<a href="https://github.com/esengine/esengine" class="btn-secondary" target="_blank">Learn More</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right particle animation area -->
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<canvas ref="canvasRef" class="particle-canvas"></canvas>
|
||||
<div class="visual-label">
|
||||
<span class="label-title">Entity Component System</span>
|
||||
<span class="label-subtitle">High Performance Framework</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
background: #0d0d0d;
|
||||
padding: 80px 0;
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Left text */
|
||||
.hero-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 28px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b9eff;
|
||||
color: #ffffff;
|
||||
border: 1px solid #3b9eff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5aadff;
|
||||
border-color: #5aadff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1a1a1a;
|
||||
color: #a0a0a0;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.particle-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visual-label {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.label-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 48px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 48px 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,595 +0,0 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--vp-nav-height: 64px;
|
||||
|
||||
--es-bg-base: #1a1a1a;
|
||||
--es-bg-elevated: #222222;
|
||||
--es-bg-overlay: #2a2a2a;
|
||||
--es-bg-input: #333333;
|
||||
--es-bg-inset: #151515;
|
||||
--es-bg-hover: #2a2d2e;
|
||||
--es-bg-active: #37373d;
|
||||
--es-bg-sidebar: #1e1e1e;
|
||||
--es-bg-card: #242424;
|
||||
--es-bg-header: #1e1e1e;
|
||||
|
||||
/* 提高文字对比度 | Improve text contrast */
|
||||
--es-text-primary: #e0e0e0;
|
||||
--es-text-secondary: #b0b0b0;
|
||||
--es-text-tertiary: #888888;
|
||||
--es-text-inverse: #ffffff;
|
||||
--es-text-muted: #c0c0c0;
|
||||
--es-text-dim: #888888;
|
||||
|
||||
--es-font-xs: 11px;
|
||||
--es-font-sm: 12px;
|
||||
--es-font-base: 13px;
|
||||
--es-font-md: 14px;
|
||||
--es-font-lg: 16px;
|
||||
|
||||
--es-border-default: #3a3a3a;
|
||||
--es-border-subtle: #1a1a1a;
|
||||
--es-border-strong: #4a4a4a;
|
||||
|
||||
--es-primary: #3b82f6;
|
||||
--es-primary-hover: #2563eb;
|
||||
--es-success: #4ade80;
|
||||
--es-warning: #f59e0b;
|
||||
--es-error: #ef4444;
|
||||
--es-info: #3b82f6;
|
||||
|
||||
--es-selected: #3d5a80;
|
||||
--es-selected-hover: #4a6a90;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
html,
|
||||
html.dark {
|
||||
--vp-c-bg: var(--es-bg-base);
|
||||
--vp-c-bg-soft: var(--es-bg-elevated);
|
||||
--vp-c-bg-mute: var(--es-bg-overlay);
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar);
|
||||
--vp-c-text-1: var(--es-text-primary);
|
||||
--vp-c-text-2: var(--es-text-tertiary);
|
||||
--vp-c-text-3: var(--es-text-muted);
|
||||
--vp-c-divider: var(--es-border-default);
|
||||
--vp-c-divider-light: var(--es-border-subtle);
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
--vp-c-bg: var(--es-bg-base) !important;
|
||||
--vp-c-bg-soft: var(--es-bg-elevated) !important;
|
||||
--vp-c-bg-mute: var(--es-bg-overlay) !important;
|
||||
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
|
||||
--vp-c-text-1: var(--es-text-primary) !important;
|
||||
--vp-c-text-2: var(--es-text-tertiary) !important;
|
||||
--vp-c-text-3: var(--es-text-muted) !important;
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar .wrapper {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNav .VPNavBar::before,
|
||||
.VPNav .VPNavBar::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 500;
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink {
|
||||
color: var(--es-text-secondary) !important;
|
||||
font-size: var(--es-font-sm) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink:hover {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--es-text-primary) !important;
|
||||
}
|
||||
|
||||
.VPNavBarSearch .DocSearch-Button {
|
||||
background: var(--es-bg-input) !important;
|
||||
border: 1px solid var(--es-border-default) !important;
|
||||
border-radius: 2px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.VPSidebar {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-right: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item {
|
||||
padding: 8px 0 4px 0;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 > .item > .text {
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link {
|
||||
padding: 4px 8px;
|
||||
margin: 1px 0;
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-sm);
|
||||
transition: all 0.1s ease;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.VPSidebarItem .link:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--es-text-inverse);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link {
|
||||
background: var(--es-selected);
|
||||
color: var(--es-text-inverse);
|
||||
border-left: 2px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .link:hover {
|
||||
background: var(--es-selected-hover);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-1 .link {
|
||||
padding-left: 20px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2 .link {
|
||||
padding-left: 32px;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.VPSidebarItem .caret:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPContent {
|
||||
background: var(--es-bg-card) !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.VPContent.has-sidebar {
|
||||
background: var(--es-bg-card) !important;
|
||||
}
|
||||
|
||||
/* 首页布局修复 | Home page layout fix */
|
||||
.VPPage {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.Layout > .VPContent {
|
||||
padding-top: var(--vp-nav-height) !important;
|
||||
}
|
||||
|
||||
.VPDoc {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .content-body {
|
||||
background: var(--es-bg-header) !important;
|
||||
}
|
||||
|
||||
.VPNavBar .divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPLocalNav {
|
||||
background: var(--es-bg-header) !important;
|
||||
border-bottom: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
.VPNavScreenMenu {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.VPNavScreen {
|
||||
background: var(--es-bg-base) !important;
|
||||
}
|
||||
|
||||
.curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav .curtain,
|
||||
.VPNavBar .curtain {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[class*="curtain"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.VPNav > div::before,
|
||||
.VPNav > div::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.vp-doc {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: var(--es-font-lg);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: var(--es-font-md);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-inverse);
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--es-bg-header);
|
||||
border-left: 3px solid var(--es-primary);
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: var(--es-font-base);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.vp-doc p {
|
||||
color: var(--es-text-primary);
|
||||
line-height: 1.7;
|
||||
font-size: var(--es-font-base);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc ul,
|
||||
.vp-doc ol {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-doc li {
|
||||
line-height: 1.7;
|
||||
margin: 4px 0;
|
||||
color: var(--es-text-primary);
|
||||
font-size: var(--es-font-base);
|
||||
}
|
||||
|
||||
.vp-doc li::marker {
|
||||
color: var(--es-text-secondary);
|
||||
}
|
||||
|
||||
.vp-doc strong {
|
||||
color: var(--es-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--es-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.VPDocAside {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline {
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .content {
|
||||
border: none !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-title {
|
||||
font-size: var(--es-font-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--es-text-secondary);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link {
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
padding: 4px 0;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link:hover {
|
||||
color: var(--es-text-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-link.active {
|
||||
color: var(--es-primary);
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*='language-'] {
|
||||
background: var(--es-bg-inset) !important;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.vp-code-group .tabs {
|
||||
background: var(--es-bg-header);
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
}
|
||||
|
||||
.vp-doc :not(pre) > code {
|
||||
background: var(--es-bg-input);
|
||||
color: var(--es-primary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: var(--es-font-sm);
|
||||
}
|
||||
|
||||
.vp-doc tr {
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vp-doc tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.vp-doc tr:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.vp-doc th {
|
||||
background: var(--es-bg-header);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-secondary);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--es-border-subtle);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.vp-doc td {
|
||||
font-size: var(--es-font-sm);
|
||||
color: var(--es-text-primary);
|
||||
padding: 8px 12px;
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc td:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--es-text-primary);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.vp-doc .warning,
|
||||
.vp-doc .custom-block.warning {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-warning);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .warning .custom-block-title,
|
||||
.vp-doc .custom-block.warning .custom-block-title {
|
||||
color: var(--es-warning);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .warning p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .tip,
|
||||
.vp-doc .custom-block.tip {
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .tip .custom-block-title,
|
||||
.vp-doc .custom-block.tip .custom-block-title {
|
||||
color: var(--es-primary);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .tip p {
|
||||
color: var(--es-text-primary);
|
||||
margin: 0;
|
||||
font-size: var(--es-font-xs);
|
||||
}
|
||||
|
||||
.vp-doc .info,
|
||||
.vp-doc .custom-block.info {
|
||||
background: rgba(74, 222, 128, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-success);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .info .custom-block-title,
|
||||
.vp-doc .custom-block.info .custom-block-title {
|
||||
color: var(--es-success);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .danger,
|
||||
.vp-doc .custom-block.danger {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border: none;
|
||||
border-left: 3px solid var(--es-error);
|
||||
border-radius: 0 2px 2px 0;
|
||||
padding: 10px 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .danger .custom-block-title,
|
||||
.vp-doc .custom-block.danger .custom-block-title {
|
||||
color: var(--es-error);
|
||||
font-weight: 600;
|
||||
font-size: var(--es-font-xs);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vp-doc .card {
|
||||
background: var(--es-bg-sidebar);
|
||||
border: 1px solid var(--es-border-subtle);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.vp-doc .card-title {
|
||||
font-size: var(--es-font-sm);
|
||||
font-weight: 600;
|
||||
color: var(--es-text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.vp-doc .card-description {
|
||||
font-size: var(--es-font-xs);
|
||||
color: var(--es-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc .tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--es-border-default);
|
||||
border-radius: 2px;
|
||||
color: var(--es-text-secondary);
|
||||
font-size: var(--es-font-xs);
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.VPFooter {
|
||||
background: var(--es-bg-sidebar) !important;
|
||||
border-top: 1px solid var(--es-border-subtle) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--es-border-strong);
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--es-bg-card);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.VPDoc .content {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import ParticleHero from './components/ParticleHero.vue'
|
||||
import ParticleHeroEn from './components/ParticleHeroEn.vue'
|
||||
import FeatureCard from './components/FeatureCard.vue'
|
||||
import './custom.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp({ app }) {
|
||||
app.component('ParticleHero', ParticleHero)
|
||||
app.component('ParticleHeroEn', ParticleHeroEn)
|
||||
app.component('FeatureCard', FeatureCard)
|
||||
}
|
||||
}
|
||||
49
docs/README.md
Normal file
49
docs/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ └── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
@@ -1,663 +0,0 @@
|
||||
# ESEngine 材质系统统一架构重构方案
|
||||
|
||||
## 问题概述
|
||||
|
||||
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
|
||||
|
||||
| 重复项 | Sprite | UI | 重复度 |
|
||||
|--------|--------|----|----|
|
||||
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
|
||||
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
|
||||
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
|
||||
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
|
||||
|
||||
**根本原因**:缺乏统一的材质覆盖接口抽象层。
|
||||
|
||||
---
|
||||
|
||||
## 一、统一材质覆盖接口
|
||||
|
||||
### 1.1 定义通用接口
|
||||
|
||||
在 `@esengine/material-system` 包中定义统一接口:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IMaterialOverridable.ts
|
||||
|
||||
/**
|
||||
* Material property override definition.
|
||||
* 材质属性覆盖定义。
|
||||
*/
|
||||
export interface MaterialPropertyOverride {
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
|
||||
value: number | number[];
|
||||
}
|
||||
|
||||
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
|
||||
|
||||
/**
|
||||
* Interface for components that support material property overrides.
|
||||
* 支持材质属性覆盖的组件接口。
|
||||
*/
|
||||
export interface IMaterialOverridable {
|
||||
/** Material GUID for asset reference | 材质资产引用的 GUID */
|
||||
materialGuid: string;
|
||||
|
||||
/** Current material overrides | 当前材质覆盖 */
|
||||
readonly materialOverrides: MaterialOverrides;
|
||||
|
||||
/** Get current material ID | 获取当前材质 ID */
|
||||
getMaterialId(): number;
|
||||
|
||||
/** Set material ID | 设置材质 ID */
|
||||
setMaterialId(id: number): void;
|
||||
|
||||
// Uniform setters
|
||||
setOverrideFloat(name: string, value: number): this;
|
||||
setOverrideVec2(name: string, x: number, y: number): this;
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this;
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
|
||||
setOverrideInt(name: string, value: number): this;
|
||||
|
||||
// Uniform getters
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined;
|
||||
removeOverride(name: string): this;
|
||||
clearOverrides(): this;
|
||||
hasOverrides(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 创建 Mixin 实现
|
||||
|
||||
使用 Mixin 模式避免代码重复:
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
|
||||
|
||||
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
|
||||
|
||||
/**
|
||||
* Mixin that provides material override functionality.
|
||||
* 提供材质覆盖功能的 Mixin。
|
||||
*/
|
||||
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
|
||||
return class extends Base {
|
||||
materialGuid: string = '';
|
||||
private _materialId: number = 0;
|
||||
private _materialOverrides: MaterialOverrides = {};
|
||||
|
||||
get materialOverrides(): MaterialOverrides {
|
||||
return this._materialOverrides;
|
||||
}
|
||||
|
||||
getMaterialId(): number {
|
||||
return this._materialId;
|
||||
}
|
||||
|
||||
setMaterialId(id: number): void {
|
||||
this._materialId = id;
|
||||
}
|
||||
|
||||
setOverrideFloat(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'float', value };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec2(name: string, x: number, y: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
|
||||
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
|
||||
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
|
||||
return this;
|
||||
}
|
||||
|
||||
setOverrideInt(name: string, value: number): this {
|
||||
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
|
||||
return this;
|
||||
}
|
||||
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined {
|
||||
return this._materialOverrides[name];
|
||||
}
|
||||
|
||||
removeOverride(name: string): this {
|
||||
delete this._materialOverrides[name];
|
||||
return this;
|
||||
}
|
||||
|
||||
clearOverrides(): this {
|
||||
this._materialOverrides = {};
|
||||
return this;
|
||||
}
|
||||
|
||||
hasOverrides(): boolean {
|
||||
return Object.keys(this._materialOverrides).length > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、Shader Property 元数据系统
|
||||
|
||||
### 2.1 定义属性元数据接口
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/interfaces/IShaderProperty.ts
|
||||
|
||||
/**
|
||||
* Shader property UI metadata.
|
||||
* 着色器属性 UI 元数据。
|
||||
*/
|
||||
export interface ShaderPropertyMeta {
|
||||
/** Property type | 属性类型 */
|
||||
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
|
||||
|
||||
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
|
||||
label: string;
|
||||
|
||||
/** Property group for organization | 属性分组 */
|
||||
group?: string;
|
||||
|
||||
/** Default value | 默认值 */
|
||||
default?: number | number[] | string;
|
||||
|
||||
// Numeric constraints
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
|
||||
/** UI hints | UI 提示 */
|
||||
hint?: 'range' | 'angle' | 'hdr' | 'normal';
|
||||
|
||||
/** Tooltip description | 工具提示描述 */
|
||||
tooltip?: string;
|
||||
|
||||
/** Whether to hide in inspector | 是否在检查器中隐藏 */
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended shader definition with property metadata.
|
||||
* 带属性元数据的扩展着色器定义。
|
||||
*/
|
||||
export interface ShaderAssetDefinition {
|
||||
/** Shader name | 着色器名称 */
|
||||
name: string;
|
||||
|
||||
/** Display name for UI | UI 显示名称 */
|
||||
displayName?: string;
|
||||
|
||||
/** Shader description | 着色器描述 */
|
||||
description?: string;
|
||||
|
||||
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
|
||||
vertexSource: string;
|
||||
|
||||
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
|
||||
fragmentSource: string;
|
||||
|
||||
/** Property metadata for inspector | 检查器属性元数据 */
|
||||
properties?: Record<string, ShaderPropertyMeta>;
|
||||
|
||||
/** Render queue / order | 渲染队列/顺序 */
|
||||
renderQueue?: number;
|
||||
|
||||
/** Preset blend mode | 预设混合模式 */
|
||||
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 .shader 资产文件格式
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "esengine://schemas/shader.json",
|
||||
"version": 1,
|
||||
"name": "Shiny",
|
||||
"displayName": "闪光效果 | Shiny Effect",
|
||||
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
|
||||
|
||||
"vertexSource": "./shaders/sprite.vert",
|
||||
"fragmentSource": "./shaders/shiny.frag",
|
||||
|
||||
"blendMode": "alpha",
|
||||
"renderQueue": 2000,
|
||||
|
||||
"properties": {
|
||||
"u_shinyProgress": {
|
||||
"type": "float",
|
||||
"label": "进度 | Progress",
|
||||
"group": "Animation",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"hidden": true
|
||||
},
|
||||
"u_shinyWidth": {
|
||||
"type": "float",
|
||||
"label": "宽度 | Width",
|
||||
"group": "Effect",
|
||||
"default": 0.25,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "闪光带宽度 | Width of the shiny band"
|
||||
},
|
||||
"u_shinyRotation": {
|
||||
"type": "float",
|
||||
"label": "角度 | Rotation",
|
||||
"group": "Effect",
|
||||
"default": 2.25,
|
||||
"min": 0,
|
||||
"max": 6.28,
|
||||
"step": 0.01,
|
||||
"hint": "angle"
|
||||
},
|
||||
"u_shinySoftness": {
|
||||
"type": "float",
|
||||
"label": "柔和度 | Softness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyBrightness": {
|
||||
"type": "float",
|
||||
"label": "亮度 | Brightness",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01
|
||||
},
|
||||
"u_shinyGloss": {
|
||||
"type": "float",
|
||||
"label": "光泽度 | Gloss",
|
||||
"group": "Effect",
|
||||
"default": 1.0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、统一效果组件/系统架构
|
||||
|
||||
### 3.1 抽取通用 ShinyEffect 基类
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/BaseShinyEffect.ts
|
||||
|
||||
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Base shiny effect configuration (shared between UI and Sprite).
|
||||
* 基础闪光效果配置(UI 和 Sprite 共享)。
|
||||
*/
|
||||
export abstract class BaseShinyEffect extends Component {
|
||||
// ============= Effect Parameters =============
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
|
||||
public width: number = 0.25;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
|
||||
public rotation: number = 129;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
|
||||
public softness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
|
||||
public brightness: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
|
||||
public gloss: number = 1.0;
|
||||
|
||||
// ============= Animation Settings =============
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play' })
|
||||
public play: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Loop' })
|
||||
public loop: boolean = true;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
|
||||
public duration: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
|
||||
public loopDelay: number = 2.0;
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
|
||||
public initialDelay: number = 0;
|
||||
|
||||
// ============= Runtime State =============
|
||||
public progress: number = 0;
|
||||
public elapsedTime: number = 0;
|
||||
public inDelay: boolean = false;
|
||||
public delayRemaining: number = 0;
|
||||
public initialDelayProcessed: boolean = false;
|
||||
|
||||
reset(): void {
|
||||
this.progress = 0;
|
||||
this.elapsedTime = 0;
|
||||
this.inDelay = false;
|
||||
this.delayRemaining = 0;
|
||||
this.initialDelayProcessed = false;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.reset();
|
||||
this.play = true;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.play = false;
|
||||
}
|
||||
|
||||
getRotationRadians(): number {
|
||||
return this.rotation * Math.PI / 180;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 通用动画更新逻辑
|
||||
|
||||
```typescript
|
||||
// packages/material-system/src/effects/ShinyEffectAnimator.ts
|
||||
|
||||
import type { BaseShinyEffect } from './BaseShinyEffect';
|
||||
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
|
||||
import { BuiltInShaders } from '../types';
|
||||
|
||||
/**
|
||||
* Shared animator logic for shiny effect.
|
||||
* 闪光效果共享的动画逻辑。
|
||||
*/
|
||||
export class ShinyEffectAnimator {
|
||||
/**
|
||||
* Update animation state.
|
||||
* 更新动画状态。
|
||||
*/
|
||||
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
|
||||
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
|
||||
shiny.delayRemaining = shiny.initialDelay;
|
||||
shiny.inDelay = true;
|
||||
shiny.initialDelayProcessed = true;
|
||||
}
|
||||
|
||||
if (shiny.inDelay) {
|
||||
shiny.delayRemaining -= deltaTime;
|
||||
if (shiny.delayRemaining <= 0) {
|
||||
shiny.inDelay = false;
|
||||
shiny.elapsedTime = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
shiny.elapsedTime += deltaTime;
|
||||
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
|
||||
|
||||
if (shiny.progress >= 1.0) {
|
||||
if (shiny.loop) {
|
||||
shiny.inDelay = true;
|
||||
shiny.delayRemaining = shiny.loopDelay;
|
||||
shiny.progress = 0;
|
||||
shiny.elapsedTime = 0;
|
||||
} else {
|
||||
shiny.play = false;
|
||||
shiny.progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply material overrides.
|
||||
* 应用材质覆盖。
|
||||
*/
|
||||
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
|
||||
if (target.getMaterialId() === 0) {
|
||||
target.setMaterialId(BuiltInShaders.Shiny);
|
||||
}
|
||||
|
||||
target.setOverrideFloat('u_shinyProgress', shiny.progress);
|
||||
target.setOverrideFloat('u_shinyWidth', shiny.width);
|
||||
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
|
||||
target.setOverrideFloat('u_shinySoftness', shiny.softness);
|
||||
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
|
||||
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Material Inspector 设计
|
||||
|
||||
### 4.1 组件架构
|
||||
|
||||
```
|
||||
MaterialPropertiesEditor (容器组件)
|
||||
├── ShaderSelector (着色器选择器)
|
||||
├── PropertyGroup (属性分组)
|
||||
│ ├── FloatProperty (浮点属性)
|
||||
│ ├── VectorProperty (向量属性)
|
||||
│ ├── ColorProperty (颜色属性)
|
||||
│ └── TextureProperty (纹理属性)
|
||||
└── OverrideIndicator (覆盖指示器)
|
||||
```
|
||||
|
||||
### 4.2 核心组件
|
||||
|
||||
```typescript
|
||||
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
|
||||
|
||||
interface MaterialPropertiesEditorProps {
|
||||
/** Target component implementing IMaterialOverridable */
|
||||
target: IMaterialOverridable;
|
||||
/** Current shader definition with property metadata */
|
||||
shaderDef?: ShaderAssetDefinition;
|
||||
/** Callback when property changes */
|
||||
onChange?: (name: string, value: MaterialPropertyOverride) => void;
|
||||
}
|
||||
|
||||
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
|
||||
target,
|
||||
shaderDef,
|
||||
onChange
|
||||
}) => {
|
||||
// Group properties by their group field
|
||||
const groupedProps = useMemo(() => {
|
||||
if (!shaderDef?.properties) return {};
|
||||
|
||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
||||
for (const [name, meta] of Object.entries(shaderDef.properties)) {
|
||||
if (meta.hidden) continue;
|
||||
const group = meta.group || 'Default';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push([name, meta]);
|
||||
}
|
||||
return groups;
|
||||
}, [shaderDef]);
|
||||
|
||||
return (
|
||||
<div className="material-properties-editor">
|
||||
<ShaderSelector
|
||||
currentShaderId={target.getMaterialId()}
|
||||
onSelect={(id) => target.setMaterialId(id)}
|
||||
/>
|
||||
|
||||
{Object.entries(groupedProps).map(([group, props]) => (
|
||||
<PropertyGroup key={group} title={group}>
|
||||
{props.map(([name, meta]) => (
|
||||
<PropertyField
|
||||
key={name}
|
||||
name={name}
|
||||
meta={meta}
|
||||
value={target.getOverride(name)?.value ?? meta.default}
|
||||
onChange={(value) => {
|
||||
applyOverride(target, name, meta.type, value);
|
||||
onChange?.(name, target.getOverride(name)!);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</PropertyGroup>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施计划
|
||||
|
||||
### Phase 1: 接口层 (1-2 天)
|
||||
|
||||
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
|
||||
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
|
||||
3. **导出新接口** (`packages/material-system/src/index.ts`)
|
||||
|
||||
### Phase 2: 重构现有组件 (2-3 天)
|
||||
|
||||
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
|
||||
3. **删除重复代码**:移除各组件中的重复材质方法
|
||||
|
||||
### Phase 3: 统一效果系统 (2-3 天)
|
||||
|
||||
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
|
||||
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
|
||||
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
|
||||
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
|
||||
5. **重构系统**:使用 ShinyEffectAnimator
|
||||
|
||||
### Phase 4: Shader Property 系统 (2-3 天)
|
||||
|
||||
1. **定义 ShaderPropertyMeta 接口**
|
||||
2. **扩展 ShaderDefinition** 添加 properties 字段
|
||||
3. **创建 ShaderLoader** 支持 .shader 文件
|
||||
4. **注册内置着色器属性元数据**
|
||||
|
||||
### Phase 5: Material Inspector (3-4 天)
|
||||
|
||||
1. **创建 MaterialPropertiesEditor 组件**
|
||||
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
|
||||
3. **集成到现有 Inspector 系统**
|
||||
4. **支持实时预览**
|
||||
|
||||
---
|
||||
|
||||
## 六、文件修改清单
|
||||
|
||||
| 优先级 | 包 | 文件 | 操作 |
|
||||
|--------|-----|------|------|
|
||||
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
|
||||
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
|
||||
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
|
||||
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
|
||||
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
|
||||
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
|
||||
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
|
||||
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
|
||||
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
|
||||
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
|
||||
|
||||
---
|
||||
|
||||
## 七、Transform 组件统一(可选)
|
||||
|
||||
### 7.1 现状分析
|
||||
|
||||
| 特性 | TransformComponent | UITransformComponent |
|
||||
|------|-------------------|---------------------|
|
||||
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
|
||||
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
|
||||
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
|
||||
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
|
||||
| **可见性** | ❌ 无 | ✅ visible, alpha |
|
||||
|
||||
### 7.2 结论
|
||||
|
||||
**不建议完全合并**,但可提取公共基类:
|
||||
|
||||
```typescript
|
||||
// packages/engine-core/src/interfaces/ITransformBase.ts
|
||||
|
||||
export interface ITransformBase {
|
||||
/** 旋转角度(度) | Rotation in degrees */
|
||||
rotation: number;
|
||||
|
||||
/** X 缩放 | Scale X */
|
||||
scaleX: number;
|
||||
|
||||
/** Y 缩放 | Scale Y */
|
||||
scaleY: number;
|
||||
|
||||
/** 本地到世界矩阵 | Local to world matrix */
|
||||
readonly localToWorldMatrix: Matrix2D;
|
||||
|
||||
/** 是否需要更新 | Dirty flag */
|
||||
isDirty: boolean;
|
||||
|
||||
/** 世界坐标 X | World position X */
|
||||
readonly worldX: number;
|
||||
|
||||
/** 世界坐标 Y | World position Y */
|
||||
readonly worldY: number;
|
||||
|
||||
/** 世界旋转 | World rotation */
|
||||
readonly worldRotation: number;
|
||||
|
||||
/** 世界缩放 X | World scale X */
|
||||
readonly worldScaleX: number;
|
||||
|
||||
/** 世界缩放 Y | World scale Y */
|
||||
readonly worldScaleY: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 收益
|
||||
|
||||
- 渲染系统可以统一处理 `ITransformBase`
|
||||
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
|
||||
- Gizmo 系统可以共享变换操作逻辑
|
||||
|
||||
---
|
||||
|
||||
## 八、向后兼容性
|
||||
|
||||
1. **接口兼容**:现有组件的 API 保持不变
|
||||
2. **序列化兼容**:不改变现有序列化格式
|
||||
3. **渐进迁移**:可分阶段进行,不影响现有功能
|
||||
178
docs/astro.config.mjs
Normal file
178
docs/astro.config.mjs
Normal file
@@ -0,0 +1,178 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import vue from '@astrojs/vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'ESEngine',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
},
|
||||
social: [
|
||||
{ icon: 'github', label: 'GitHub', href: 'https://github.com/esengine/esengine' }
|
||||
],
|
||||
defaultLocale: 'root',
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
},
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: '快速开始',
|
||||
translations: { en: 'Getting Started' },
|
||||
items: [
|
||||
{ label: '快速入门', slug: 'guide/getting-started', translations: { en: 'Quick Start' } },
|
||||
{ label: '指南概览', slug: 'guide', translations: { en: 'Guide Overview' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '核心概念',
|
||||
translations: { en: 'Core Concepts' },
|
||||
items: [
|
||||
{ label: '实体', slug: 'guide/entity', translations: { en: 'Entity' } },
|
||||
{ label: '层级结构', slug: 'guide/hierarchy', translations: { en: 'Hierarchy' } },
|
||||
{
|
||||
label: '组件',
|
||||
translations: { en: 'Component' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/component', translations: { en: 'Overview' } },
|
||||
{ label: '生命周期', slug: 'guide/component/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: 'EntityRef 装饰器', slug: 'guide/component/entity-ref', translations: { en: 'EntityRef' } },
|
||||
{ label: '最佳实践', slug: 'guide/component/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '实体查询',
|
||||
translations: { en: 'Entity Query' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/entity-query', translations: { en: 'Overview' } },
|
||||
{ label: 'Matcher API', slug: 'guide/entity-query/matcher-api', translations: { en: 'Matcher API' } },
|
||||
{ label: '编译查询', slug: 'guide/entity-query/compiled-query', translations: { en: 'Compiled Query' } },
|
||||
{ label: '最佳实践', slug: 'guide/entity-query/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '系统',
|
||||
translations: { en: 'System' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/system', translations: { en: 'Overview' } },
|
||||
{ label: '系统类型', slug: 'guide/system/types', translations: { en: 'System Types' } },
|
||||
{ label: '生命周期', slug: 'guide/system/lifecycle', translations: { en: 'Lifecycle' } },
|
||||
{ label: '命令缓冲区', slug: 'guide/system/command-buffer', translations: { en: 'Command Buffer' } },
|
||||
{ label: '系统调度', slug: 'guide/system/scheduling', translations: { en: 'Scheduling' } },
|
||||
{ label: '变更检测', slug: 'guide/system/change-detection', translations: { en: 'Change Detection' } },
|
||||
{ label: '最佳实践', slug: 'guide/system/best-practices', translations: { en: 'Best Practices' } },
|
||||
],
|
||||
},
|
||||
{ label: '场景', slug: 'guide/scene', translations: { en: 'Scene' } },
|
||||
{
|
||||
label: '序列化',
|
||||
translations: { en: 'Serialization' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/serialization', translations: { en: 'Overview' } },
|
||||
{ label: '装饰器与继承', slug: 'guide/serialization/decorators', translations: { en: 'Decorators & Inheritance' } },
|
||||
{ label: '增量序列化', slug: 'guide/serialization/incremental', translations: { en: 'Incremental' } },
|
||||
{ label: '版本迁移', slug: 'guide/serialization/migration', translations: { en: 'Migration' } },
|
||||
{ label: '使用场景', slug: 'guide/serialization/use-cases', translations: { en: 'Use Cases' } },
|
||||
],
|
||||
},
|
||||
{ label: '事件系统', slug: 'guide/event-system', translations: { en: 'Event System' } },
|
||||
{ label: '时间与定时器', slug: 'guide/time-and-timers', translations: { en: 'Time & Timers' } },
|
||||
{ label: '日志系统', slug: 'guide/logging', translations: { en: 'Logging' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '高级功能',
|
||||
translations: { en: 'Advanced Features' },
|
||||
items: [
|
||||
{
|
||||
label: '服务容器',
|
||||
translations: { en: 'Service Container' },
|
||||
items: [
|
||||
{ label: '概述', slug: 'guide/service-container', translations: { en: 'Overview' } },
|
||||
{ label: '内置服务', slug: 'guide/service-container/built-in-services', translations: { en: 'Built-in Services' } },
|
||||
{ label: '依赖注入', slug: 'guide/service-container/dependency-injection', translations: { en: 'Dependency Injection' } },
|
||||
{ label: 'PluginServiceRegistry', slug: 'guide/service-container/plugin-service-registry', translations: { en: 'PluginServiceRegistry' } },
|
||||
{ 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: 'Platform Adapters' },
|
||||
items: [
|
||||
{ label: '概览', slug: 'guide/platform-adapter', translations: { en: 'Overview' } },
|
||||
{ label: '浏览器', slug: 'guide/platform-adapter/browser', translations: { en: 'Browser' } },
|
||||
{ label: '微信小游戏', slug: 'guide/platform-adapter/wechat-minigame', translations: { en: 'WeChat Mini Game' } },
|
||||
{ label: 'Node.js', slug: 'guide/platform-adapter/nodejs', translations: { en: 'Node.js' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '模块',
|
||||
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: '网络', slug: 'modules/network', translations: { en: 'Network' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '示例',
|
||||
translations: { en: 'Examples' },
|
||||
items: [
|
||||
{ label: '示例总览', slug: 'examples', translations: { en: 'Examples Overview' } },
|
||||
{ label: 'Worker 系统演示', slug: 'examples/worker-system-demo', translations: { en: 'Worker System Demo' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'API 参考',
|
||||
translations: { en: 'API Reference' },
|
||||
autogenerate: { directory: 'api' },
|
||||
},
|
||||
{
|
||||
label: '更新日志',
|
||||
translations: { en: 'Changelog' },
|
||||
items: [
|
||||
{ label: '@esengine/ecs-framework', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/core/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/behavior-tree', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/behavior-tree/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/fsm', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/fsm/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/timer', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/timer/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/network', link: 'https://github.com/esengine/esengine/blob/master/packages/framework/network/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
{ label: '@esengine/cli', link: 'https://github.com/esengine/esengine/blob/master/packages/tools/cli/CHANGELOG.md', attrs: { target: '_blank' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
head: [
|
||||
{ tag: 'meta', attrs: { name: 'theme-color', content: '#646cff' } },
|
||||
],
|
||||
components: {
|
||||
Head: './src/components/Head.astro',
|
||||
ThemeSelect: './src/components/ThemeSelect.astro',
|
||||
},
|
||||
}),
|
||||
vue(),
|
||||
],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
@@ -1,293 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer 实体过滤**: 增量序列化支持 `entityFilter` 选项 (#335)
|
||||
- 创建快照时可按条件过滤实体
|
||||
- 支持按标签、组件类型等自定义过滤逻辑
|
||||
- 适用于只同步部分实体的场景(如只同步玩家)
|
||||
|
||||
```typescript
|
||||
// 只快照玩家实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// 只快照有特定组件的实体
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- 优化 `PlatformWorkerPool` 代码规范,提取为独立模块 (#335)
|
||||
- 优化 `WorkerEntitySystem` 实现,改进代码结构 (#334)
|
||||
- 代码规范化与依赖清理 (#317)
|
||||
- 代码结构优化,添加 `GlobalTypes.ts` 统一类型定义 (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复 `IntervalSystem` 时间累加 bug,间隔计时更加准确
|
||||
- 修复 Cocos Creator 兼容性问题,类型导出更完整
|
||||
|
||||
### Documentation
|
||||
|
||||
- 新增 `Core.paused` 属性文档说明
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **EntityHandle 实体句柄**: 轻量级实体引用抽象 (#304)
|
||||
- 28位索引 + 20位代数(generation)设计,高效复用已销毁实体槽位
|
||||
- `EntityHandleManager` 管理句柄生命周期和有效性验证
|
||||
- 支持句柄转换为实体引用,检测悬空引用
|
||||
|
||||
- **SystemScheduler 系统调度器**: 声明式系统调度 (#304)
|
||||
- 新增 `@Stage(name)` 装饰器指定系统执行阶段
|
||||
- 新增 `@Before(SystemClass)` / `@After(SystemClass)` 装饰器声明系统依赖
|
||||
- 新增 `@InSet(setName)` 装饰器将系统归入逻辑分组
|
||||
- 基于拓扑排序自动解析执行顺序,检测循环依赖
|
||||
|
||||
- **EpochManager 变更检测**: 帧级变更追踪机制 (#304)
|
||||
- 跟踪组件添加/修改时间戳(epoch)
|
||||
- 支持查询"自上次检查以来变化的组件"
|
||||
- 适用于脏检测、增量更新等优化场景
|
||||
|
||||
- **CompiledQuery 编译查询**: 预编译类型安全查询 (#304)
|
||||
- 编译时生成优化的查询逻辑,减少运行时开销
|
||||
- 完整的 TypeScript 类型推断支持
|
||||
- 支持 `With`、`Without`、`Changed` 等查询条件组合
|
||||
|
||||
- **PluginServiceRegistry**: 类型安全的插件服务注册表 (#300)
|
||||
- 通过 `Core.pluginServices` 访问
|
||||
- 支持 `ServiceToken<T>` 模式获取服务
|
||||
|
||||
- **组件自动注册**: `@ECSComponent` 装饰器增强 (#302)
|
||||
- 装饰器现在自动注册到 `ComponentRegistry`
|
||||
- 解决 `Decorators ↔ ComponentRegistry` 循环依赖
|
||||
- 新建 `ComponentTypeUtils.ts` 作为底层无依赖模块
|
||||
|
||||
### API Changes
|
||||
|
||||
- `EntitySystem` 添加 `getBefore()` / `getAfter()` / `getSets()` getter 方法
|
||||
- `Entity` 添加 `markDirty()` 辅助方法用于手动触发变更检测
|
||||
- `IScene` 添加 `epochManager` 属性
|
||||
- `CommandBuffer.pendingCount` 修正为返回实际操作数(而非实体数)
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新系统调度文档,添加声明式依赖配置章节
|
||||
- 更新实体查询文档,添加编译查询使用说明
|
||||
|
||||
---
|
||||
|
||||
## v2.3.2 (2025-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **微信小游戏 Worker 支持**: 添加对微信小游戏平台 Worker 的完整支持 (#297)
|
||||
- 新增 `workerScriptPath` 配置项,支持预编译 Worker 脚本路径
|
||||
- 修复微信小游戏 Worker 消息格式差异(`res` 直接是数据,无需 `.data`)
|
||||
- 适用于微信小游戏等不支持动态脚本的平台
|
||||
|
||||
### New Package
|
||||
|
||||
- **@esengine/worker-generator** `v1.0.2`: CLI 工具,从 `WorkerEntitySystem` 子类自动生成 Worker 文件
|
||||
- 自动扫描并提取 `workerProcess` 方法体
|
||||
- 支持 `--wechat` 模式,使用 TypeScript 编译器转换为 ES5 语法
|
||||
- 读取代码中的 `workerScriptPath` 配置,生成到指定路径
|
||||
- 生成 `worker-mapping.json` 映射文件
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新 Worker 系统文档,添加微信小游戏支持章节
|
||||
- 新增英文版 Worker 系统文档 (`docs/en/guide/worker-system.md`)
|
||||
|
||||
---
|
||||
|
||||
## v2.3.1 (2025-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **类型导出修复**: 修复 v2.3.0 中的类型导出问题
|
||||
- 解决 `ServiceToken` 跨包类型兼容性问题
|
||||
- 修复 `editor-app` 和 `behavior-tree-editor` 中的类型引用
|
||||
|
||||
---
|
||||
|
||||
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
|
||||
|
||||
> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。
|
||||
>
|
||||
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
- **持久化实体**: 添加实体跨场景迁移支持 (#285)
|
||||
- 新增 `EEntityLifecyclePolicy` 枚举(`SceneLocal`/`Persistent`)
|
||||
- Entity 添加 `setPersistent()`、`setSceneLocal()`、`isPersistent` API
|
||||
- Scene 添加 `findPersistentEntities()`、`extractPersistentEntities()`、`receiveMigratedEntities()`
|
||||
- `SceneManager.setScene()` 自动处理持久化实体迁移
|
||||
- 适用场景:全局管理器、玩家角色、跨场景状态保持
|
||||
|
||||
- **CommandBuffer 延迟命令系统**: 在帧末统一执行实体操作 (#281)
|
||||
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
|
||||
- 每个系统拥有独立的 `commands` 属性
|
||||
- 避免在迭代过程中修改实体列表,防止迭代问题
|
||||
- Scene 在 `lateUpdate` 后自动刷新所有命令缓冲区
|
||||
|
||||
### Performance
|
||||
|
||||
- **ReactiveQuery 快照优化**: 优化实体查询迭代性能 (#281)
|
||||
- 添加快照机制,避免每帧拷贝数组
|
||||
- 只在实体列表变化时创建新快照
|
||||
- 静态场景下多个系统共享同一快照
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
|
||||
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
|
||||
- 使用脏实体集合代替每帧遍历所有实体
|
||||
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
|
||||
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
|
||||
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
|
||||
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
|
||||
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
|
||||
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
|
||||
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
|
||||
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
|
||||
- 确保相同优先级的系统按添加顺序执行
|
||||
|
||||
- **模块配置**: 添加 `module.json` 配置文件 (#256)
|
||||
- 定义模块 ID、名称、版本等元信息
|
||||
- 支持模块依赖声明和导出配置
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
|
||||
- `ProfilerSDK`: 统一的性能分析接口
|
||||
- 手动采样标记 (`beginSample`/`endSample`)
|
||||
- 自动作用域测量 (`measure`/`measureAsync`)
|
||||
- 调用层级追踪和调用图生成
|
||||
- 计数器和仪表支持
|
||||
- `AdvancedProfilerCollector`: 高级性能数据收集器
|
||||
- 帧时间统计和历史记录
|
||||
- 内存快照和 GC 检测
|
||||
- 长任务检测 (Long Task API)
|
||||
- 性能报告生成
|
||||
- `DebugManager`: 调试管理器
|
||||
- 统一的调试工具入口
|
||||
- 性能分析器集成
|
||||
|
||||
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
|
||||
- 支持更多序列化选项配置
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
|
||||
- 覆盖实体查询、缓存、生命周期等场景
|
||||
|
||||
- **Matcher 增强**: 优化匹配器功能 (#240)
|
||||
- 改进匹配逻辑和性能
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
|
||||
- 支持通过名称注册和查询组件类型
|
||||
- 添加组件掩码缓存优化性能
|
||||
|
||||
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
|
||||
- 支持更灵活的序列化配置
|
||||
- 改进嵌套对象序列化
|
||||
|
||||
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
|
||||
- `onSceneStart()`: 场景开始时调用
|
||||
- `onSceneStop()`: 场景停止时调用
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
|
||||
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
|
||||
|
||||
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
|
||||
- 支持 `Symbol.for()` 模式的服务标识
|
||||
- 新增 `@InjectProperty` 属性注入装饰器
|
||||
- 改进服务解析和生命周期管理
|
||||
|
||||
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
|
||||
- 支持更多组件类型的序列化
|
||||
- 改进反序列化错误处理
|
||||
|
||||
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
|
||||
- 支持 `@range`、`@slider` 等编辑器提示
|
||||
- 支持 `@dropdown`、`@color` 等 UI 类型
|
||||
- 支持 `@asset` 资源引用类型
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
|
||||
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
- **主版本号**: 重大不兼容更新
|
||||
- **次版本号**: 新功能添加(向后兼容)
|
||||
- **修订版本号**: Bug 修复和小改进
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/esengine/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [文档首页](./index.md)
|
||||
@@ -1,291 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
This document records the version update history of the `@esengine/ecs-framework` core library.
|
||||
|
||||
---
|
||||
|
||||
## v2.4.2 (2025-12-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **IncrementalSerializer Entity Filter**: Incremental serialization supports `entityFilter` option (#335)
|
||||
- Filter entities by condition when creating snapshots
|
||||
- Support custom filter logic by tag, component type, etc.
|
||||
- Suitable for scenarios that only sync partial entities (e.g., only sync players)
|
||||
|
||||
```typescript
|
||||
// Only snapshot player entities
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.tag === PLAYER_TAG
|
||||
});
|
||||
|
||||
// Only snapshot entities with specific component
|
||||
const snapshot = IncrementalSerializer.createSnapshot(scene, {
|
||||
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
|
||||
});
|
||||
```
|
||||
|
||||
### Refactor
|
||||
|
||||
- Optimize `PlatformWorkerPool` code style, extract as standalone module (#335)
|
||||
- Optimize `WorkerEntitySystem` implementation, improve code structure (#334)
|
||||
- Code standardization and dependency cleanup (#317)
|
||||
- Code structure optimization, add `GlobalTypes.ts` for unified type definitions (#316)
|
||||
|
||||
---
|
||||
|
||||
## v2.4.1 (2025-12-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
|
||||
- Fix Cocos Creator compatibility issue, more complete type exports
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `Core.paused` property documentation
|
||||
|
||||
---
|
||||
|
||||
## v2.4.0 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **EntityHandle**: Lightweight entity reference abstraction (#304)
|
||||
- 28-bit index + 20-bit generation design for efficient reuse of destroyed entity slots
|
||||
- `EntityHandleManager` manages handle lifecycle and validity verification
|
||||
- Support handle-to-entity conversion with dangling reference detection
|
||||
|
||||
- **SystemScheduler**: Declarative system scheduling (#304)
|
||||
- New `@Stage(name)` decorator to specify system execution stage
|
||||
- New `@Before(SystemClass)` / `@After(SystemClass)` decorators to declare dependencies
|
||||
- New `@InSet(setName)` decorator to group systems logically
|
||||
- Automatic execution order resolution via topological sort with cycle detection
|
||||
|
||||
- **EpochManager**: Frame-level change detection mechanism (#304)
|
||||
- Track component add/modify timestamps (epochs)
|
||||
- Support querying "components changed since last check"
|
||||
- Suitable for dirty checking, incremental updates, and other optimization scenarios
|
||||
|
||||
- **CompiledQuery**: Pre-compiled type-safe queries (#304)
|
||||
- Compile-time generated optimized query logic, reducing runtime overhead
|
||||
- Full TypeScript type inference support
|
||||
- Support `With`, `Without`, `Changed` and other query condition combinations
|
||||
|
||||
- **PluginServiceRegistry**: Type-safe plugin service registry (#300)
|
||||
- Accessible via `Core.pluginServices`
|
||||
- Support `ServiceToken<T>` pattern for service retrieval
|
||||
|
||||
- **Component Auto-Registration**: `@ECSComponent` decorator enhancement (#302)
|
||||
- Decorator now automatically registers to `ComponentRegistry`
|
||||
- Resolved `Decorators ↔ ComponentRegistry` circular dependency
|
||||
- New `ComponentTypeUtils.ts` as low-level dependency-free module
|
||||
|
||||
### API Changes
|
||||
|
||||
- `EntitySystem` adds `getBefore()` / `getAfter()` / `getSets()` getter methods
|
||||
- `Entity` adds `markDirty()` helper method for manual change detection triggering
|
||||
- `IScene` adds `epochManager` property
|
||||
- `CommandBuffer.pendingCount` corrected to return actual operation count (not entity count)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated system scheduling documentation with declarative dependency configuration
|
||||
- Updated entity query documentation with compiled query usage
|
||||
|
||||
---
|
||||
|
||||
## v2.3.2 (2025-12-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **WeChat Mini Game Worker Support**: Add complete Worker support for WeChat Mini Game platform (#297)
|
||||
- New `workerScriptPath` config option for pre-compiled Worker script path
|
||||
- Fix WeChat Mini Game Worker message format difference (`res` is data directly, no `.data` wrapper)
|
||||
- Applicable to WeChat Mini Game and other platforms that don't support dynamic scripts
|
||||
|
||||
### New Package
|
||||
|
||||
- **@esengine/worker-generator** `v1.0.2`: CLI tool to auto-generate Worker files from `WorkerEntitySystem` subclasses
|
||||
- Automatically scan and extract `workerProcess` method body
|
||||
- Support `--wechat` mode, use TypeScript compiler to convert to ES5 syntax
|
||||
- Read `workerScriptPath` config from code, generate to specified path
|
||||
- Generate `worker-mapping.json` mapping file
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated Worker system documentation with WeChat Mini Game support section
|
||||
- Added English Worker system documentation (`docs/en/guide/worker-system.md`)
|
||||
|
||||
---
|
||||
|
||||
## v2.3.1 (2025-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Type export fix**: Fix type export issues in v2.3.0
|
||||
- Resolve `ServiceToken` cross-package type compatibility issues
|
||||
- Fix type references in `editor-app` and `behavior-tree-editor`
|
||||
|
||||
---
|
||||
|
||||
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
|
||||
|
||||
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
- **Persistent Entity**: Add entity cross-scene migration support (#285)
|
||||
- New `EEntityLifecyclePolicy` enum (`SceneLocal`/`Persistent`)
|
||||
- Entity adds `setPersistent()`, `setSceneLocal()`, `isPersistent` API
|
||||
- Scene adds `findPersistentEntities()`, `extractPersistentEntities()`, `receiveMigratedEntities()`
|
||||
- `SceneManager.setScene()` automatically handles persistent entity migration
|
||||
- Use cases: global managers, player characters, cross-scene state persistence
|
||||
|
||||
- **CommandBuffer Deferred Command System**: Execute entity operations uniformly at end of frame (#281)
|
||||
- Support deferred add/remove components, destroy entities, set entity active state
|
||||
- Each system has its own `commands` property
|
||||
- Avoid modifying entity list during iteration, preventing iteration issues
|
||||
- Scene automatically flushes all command buffers after `lateUpdate`
|
||||
|
||||
### Performance
|
||||
|
||||
- **ReactiveQuery Snapshot Optimization**: Optimize entity query iteration performance (#281)
|
||||
- Add snapshot mechanism to avoid copying arrays every frame
|
||||
- Only create new snapshots when entity list changes
|
||||
- Multiple systems share the same snapshot in static scenes
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
|
||||
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
|
||||
- Use dirty entity set instead of iterating all entities
|
||||
- Static scene `process()` optimized from O(n) to O(1)
|
||||
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
|
||||
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
|
||||
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
|
||||
- System initialization now triggers `onAdded` callback for existing matching entities
|
||||
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
|
||||
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **System stable sorting**: Add stable sorting support for systems (#257)
|
||||
- Added `addOrder` property for stable sorting when `updateOrder` is the same
|
||||
- Ensures systems with same priority execute in add order
|
||||
|
||||
- **Module configuration**: Add `module.json` configuration file (#256)
|
||||
- Define module ID, name, version and other metadata
|
||||
- Support module dependency declaration and export configuration
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
|
||||
- `ProfilerSDK`: Unified performance analysis interface
|
||||
- Manual sampling markers (`beginSample`/`endSample`)
|
||||
- Automatic scope measurement (`measure`/`measureAsync`)
|
||||
- Call hierarchy tracking and call graph generation
|
||||
- Counter and gauge support
|
||||
- `AdvancedProfilerCollector`: Advanced performance data collector
|
||||
- Frame time statistics and history
|
||||
- Memory snapshots and GC detection
|
||||
- Long task detection (Long Task API)
|
||||
- Performance report generation
|
||||
- `DebugManager`: Debug manager
|
||||
- Unified debug tool entry point
|
||||
- Profiler integration
|
||||
|
||||
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
|
||||
- Support more serialization option configurations
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem test coverage**: Add complete system test cases (#240)
|
||||
- Cover entity query, cache, lifecycle scenarios
|
||||
|
||||
- **Matcher enhancement**: Optimize matcher functionality (#240)
|
||||
- Improved matching logic and performance
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry enhancement**: Add new component registry features (#244)
|
||||
- Support registering and querying component types by name
|
||||
- Add component mask caching for performance optimization
|
||||
|
||||
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
|
||||
- Support more flexible serialization configuration
|
||||
- Improved nested object serialization
|
||||
|
||||
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
|
||||
- `onSceneStart()`: Called when scene starts
|
||||
- `onSceneStop()`: Called when scene stops
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **Component lifecycle**: Add component lifecycle callback support (#237)
|
||||
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
|
||||
|
||||
- **ServiceContainer enhancement**: Improve service container functionality (#237)
|
||||
- Support `Symbol.for()` pattern for service identifiers
|
||||
- Added `@InjectProperty` property injection decorator
|
||||
- Improved service resolution and lifecycle management
|
||||
|
||||
- **SceneSerializer enhancement**: New scene serializer features (#237)
|
||||
- Support serialization of more component types
|
||||
- Improved deserialization error handling
|
||||
|
||||
- **Property decorator extension**: Extend `@serialize` decorator (#238)
|
||||
- Support `@range`, `@slider` and other editor hints
|
||||
- Support `@dropdown`, `@color` and other UI types
|
||||
- Support `@asset` resource reference type
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher tests**: Add Matcher test cases (#240)
|
||||
- **EntitySystem tests**: Add complete entity system test coverage (#240)
|
||||
|
||||
---
|
||||
|
||||
## Version Notes
|
||||
|
||||
- **Major version**: Breaking changes
|
||||
- **Minor version**: New features (backward compatible)
|
||||
- **Patch version**: Bug fixes and improvements
|
||||
|
||||
## Related Links
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/esengine/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [Documentation Home](./index.md)
|
||||
File diff suppressed because it is too large
Load Diff
317
docs/en/index.md
317
docs/en/index.md
@@ -1,317 +0,0 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - High-performance TypeScript ECS Framework
|
||||
---
|
||||
|
||||
<ParticleHeroEn />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">Quick Links</h2>
|
||||
<a href="/en/guide/" class="news-more">View Docs</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/en/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">Quick Start</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Get Started in 5 Minutes</h3>
|
||||
<p>From installation to your first ECS app, learn the core concepts quickly.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/en/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI System</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>Visual Behavior Tree Editor</h3>
|
||||
<p>Built-in AI behavior tree system with visual editing and real-time debugging.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">Core Features</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">High-performance ECS Architecture</h3>
|
||||
<p class="feature-desc">Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.</p>
|
||||
<a href="/en/guide/entity" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Full Type Support</h3>
|
||||
<p class="feature-desc">100% TypeScript with complete type definitions and compile-time checking for the best development experience.</p>
|
||||
<a href="/en/guide/component" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Visual Behavior Tree</h3>
|
||||
<p class="feature-desc">Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.</p>
|
||||
<a href="/en/guide/behavior-tree/" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Multi-Platform Support</h3>
|
||||
<p class="feature-desc">Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.</p>
|
||||
<a href="/en/guide/platform-adapter" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Modular Design</h3>
|
||||
<p class="feature-desc">Core features packaged independently, import only what you need. Support for custom plugin extensions.</p>
|
||||
<a href="/en/guide/plugin-system" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">Developer Tools</h3>
|
||||
<p class="feature-desc">Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.</p>
|
||||
<a href="/en/guide/logging" class="feature-link">Learn more</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,725 +0,0 @@
|
||||
# 组件系统
|
||||
|
||||
在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。
|
||||
|
||||
## 基本概念
|
||||
|
||||
组件是继承自 `Component` 抽象基类的具体类,用于:
|
||||
- 存储实体的数据(如位置、速度、健康值等)
|
||||
- 定义与数据相关的行为方法
|
||||
- 提供生命周期回调钩子
|
||||
- 支持序列化和调试
|
||||
|
||||
## 创建组件
|
||||
|
||||
### 基础组件定义
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 组件可以包含行为方法
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @ECSComponent 装饰器
|
||||
|
||||
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||
|
||||
#### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||
|
||||
#### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ 推荐:类型名与类名保持一致
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确序列化
|
||||
// 2. 组件未注册到框架,查询和匹配可能失效
|
||||
}
|
||||
```
|
||||
|
||||
#### 与 @Serializable 配合使用
|
||||
|
||||
当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
|
||||
|
||||
#### 组件类型名的唯一性
|
||||
|
||||
每个组件的类型名应该是唯一的:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:两个组件使用相同的类型名
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // 冲突!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ 正确:使用不同的类型名
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## 组件生命周期
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* 组件被添加到实体时调用
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件与实体的关系
|
||||
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
每个组件都有一些内置属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果需要访问实体对象,应该在 System 中进行:
|
||||
|
||||
```typescript
|
||||
class ExampleSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(ExampleComponent));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const comp = entity.getComponent(ExampleComponent)!;
|
||||
console.log(`实体名称: ${entity.name}`);
|
||||
console.log(`组件数据: ${comp.someData}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 复杂组件示例
|
||||
|
||||
### 状态机组件
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据组件
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // 深拷贝避免共享引用
|
||||
}
|
||||
|
||||
// 提供便捷的访问方法
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// 太多不相关的属性
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用构造函数初始化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 明确的类型定义
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 高级特性
|
||||
|
||||
### EntityRef 装饰器 - 自动引用追踪
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
#### 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
#### 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
#### 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
#### 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
```typescript
|
||||
// 推荐:适合使用 @EntityRef 的场景 - 父子关系
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
|
||||
@ECSComponent('AITarget')
|
||||
class AITarget extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null; // 如果目标频繁变化,用ID更好
|
||||
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
|
||||
// 推荐:这种场景用ID更好
|
||||
@ECSComponent('AITarget')
|
||||
class AITargetBetter extends Component {
|
||||
targetId: number | null = null; // 存储ID
|
||||
updateCooldown: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
#### 总结
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
@@ -1,750 +0,0 @@
|
||||
# 实体查询系统
|
||||
|
||||
实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Matcher - 查询条件描述符
|
||||
|
||||
Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。
|
||||
|
||||
### QuerySystem - 查询执行引擎
|
||||
|
||||
QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。
|
||||
|
||||
## 在 EntitySystem 中使用 Matcher
|
||||
|
||||
这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 方式1: 使用 Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// 方式2: 直接使用 Matcher.all() (等价)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher 链式 API
|
||||
|
||||
#### all() - 必须包含所有组件
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体必须同时拥有 Health 和 Position 组件
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理同时拥有两个组件的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - 至少包含一个组件
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体至少拥有 Health 或 Shield 其中之一
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理拥有生命值或护盾的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - 不能包含指定组件
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体不能拥有 DeadTag 组件
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 组合条件
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // 必须有位置和生命
|
||||
.any(WeaponComponent, MagicComponent) // 至少有武器或魔法
|
||||
.none(DeadTag, FrozenTag) // 不能是死亡或冰冻状态
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理可以战斗的活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - 不匹配任何实体
|
||||
|
||||
用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 不匹配任何实体
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing() 的区别
|
||||
|
||||
| 方法 | 行为 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 |
|
||||
| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 |
|
||||
|
||||
```typescript
|
||||
// empty() - 返回场景中的所有实体
|
||||
class AllEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 包含场景中的所有实体
|
||||
console.log(`场景中共有 ${entities.length} 个实体`);
|
||||
}
|
||||
}
|
||||
|
||||
// nothing() - 不返回任何实体
|
||||
class NoEntitiesSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// entities 永远是空数组,此方法不会被调用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定标签的实体
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理玩家实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定名称的实体
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理名为 'Boss' 的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 直接使用 QuerySystem
|
||||
|
||||
如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。
|
||||
|
||||
### 基础查询方法
|
||||
|
||||
```typescript
|
||||
// 获取场景的查询系统
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询拥有所有指定组件的实体
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result1.count} 个移动实体`);
|
||||
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// 查询拥有任意指定组件的实体
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`找到 ${result2.count} 个战斗单位`);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`找到 ${result3.count} 个活着的实体`);
|
||||
```
|
||||
|
||||
### 按标签查询
|
||||
|
||||
```typescript
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('玩家:', player.name);
|
||||
}
|
||||
```
|
||||
|
||||
### 按名称查询
|
||||
|
||||
```typescript
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('找到Boss:', boss);
|
||||
}
|
||||
```
|
||||
|
||||
### 按单个组件查询
|
||||
|
||||
```typescript
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`有 ${healthResult.count} 个实体拥有生命值`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 自动缓存
|
||||
|
||||
QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:
|
||||
|
||||
```typescript
|
||||
// 第一次查询,执行实际查询
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// 第二次相同查询,使用缓存
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### 实体变化自动更新
|
||||
|
||||
当实体添加/移除组件时,查询缓存会自动更新:
|
||||
|
||||
```typescript
|
||||
// 查询拥有武器的实体
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之前:', before.count); // 假设为 5
|
||||
|
||||
// 给实体添加武器
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// 再次查询,自动包含新实体
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之后:', after.count); // 现在是 6
|
||||
```
|
||||
|
||||
### 查询性能统计
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('总查询次数:', stats.queryStats.totalQueries);
|
||||
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
|
||||
console.log('缓存大小:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 场景1: 物理系统
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// 应用重力
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// 更新位置
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 渲染系统
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // 排除不可见实体
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按 z-order 排序
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// 渲染实体
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 碰撞检测
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 O(n²) 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// 触发碰撞事件
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
|
||||
private isOverlapping(...args: any[]): boolean {
|
||||
// 碰撞检测逻辑
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4: 一次性查询
|
||||
|
||||
```typescript
|
||||
// 在系统外部执行一次性查询
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编译查询 (CompiledQuery)
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```typescript
|
||||
// 创建编译查询
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// 类型安全的遍历 - 组件参数自动推断类型
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// 获取实体数量
|
||||
console.log(`匹配实体数: ${query.count}`);
|
||||
|
||||
// 获取第一个匹配的实体
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`第一个实体: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 变更检测
|
||||
|
||||
CompiledQuery 支持基于 epoch 的变更检测:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理 Transform 或 Sprite 发生变化的实体
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// 保存当前 epoch 作为下次检查的起点
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// 更新渲染数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 函数式 API
|
||||
|
||||
CompiledQuery 提供了丰富的函数式 API:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - 转换实体数据
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - 过滤实体
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - 查找第一个匹配的实体
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - 转换为数组
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - 检查是否有匹配
|
||||
if (query.any()) {
|
||||
console.log('有匹配的实体');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('没有匹配的实体');
|
||||
}
|
||||
```
|
||||
|
||||
### CompiledQuery vs EntitySystem
|
||||
|
||||
| 特性 | CompiledQuery | EntitySystem |
|
||||
|------|---------------|--------------|
|
||||
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
|
||||
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
|
||||
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
|
||||
| **变更检测** | forEachChanged | forEachChanged |
|
||||
| **事件监听** | 无 | addEventListener |
|
||||
| **命令缓冲** | 无 | this.commands |
|
||||
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
|
||||
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
|
||||
|
||||
**选择建议**:
|
||||
|
||||
- 使用 **EntitySystem** 处理核心游戏逻辑(移动、战斗、AI 等)
|
||||
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
|
||||
|
||||
### CompiledQuery API 参考
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
|
||||
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
|
||||
| `first()` | 获取第一个匹配的实体和组件 |
|
||||
| `toArray()` | 转换为 [entity, ...components] 数组 |
|
||||
| `map(callback)` | 映射转换 |
|
||||
| `filter(predicate)` | 过滤实体 |
|
||||
| `find(predicate)` | 查找第一个满足条件的实体 |
|
||||
| `any()` | 是否有任何匹配 |
|
||||
| `empty()` | 是否没有匹配 |
|
||||
| `count` | 匹配的实体数量 |
|
||||
| `entities` | 匹配的实体列表(只读) |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 优先使用 EntitySystem
|
||||
|
||||
```typescript
|
||||
// 推荐: 使用 EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 自动获得符合条件的实体,每帧自动更新
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐: 在 update 中手动查询
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每帧手动查询,浪费性能
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理使用 none() 排除条件
|
||||
|
||||
```typescript
|
||||
// 排除已死亡的敌人
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // 不处理死亡的敌人
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用标签优化查询
|
||||
|
||||
```typescript
|
||||
// 不好: 查询所有实体再过滤
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// 好: 直接按标签查询
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. 避免过于复杂的查询条件
|
||||
|
||||
```typescript
|
||||
// 不推荐: 过于复杂
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// 推荐: 拆分成多个简单系统
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 查询结果是只读的
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// 不要修改返回的数组
|
||||
result.entities.push(someEntity); // 错误!
|
||||
|
||||
// 如果需要修改,先复制
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // 正确
|
||||
```
|
||||
|
||||
### 2. 组件添加/移除后的查询时机
|
||||
|
||||
```typescript
|
||||
// 创建实体并添加组件
|
||||
const entity = scene.createEntity('Player');
|
||||
entity.addComponent(new PositionComponent());
|
||||
|
||||
// 立即查询可能获取到新实体
|
||||
const result = scene.querySystem.queryAll(PositionComponent);
|
||||
// result.entities 包含新创建的实体
|
||||
```
|
||||
|
||||
### 3. Matcher 是不可变的
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// 链式调用返回新的 Matcher 实例
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// matcher 本身不变
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
## Matcher API 快速参考
|
||||
|
||||
### 静态创建方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` |
|
||||
|
||||
### 链式方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` |
|
||||
| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | 添加排除的组件 | `.none(Dead)` |
|
||||
| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` |
|
||||
| `.withName(name)` | 添加名称条件 | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` |
|
||||
|
||||
### 实用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `.getCondition()` | 获取查询条件(只读) |
|
||||
| `.isEmpty()` | 检查是否为空条件 |
|
||||
| `.isNothing()` | 检查是否为 nothing 匹配器 |
|
||||
| `.clone()` | 克隆匹配器 |
|
||||
| `.reset()` | 重置所有条件 |
|
||||
| `.toString()` | 获取字符串表示 |
|
||||
|
||||
### 常用组合示例
|
||||
|
||||
```typescript
|
||||
// 基础移动系统
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// 可攻击的活着的实体
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// 所有带标签的敌人
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// 只需要生命周期的系统
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## 相关 API
|
||||
|
||||
- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考
|
||||
- [QuerySystem](../api/classes/QuerySystem.md) - 查询系统 API 参考
|
||||
- [EntitySystem](../api/classes/EntitySystem.md) - 实体系统 API 参考
|
||||
- [Entity](../api/classes/Entity.md) - 实体 API 参考
|
||||
@@ -1,360 +0,0 @@
|
||||
# 持久化实体
|
||||
|
||||
> **版本**: v2.3.0+
|
||||
|
||||
持久化实体(Persistent Entity)是一种可以在场景切换时自动迁移到新场景的特殊实体。适用于需要跨场景保持状态的游戏对象,如玩家、游戏管理器、音频管理器等。
|
||||
|
||||
## 基本概念
|
||||
|
||||
在 ECS 框架中,实体有两种生命周期策略:
|
||||
|
||||
| 策略 | 说明 | 默认 |
|
||||
|-----|------|------|
|
||||
| `SceneLocal` | 场景本地实体,场景切换时销毁 | ✓ |
|
||||
| `Persistent` | 持久化实体,场景切换时自动迁移 | |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建持久化实体
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 创建持久化玩家实体
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(100, 200));
|
||||
player.addComponent(new PlayerData('Hero', 500));
|
||||
|
||||
// 创建普通敌人实体(场景切换时销毁)
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(300, 200));
|
||||
enemy.addComponent(new EnemyAI());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换时的行为
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始场景
|
||||
class Level1Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 玩家 - 持久化,会迁移到下一个场景
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Position(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 敌人 - 场景本地,切换时销毁
|
||||
const enemy = this.createEntity('Enemy');
|
||||
enemy.addComponent(new Position(100, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// 目标场景
|
||||
class Level2Scene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 新的敌人
|
||||
const enemy = this.createEntity('Boss');
|
||||
enemy.addComponent(new Position(200, 200));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
// 玩家已自动迁移到此场景
|
||||
const player = this.findEntity('Player');
|
||||
console.log(player !== null); // true
|
||||
|
||||
// 位置和血量数据完整保留
|
||||
const position = player?.getComponent(Position);
|
||||
const health = player?.getComponent(Health);
|
||||
console.log(position?.x, position?.y); // 0, 0
|
||||
console.log(health?.value); // 100
|
||||
}
|
||||
}
|
||||
|
||||
// 切换场景
|
||||
Core.create({ debug: true });
|
||||
Core.setScene(new Level1Scene());
|
||||
|
||||
// 稍后切换到 Level2
|
||||
Core.loadScene(new Level2Scene());
|
||||
// Player 实体自动迁移,Enemy 实体被销毁
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Entity 方法
|
||||
|
||||
#### setPersistent()
|
||||
|
||||
将实体标记为持久化,场景切换时不会被销毁。
|
||||
|
||||
```typescript
|
||||
public setPersistent(): this
|
||||
```
|
||||
|
||||
**返回**: 返回实体本身,支持链式调用
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const player = scene.createEntity('Player')
|
||||
.setPersistent();
|
||||
|
||||
player.addComponent(new Position(100, 200));
|
||||
```
|
||||
|
||||
#### setSceneLocal()
|
||||
|
||||
将实体恢复为场景本地策略(默认)。
|
||||
|
||||
```typescript
|
||||
public setSceneLocal(): this
|
||||
```
|
||||
|
||||
**返回**: 返回实体本身,支持链式调用
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
// 动态取消持久化
|
||||
player.setSceneLocal();
|
||||
```
|
||||
|
||||
#### isPersistent
|
||||
|
||||
检查实体是否为持久化实体。
|
||||
|
||||
```typescript
|
||||
public get isPersistent(): boolean
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
if (entity.isPersistent) {
|
||||
console.log('这是持久化实体');
|
||||
}
|
||||
```
|
||||
|
||||
#### lifecyclePolicy
|
||||
|
||||
获取实体的生命周期策略。
|
||||
|
||||
```typescript
|
||||
public get lifecyclePolicy(): EEntityLifecyclePolicy
|
||||
```
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
|
||||
|
||||
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
|
||||
console.log('持久化实体');
|
||||
}
|
||||
```
|
||||
|
||||
### Scene 方法
|
||||
|
||||
#### findPersistentEntities()
|
||||
|
||||
查找场景中所有持久化实体。
|
||||
|
||||
```typescript
|
||||
public findPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**返回**: 持久化实体数组
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const persistentEntities = scene.findPersistentEntities();
|
||||
console.log(`场景中有 ${persistentEntities.length} 个持久化实体`);
|
||||
```
|
||||
|
||||
#### extractPersistentEntities()
|
||||
|
||||
提取并从场景中移除所有持久化实体(通常由框架内部调用)。
|
||||
|
||||
```typescript
|
||||
public extractPersistentEntities(): Entity[]
|
||||
```
|
||||
|
||||
**返回**: 被提取的持久化实体数组
|
||||
|
||||
#### receiveMigratedEntities()
|
||||
|
||||
接收迁移过来的实体(通常由框架内部调用)。
|
||||
|
||||
```typescript
|
||||
public receiveMigratedEntities(entities: Entity[]): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `entities` - 要接收的实体数组
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 玩家实体跨关卡
|
||||
|
||||
```typescript
|
||||
class PlayerSetupScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 玩家在所有关卡中保持状态
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
player.addComponent(new Transform(0, 0));
|
||||
player.addComponent(new Health(100));
|
||||
player.addComponent(new Inventory());
|
||||
player.addComponent(new PlayerStats());
|
||||
}
|
||||
}
|
||||
|
||||
class Level1 extends Scene { /* ... */ }
|
||||
class Level2 extends Scene { /* ... */ }
|
||||
class Level3 extends Scene { /* ... */ }
|
||||
|
||||
// 玩家实体会自动在所有关卡间迁移
|
||||
Core.setScene(new PlayerSetupScene());
|
||||
// ... 游戏进行
|
||||
Core.loadScene(new Level1());
|
||||
// ... 关卡完成
|
||||
Core.loadScene(new Level2());
|
||||
// 玩家数据(血量、物品栏、属性)完整保留
|
||||
```
|
||||
|
||||
### 2. 全局管理器
|
||||
|
||||
```typescript
|
||||
class BootstrapScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 音频管理器 - 跨场景保持
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent();
|
||||
audioManager.addComponent(new AudioController());
|
||||
|
||||
// 成就管理器 - 跨场景保持
|
||||
const achievementManager = this.createEntity('AchievementManager').setPersistent();
|
||||
achievementManager.addComponent(new AchievementTracker());
|
||||
|
||||
// 游戏设置 - 跨场景保持
|
||||
const settings = this.createEntity('GameSettings').setPersistent();
|
||||
settings.addComponent(new SettingsData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动态切换持久化状态
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 初始创建为普通实体
|
||||
const companion = this.createEntity('Companion');
|
||||
companion.addComponent(new Transform(0, 0));
|
||||
companion.addComponent(new CompanionAI());
|
||||
|
||||
// 监听招募事件
|
||||
this.eventSystem.on('companion:recruited', () => {
|
||||
// 招募后变为持久化实体
|
||||
companion.setPersistent();
|
||||
console.log('同伴已加入队伍,将跟随玩家跨场景');
|
||||
});
|
||||
|
||||
// 监听解散事件
|
||||
this.eventSystem.on('companion:dismissed', () => {
|
||||
// 解散后恢复为场景本地实体
|
||||
companion.setSceneLocal();
|
||||
console.log('同伴已离队,不再跨场景');
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确标识持久化实体
|
||||
|
||||
```typescript
|
||||
// 推荐:在创建时立即标记
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
|
||||
// 不推荐:创建后再标记(容易遗漏)
|
||||
const player = this.createEntity('Player');
|
||||
// ... 很多代码 ...
|
||||
player.setPersistent(); // 容易忘记
|
||||
```
|
||||
|
||||
### 2. 合理使用持久化
|
||||
|
||||
```typescript
|
||||
// ✓ 适合持久化的实体
|
||||
const player = this.createEntity('Player').setPersistent(); // 玩家
|
||||
const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器
|
||||
const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统
|
||||
|
||||
// ✗ 不应持久化的实体
|
||||
const bullet = this.createEntity('Bullet'); // 临时对象
|
||||
const enemy = this.createEntity('Enemy'); // 关卡特定敌人
|
||||
const particle = this.createEntity('Particle'); // 特效粒子
|
||||
```
|
||||
|
||||
### 3. 检查迁移后的实体
|
||||
|
||||
```typescript
|
||||
class NewScene extends Scene {
|
||||
public onStart(): void {
|
||||
// 检查预期的持久化实体是否存在
|
||||
const player = this.findEntity('Player');
|
||||
if (!player) {
|
||||
console.error('玩家实体未正确迁移!');
|
||||
// 处理错误情况
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// ✗ 避免:持久化实体引用场景本地实体
|
||||
class BadScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// 危险:player 持久化但 enemy 不是
|
||||
// 场景切换后 enemy 被销毁,引用失效
|
||||
player.addComponent(new TargetComponent(enemy));
|
||||
}
|
||||
}
|
||||
|
||||
// ✓ 推荐:使用 ID 引用或事件系统
|
||||
class GoodScene extends Scene {
|
||||
protected initialize(): void {
|
||||
const player = this.createEntity('Player').setPersistent();
|
||||
const enemy = this.createEntity('Enemy');
|
||||
|
||||
// 存储 ID 而非直接引用
|
||||
player.addComponent(new TargetComponent(enemy.id));
|
||||
|
||||
// 或使用事件系统通信
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久化也不会迁移。
|
||||
|
||||
2. **组件数据完整保留**:迁移时所有组件及其状态都会保留。
|
||||
|
||||
3. **场景引用会更新**:迁移后实体的 `scene` 属性会指向新场景。
|
||||
|
||||
4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。
|
||||
|
||||
5. **延迟切换同样生效**:使用 `Core.loadScene()` 延迟切换时,持久化实体同样会迁移。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景管理](./scene.md) - 了解场景的基本使用
|
||||
- [SceneManager](./scene-manager.md) - 了解场景切换
|
||||
- [WorldManager](./world-manager.md) - 了解多世界管理
|
||||
@@ -1,681 +0,0 @@
|
||||
# SceneManager
|
||||
|
||||
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
|
||||
|
||||
## 适用场景
|
||||
|
||||
SceneManager 适合以下场景:
|
||||
- 单人游戏
|
||||
- 简单多人游戏
|
||||
- 移动游戏
|
||||
- 需要场景切换的游戏(菜单、游戏、暂停等)
|
||||
- 不需要多 World 隔离的项目
|
||||
|
||||
## 特点
|
||||
|
||||
- 轻量级,零额外开销
|
||||
- 简单直观的 API
|
||||
- 支持延迟场景切换(避免在当前帧中途切换)
|
||||
- 自动管理 ECS 流式 API
|
||||
- 自动处理场景生命周期
|
||||
- 集成在 Core 中,自动更新
|
||||
- 支持[持久化实体](./persistent-entity.md)跨场景迁移(v2.3.0+)
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 推荐方式:使用 Core 的静态方法
|
||||
|
||||
这是最简单和推荐的方式,适合大多数应用:
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 1. 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 2. 创建并设置场景
|
||||
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 Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏场景已启动");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设置场景
|
||||
Core.setScene(new GameScene());
|
||||
|
||||
// 4. 游戏循环(Core.update 会自动更新场景)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// Laya 引擎集成
|
||||
Laya.timer.frameLoop(1, this, () => {
|
||||
const deltaTime = Laya.timer.delta / 1000;
|
||||
Core.update(deltaTime);
|
||||
});
|
||||
|
||||
// Cocos Creator 集成
|
||||
update(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 高级方式:直接使用 SceneManager
|
||||
|
||||
如果需要更多控制,可以直接使用 SceneManager:
|
||||
|
||||
```typescript
|
||||
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 获取 SceneManager(Core 已自动创建并注册)
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置场景
|
||||
const gameScene = new GameScene();
|
||||
sceneManager.setScene(gameScene);
|
||||
|
||||
// 游戏循环(仍然使用 Core.update)
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Core会自动调用sceneManager.update()
|
||||
}
|
||||
```
|
||||
|
||||
**重要**:无论使用哪种方式,游戏循环中都应该只调用 `Core.update()`,它会自动更新 SceneManager 和场景。不需要手动调用 `sceneManager.update()`。
|
||||
|
||||
## 场景切换
|
||||
|
||||
### 立即切换
|
||||
|
||||
使用 `Core.setScene()` 或 `sceneManager.setScene()` 立即切换场景:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new MenuScene());
|
||||
```
|
||||
|
||||
### 延迟切换
|
||||
|
||||
使用 `Core.loadScene()` 或 `sceneManager.loadScene()` 延迟切换场景,场景会在下一帧切换:
|
||||
|
||||
```typescript
|
||||
// 方式1:使用 Core(推荐)
|
||||
Core.loadScene(new GameOverScene());
|
||||
|
||||
// 方式2:使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
在 System 中切换场景时,应该使用延迟切换:
|
||||
|
||||
```typescript
|
||||
class GameOverSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
const player = entities.find(e => e.name === 'Player');
|
||||
const health = player?.getComponent(Health);
|
||||
|
||||
if (health && health.value <= 0) {
|
||||
// 延迟切换到游戏结束场景(下一帧生效)
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续执行,不会中断当前系统的处理
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整的场景切换示例
|
||||
|
||||
```typescript
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 菜单场景
|
||||
class MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
|
||||
// 监听开始游戏事件
|
||||
this.eventSystem.on('start_game', () => {
|
||||
Core.loadScene(new GameScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示菜单界面");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("菜单场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏场景
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameScene";
|
||||
|
||||
// 创建游戏实体
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(400, 300));
|
||||
player.addComponent(new Health(100));
|
||||
|
||||
// 监听游戏结束事件
|
||||
this.eventSystem.on('game_over', () => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("游戏开始");
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log("游戏场景卸载");
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏结束场景
|
||||
class GameOverScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "GameOverScene";
|
||||
|
||||
// 监听返回菜单事件
|
||||
this.eventSystem.on('back_to_menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("显示游戏结束界面");
|
||||
}
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新场景
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Core 静态方法(推荐)
|
||||
|
||||
#### Core.setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public static setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要设置的场景实例
|
||||
|
||||
**返回**:
|
||||
- 返回设置的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const gameScene = Core.setScene(new GameScene());
|
||||
console.log(gameScene.name);
|
||||
```
|
||||
|
||||
#### Core.loadScene()
|
||||
|
||||
延迟加载场景(下一帧切换)。
|
||||
|
||||
```typescript
|
||||
public static loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `scene` - 要加载的场景实例
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
Core.loadScene(new GameOverScene());
|
||||
```
|
||||
|
||||
#### Core.scene
|
||||
|
||||
获取当前活跃的场景。
|
||||
|
||||
```typescript
|
||||
public static get scene(): IScene | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- 当前场景实例,如果没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const currentScene = Core.scene;
|
||||
if (currentScene) {
|
||||
console.log(`当前场景: ${currentScene.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### Core.ecsAPI
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public static get ecsAPI(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
**返回**:
|
||||
- ECS API 实例,如果当前没有场景则返回 null
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (api) {
|
||||
// 查询实体
|
||||
const enemies = api.find(Enemy, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('game:start', { level: 1 });
|
||||
}
|
||||
```
|
||||
|
||||
### SceneManager 方法(高级)
|
||||
|
||||
如果需要直接使用 SceneManager,可以通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
```
|
||||
|
||||
#### setScene()
|
||||
|
||||
立即切换场景。
|
||||
|
||||
```typescript
|
||||
public setScene<T extends IScene>(scene: T): T
|
||||
```
|
||||
|
||||
#### loadScene()
|
||||
|
||||
延迟加载场景。
|
||||
|
||||
```typescript
|
||||
public loadScene<T extends IScene>(scene: T): void
|
||||
```
|
||||
|
||||
#### currentScene
|
||||
|
||||
获取当前场景。
|
||||
|
||||
```typescript
|
||||
public get currentScene(): IScene | null
|
||||
```
|
||||
|
||||
#### api
|
||||
|
||||
获取 ECS 流式 API。
|
||||
|
||||
```typescript
|
||||
public get api(): ECSFluentAPI | null
|
||||
```
|
||||
|
||||
#### hasScene
|
||||
|
||||
检查是否有活跃场景。
|
||||
|
||||
```typescript
|
||||
public get hasScene(): boolean
|
||||
```
|
||||
|
||||
#### hasPendingScene
|
||||
|
||||
检查是否有待切换的场景。
|
||||
|
||||
```typescript
|
||||
public get hasPendingScene(): boolean
|
||||
```
|
||||
|
||||
## 使用 ECS 流式 API
|
||||
|
||||
通过 `Core.ecsAPI` 可以方便地访问场景的 ECS 功能:
|
||||
|
||||
```typescript
|
||||
const api = Core.ecsAPI;
|
||||
if (!api) {
|
||||
console.error('没有活跃场景');
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询实体
|
||||
const players = api.find(Player, Transform);
|
||||
const enemies = api.find(Enemy, Health, Transform);
|
||||
|
||||
// 发射事件
|
||||
api.emit('player:scored', { points: 100 });
|
||||
|
||||
// 监听事件
|
||||
api.on('enemy:died', (data) => {
|
||||
console.log('敌人死亡:', data);
|
||||
});
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 Core 的静态方法
|
||||
|
||||
```typescript
|
||||
// 推荐:使用 Core 的静态方法
|
||||
Core.setScene(new GameScene());
|
||||
Core.loadScene(new MenuScene());
|
||||
const currentScene = Core.scene;
|
||||
|
||||
// 不推荐:除非有特殊需求,否则不需要直接使用 SceneManager
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
sceneManager.setScene(new GameScene());
|
||||
```
|
||||
|
||||
### 2. 只调用 Core.update()
|
||||
|
||||
```typescript
|
||||
// 正确:只调用 Core.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有服务和场景
|
||||
}
|
||||
|
||||
// 错误:不要手动调用 sceneManager.update()
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
sceneManager.update(); // 重复更新,会导致问题!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用延迟切换避免问题
|
||||
|
||||
在 System 中切换场景时,应该使用 `loadScene()` 而不是 `setScene()`:
|
||||
|
||||
```typescript
|
||||
// 推荐:延迟切换
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.loadScene(new GameOverScene());
|
||||
// 当前帧继续处理其他实体
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:立即切换可能导致问题
|
||||
class HealthSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health.value <= 0) {
|
||||
Core.setScene(new GameOverScene());
|
||||
// 场景立即切换,当前帧的其他实体可能无法正常处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 场景职责分离
|
||||
|
||||
每个场景应该只负责一个特定的游戏状态:
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 职责清晰
|
||||
class MenuScene extends Scene {
|
||||
// 只处理菜单相关逻辑
|
||||
}
|
||||
|
||||
class GameScene extends Scene {
|
||||
// 只处理游戏玩法逻辑
|
||||
}
|
||||
|
||||
class PauseScene extends Scene {
|
||||
// 只处理暂停界面逻辑
|
||||
}
|
||||
|
||||
// 避免的设计 - 职责混乱
|
||||
class MegaScene extends Scene {
|
||||
// 包含菜单、游戏、暂停等所有逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 资源管理
|
||||
|
||||
在场景的 `unload()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private textures: Map<string, any> = new Map();
|
||||
private sounds: Map<string, any> = new Map();
|
||||
|
||||
protected initialize(): void {
|
||||
this.loadResources();
|
||||
}
|
||||
|
||||
private loadResources(): void {
|
||||
this.textures.set('player', loadTexture('player.png'));
|
||||
this.sounds.set('bgm', loadSound('bgm.mp3'));
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
// 清理资源
|
||||
this.textures.clear();
|
||||
this.sounds.clear();
|
||||
console.log('场景资源已清理');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 事件驱动的场景切换
|
||||
|
||||
使用事件系统来触发场景切换,保持代码解耦:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// 监听场景切换事件
|
||||
this.eventSystem.on('goto:menu', () => {
|
||||
Core.loadScene(new MenuScene());
|
||||
});
|
||||
|
||||
this.eventSystem.on('goto:gameover', (data) => {
|
||||
Core.loadScene(new GameOverScene());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中触发事件
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
if (levelComplete) {
|
||||
this.scene.eventSystem.emitSync('goto:gameover', {
|
||||
score: 1000,
|
||||
level: 5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构层次
|
||||
|
||||
SceneManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── SceneManager (场景管理,自动更新)
|
||||
└── Scene (当前场景)
|
||||
├── EntitySystem (系统)
|
||||
├── Entity (实体)
|
||||
└── Component (组件)
|
||||
```
|
||||
|
||||
## 与 WorldManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
|
||||
## 完整示例
|
||||
|
||||
```typescript
|
||||
import { Core, 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 MenuScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.name = "MenuScene";
|
||||
console.log("菜单场景初始化");
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log("菜单场景启动");
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public onStart(): void {
|
||||
console.log('游戏场景启动');
|
||||
}
|
||||
|
||||
public unload(): void {
|
||||
console.log('游戏场景卸载');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 设置初始场景
|
||||
Core.setScene(new MenuScene());
|
||||
|
||||
// 游戏循环
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime: number) {
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
// 只需要调用 Core.update,它会自动更新场景
|
||||
Core.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
// 切换到游戏场景
|
||||
setTimeout(() => {
|
||||
Core.loadScene(new GameScene());
|
||||
}, 3000);
|
||||
```
|
||||
|
||||
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [持久化实体](./persistent-entity.md) - 了解如何让实体跨场景保持状态
|
||||
- [WorldManager](./world-manager.md) - 了解更高级的多世界隔离功能
|
||||
@@ -1,923 +0,0 @@
|
||||
# 序列化系统
|
||||
|
||||
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
|
||||
|
||||
## 基本概念
|
||||
|
||||
序列化系统分为两个层次:
|
||||
|
||||
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
|
||||
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
|
||||
|
||||
### 支持的数据格式
|
||||
|
||||
- **JSON格式**:人类可读,便于调试和编辑
|
||||
- **Binary格式**:使用MessagePack,体积更小,性能更高
|
||||
|
||||
> **📢 v2.2.2 重要变更**
|
||||
>
|
||||
> 从 v2.2.2 开始,二进制序列化格式返回 `Uint8Array` 而非 Node.js 的 `Buffer`,以确保浏览器兼容性:
|
||||
> - `serialize({ format: 'binary' })` 返回 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `deserialize(data)` 接收 `string | Uint8Array`(原为 `string | Buffer`)
|
||||
> - `applyIncremental(data)` 接收 `IncrementalSnapshot | string | Uint8Array`(原为包含 `Buffer`)
|
||||
>
|
||||
> **迁移影响**:
|
||||
> - ✅ **运行时兼容**:Node.js 的 `Buffer` 继承自 `Uint8Array`,现有代码可直接运行
|
||||
> - ⚠️ **类型检查**:如果你的 TypeScript 代码中显式使用了 `Buffer` 类型,需要改为 `Uint8Array`
|
||||
> - ✅ **浏览器支持**:`Uint8Array` 是标准 JavaScript 类型,所有现代浏览器都支持
|
||||
|
||||
## 全量序列化
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 标记可序列化组件
|
||||
|
||||
使用 `@Serializable` 和 `@Serialize` 装饰器标记需要序列化的组件和字段:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 序列化场景
|
||||
|
||||
```typescript
|
||||
// JSON格式序列化
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // 美化输出
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary格式序列化(更小的体积)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 保存为文件(Node.js环境)
|
||||
// 注意:binaryData 是 Uint8Array 类型,Node.js 的 fs 可以直接写入
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. 反序列化场景
|
||||
|
||||
```typescript
|
||||
// 从JSON恢复
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // 替换当前场景内容
|
||||
});
|
||||
}
|
||||
|
||||
// 从Binary恢复
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // 合并到现有场景
|
||||
});
|
||||
```
|
||||
|
||||
### 序列化选项
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// 指定要序列化的组件类型(可选)
|
||||
components?: ComponentType[];
|
||||
|
||||
// 序列化格式:'json' 或 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// JSON美化输出
|
||||
pretty?: boolean;
|
||||
|
||||
// 包含元数据
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```typescript
|
||||
// 只序列化特定组件类型
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// 反序列化策略
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// 组件类型注册表(可选,默认使用全局注册表)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### 高级装饰器
|
||||
|
||||
#### 字段序列化选项
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// 使用别名
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// 自定义序列化器
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// 忽略序列化
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 集合类型序列化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map序列化
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set序列化
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件继承与序列化
|
||||
|
||||
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
|
||||
|
||||
#### 基础继承
|
||||
|
||||
```typescript
|
||||
// 基类组件
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// 子类组件 - 自动继承父类的序列化字段
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// 另一个子类组件
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
#### 继承规则
|
||||
|
||||
1. **字段继承**:子类自动继承父类所有被 `@Serialize()` 标记的字段
|
||||
2. **独立元数据**:每个子类维护独立的序列化元数据,修改子类不会影响父类或其他子类
|
||||
3. **typeId 区分**:使用 `typeId` 选项为每个类指定唯一标识,确保反序列化时能正确识别组件类型
|
||||
|
||||
#### 使用 typeId 的重要性
|
||||
|
||||
当使用组件继承时,**强烈建议**为每个类设置唯一的 `typeId`:
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:明确指定 typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ 不推荐:依赖类名作为 typeId
|
||||
// 在代码压缩后类名可能变化,导致反序列化失败
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
#### 子类覆盖父类字段
|
||||
|
||||
子类可以重新声明父类的字段以修改其序列化选项:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// 覆盖父类字段,使用不同的别名
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
#### 忽略继承的字段
|
||||
|
||||
使用 `@IgnoreSerialization()` 可以在子类中忽略从父类继承的字段:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// 忽略父类的 friction 和 restitution 字段
|
||||
// 因为 Trigger 不需要物理材质属性
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 场景自定义数据
|
||||
|
||||
除了实体和组件,还可以序列化场景级别的配置数据:
|
||||
|
||||
```typescript
|
||||
// 设置场景数据
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// 序列化时会自动包含场景数据
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// 反序列化后场景数据会恢复
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## 增量序列化
|
||||
|
||||
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
|
||||
|
||||
### 基础用法
|
||||
|
||||
#### 1. 创建基础快照
|
||||
|
||||
```typescript
|
||||
// 在需要开始记录变更前创建基础快照
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
#### 2. 修改场景
|
||||
|
||||
```typescript
|
||||
// 添加实体
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// 修改组件
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// 删除组件
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// 删除实体
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// 修改场景数据
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
#### 3. 获取增量变更
|
||||
|
||||
```typescript
|
||||
// 获取相对于基础快照的所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// 查看变更统计
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('总变更数:', stats.totalChanges);
|
||||
console.log('新增实体:', stats.addedEntities);
|
||||
console.log('删除实体:', stats.removedEntities);
|
||||
console.log('新增组件:', stats.addedComponents);
|
||||
console.log('更新组件:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
#### 4. 序列化增量数据
|
||||
|
||||
```typescript
|
||||
// JSON格式(默认)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// 二进制格式(更小的体积,更高性能)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// 美化JSON输出(便于调试)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// 发送或保存
|
||||
socket.send(binaryData); // 网络传输使用二进制
|
||||
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
|
||||
```
|
||||
|
||||
#### 5. 应用增量变更
|
||||
|
||||
```typescript
|
||||
// 在另一个场景应用变更
|
||||
const otherScene = new Scene();
|
||||
|
||||
// 直接应用增量对象
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// 从JSON字符串应用
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// 从二进制Uint8Array应用
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
### 增量快照管理
|
||||
|
||||
#### 更新快照基准
|
||||
|
||||
在应用增量变更后,可以更新快照基准:
|
||||
|
||||
```typescript
|
||||
// 创建初始快照
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// 第一次修改
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// 更新基准(将当前状态设为新的基准)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// 第二次修改(增量将基于更新后的基准)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
#### 清除快照
|
||||
|
||||
```typescript
|
||||
// 释放快照占用的内存
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// 检查是否有快照
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('存在增量快照');
|
||||
}
|
||||
```
|
||||
|
||||
### 增量序列化选项
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// 是否进行组件数据的深度对比
|
||||
// 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// 是否跟踪场景数据变更
|
||||
// 默认true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// 是否压缩快照(使用JSON序列化)
|
||||
// 默认false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// 序列化格式
|
||||
// 'json': JSON格式(可读性好,方便调试)
|
||||
// 'binary': MessagePack二进制格式(体积小,性能高)
|
||||
// 默认 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// 是否美化JSON输出(仅在format='json'时有效)
|
||||
// 默认false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// 使用选项
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
### 增量数据结构
|
||||
|
||||
增量快照包含以下变更类型:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // 快照版本号
|
||||
timestamp: number; // 时间戳
|
||||
sceneName: string; // 场景名称
|
||||
baseVersion: number; // 基础版本号
|
||||
entityChanges: EntityChange[]; // 实体变更
|
||||
componentChanges: ComponentChange[]; // 组件变更
|
||||
sceneDataChanges: SceneDataChange[]; // 场景数据变更
|
||||
}
|
||||
|
||||
// 变更操作类型
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## 版本迁移
|
||||
|
||||
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
|
||||
|
||||
### 注册迁移函数
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 假设 PlayerComponent v1 有 hp 字段
|
||||
// v2 改为 health 和 maxHealth 字段
|
||||
|
||||
// 注册从版本1到版本2的迁移
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(data) => {
|
||||
// 迁移逻辑
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 使用迁移构建器
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// 从版本2迁移到版本3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
### 场景级迁移
|
||||
|
||||
```typescript
|
||||
// 注册场景级迁移
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // 从版本
|
||||
2, // 到版本
|
||||
(scene) => {
|
||||
// 迁移场景结构
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### 检查迁移路径
|
||||
|
||||
```typescript
|
||||
// 检查是否可以迁移
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // 从版本
|
||||
3 // 到版本
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// 可以安全迁移
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// 获取迁移路径
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('可用迁移版本:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 游戏存档系统
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// 保存游戏
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('游戏已保存');
|
||||
}
|
||||
|
||||
// 加载游戏
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('游戏已加载');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有存档
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 网络同步
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // 100ms同步一次
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// 创建基础快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// 定期发送增量
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// 接收远程增量
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// 只在有变更时发送
|
||||
if (stats.totalChanges > 0) {
|
||||
// 使用二进制格式减少网络传输量
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// 更新基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// 直接应用二进制数据(ArrayBuffer 转 Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 撤销/重做系统
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// 创建初始快照
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// 保存当前状态
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// 删除当前位置之后的历史
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// 更新快照基准
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// 撤销
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 重做
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关卡编辑器
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// 导出关卡
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// 浏览器环境
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导入关卡
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证关卡数据
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('关卡数据无效:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取关卡信息(不完全反序列化)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 选择合适的格式
|
||||
|
||||
- **开发阶段**:使用JSON格式,便于调试和查看
|
||||
- **生产环境**:使用Binary格式,减少30-50%的数据大小
|
||||
|
||||
### 2. 按需序列化
|
||||
|
||||
```typescript
|
||||
// 只序列化需要持久化的组件
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 增量序列化优化
|
||||
|
||||
```typescript
|
||||
// 对于高频同步,关闭深度对比以提升性能
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // 只检测组件的添加/删除
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量修改后再序列化
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// 批量修改
|
||||
});
|
||||
|
||||
// 一次性序列化所有变更
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 明确序列化字段
|
||||
|
||||
```typescript
|
||||
// 明确标记需要序列化的字段
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// 运行时数据不序列化
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用版本控制
|
||||
|
||||
```typescript
|
||||
// 为组件指定版本
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// 版本2的字段
|
||||
}
|
||||
|
||||
// 注册迁移函数确保兼容性
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. 避免循环引用
|
||||
|
||||
```typescript
|
||||
// 不要在组件中直接引用其他实体
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// 存储实体ID而不是实体引用
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 压缩大数据
|
||||
|
||||
```typescript
|
||||
// 对于大型数据结构,使用自定义序列化
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### 全量序列化API
|
||||
|
||||
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
|
||||
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
|
||||
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
|
||||
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
|
||||
|
||||
### 增量序列化API
|
||||
|
||||
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
|
||||
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
|
||||
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更(支持IncrementalSnapshot对象、JSON字符串或二进制Uint8Array)
|
||||
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
|
||||
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
|
||||
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
|
||||
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
|
||||
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
|
||||
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
|
||||
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
|
||||
|
||||
### 版本迁移API
|
||||
|
||||
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
|
||||
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
|
||||
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
|
||||
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
|
||||
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
@@ -1,828 +0,0 @@
|
||||
# 服务容器
|
||||
|
||||
服务容器(ServiceContainer)是 ECS Framework 的依赖注入容器,负责管理框架中所有服务的注册、解析和生命周期。通过服务容器,你可以实现松耦合的架构设计,提高代码的可测试性和可维护性。
|
||||
|
||||
## 概述
|
||||
|
||||
### 什么是服务容器
|
||||
|
||||
服务容器是一个轻量级的依赖注入(DI)容器,它提供了:
|
||||
|
||||
- **服务注册**: 将服务类型注册到容器中
|
||||
- **服务解析**: 从容器中获取服务实例
|
||||
- **生命周期管理**: 自动管理服务实例的创建和销毁
|
||||
- **依赖注入**: 自动解析服务之间的依赖关系
|
||||
|
||||
### 核心概念
|
||||
|
||||
#### 服务(Service)
|
||||
|
||||
服务是实现了 `IService` 接口的类,必须提供 `dispose()` 方法用于资源清理:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class MyService implements IService {
|
||||
constructor() {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 服务标识符(ServiceIdentifier)
|
||||
|
||||
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
||||
|
||||
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
||||
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
||||
|
||||
```typescript
|
||||
// 方式1: 使用类作为标识符
|
||||
Core.services.registerSingleton(DataService);
|
||||
const data = Core.services.resolve(DataService);
|
||||
|
||||
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
||||
const IFileSystem = Symbol.for('IFileSystem');
|
||||
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||
```
|
||||
|
||||
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
||||
|
||||
#### 生命周期
|
||||
|
||||
服务容器支持两种生命周期:
|
||||
|
||||
- **Singleton(单例)**: 整个应用程序生命周期内只有一个实例,所有解析请求返回同一个实例
|
||||
- **Transient(瞬时)**: 每次解析都创建新的实例
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 访问服务容器
|
||||
|
||||
ECS Framework 提供了三级服务容器:
|
||||
|
||||
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
||||
|
||||
#### Core 级别服务容器
|
||||
|
||||
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 访问全局服务容器
|
||||
const container = Core.services;
|
||||
```
|
||||
|
||||
#### World 级别服务容器
|
||||
|
||||
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
||||
|
||||
```typescript
|
||||
import { World } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建 World
|
||||
const world = new World({ name: 'GameWorld' });
|
||||
|
||||
// 访问 World 级别的服务容器
|
||||
const worldContainer = world.services;
|
||||
|
||||
// 注册 World 级别的服务
|
||||
world.services.registerSingleton(RoomManager);
|
||||
```
|
||||
|
||||
#### Scene 级别服务容器
|
||||
|
||||
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
||||
|
||||
```typescript
|
||||
// 访问 Scene 级别的服务容器
|
||||
const sceneContainer = scene.services;
|
||||
|
||||
// 注册 Scene 级别的服务
|
||||
scene.services.registerSingleton(PhysicsSystem);
|
||||
```
|
||||
|
||||
#### 服务容器层级
|
||||
|
||||
```
|
||||
Core.services (应用程序全局)
|
||||
└─ World.services (World 级别)
|
||||
└─ Scene.services (Scene 级别)
|
||||
```
|
||||
|
||||
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
||||
|
||||
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
||||
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
||||
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
||||
|
||||
### 注册服务
|
||||
|
||||
#### 注册单例服务
|
||||
|
||||
单例服务在首次解析时创建,之后所有解析请求都返回同一个实例:
|
||||
|
||||
```typescript
|
||||
class DataService implements IService {
|
||||
private data: Map<string, any> = new Map();
|
||||
|
||||
getData(key: string) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
setData(key: string, value: any) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册单例服务
|
||||
Core.services.registerSingleton(DataService);
|
||||
```
|
||||
|
||||
#### 注册瞬时服务
|
||||
|
||||
瞬时服务每次解析都创建新实例,适用于无状态或短生命周期的服务:
|
||||
|
||||
```typescript
|
||||
class CommandService implements IService {
|
||||
execute(command: string) {
|
||||
console.log(`Executing: ${command}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 注册瞬时服务
|
||||
Core.services.registerTransient(CommandService);
|
||||
```
|
||||
|
||||
#### 注册服务实例
|
||||
|
||||
直接注册已创建的实例,自动视为单例:
|
||||
|
||||
```typescript
|
||||
const config = new ConfigService();
|
||||
config.load('./config.json');
|
||||
|
||||
// 注册实例
|
||||
Core.services.registerInstance(ConfigService, config);
|
||||
```
|
||||
|
||||
#### 使用工厂函数注册
|
||||
|
||||
工厂函数允许你在创建服务时执行自定义逻辑:
|
||||
|
||||
```typescript
|
||||
Core.services.registerSingleton(LoggerService, (container) => {
|
||||
const logger = new LoggerService();
|
||||
logger.setLevel('debug');
|
||||
return logger;
|
||||
});
|
||||
```
|
||||
|
||||
### 解析服务
|
||||
|
||||
#### resolve 方法
|
||||
|
||||
解析服务实例,如果服务未注册会抛出异常:
|
||||
|
||||
```typescript
|
||||
// 解析服务
|
||||
const dataService = Core.services.resolve(DataService);
|
||||
dataService.setData('player', { name: 'Alice', score: 100 });
|
||||
|
||||
// 单例服务,多次解析返回同一个实例
|
||||
const same = Core.services.resolve(DataService);
|
||||
console.log(same === dataService); // true
|
||||
```
|
||||
|
||||
#### tryResolve 方法
|
||||
|
||||
尝试解析服务,如果未注册返回 null 而不抛出异常:
|
||||
|
||||
```typescript
|
||||
const optional = Core.services.tryResolve(OptionalService);
|
||||
if (optional) {
|
||||
optional.doSomething();
|
||||
}
|
||||
```
|
||||
|
||||
#### isRegistered 方法
|
||||
|
||||
检查服务是否已注册:
|
||||
|
||||
```typescript
|
||||
if (Core.services.isRegistered(DataService)) {
|
||||
const service = Core.services.resolve(DataService);
|
||||
}
|
||||
```
|
||||
|
||||
## 内置服务
|
||||
|
||||
Core 在初始化时自动注册了以下内置服务:
|
||||
|
||||
### TimerManager
|
||||
|
||||
定时器管理器,负责管理所有游戏定时器:
|
||||
|
||||
```typescript
|
||||
const timerManager = Core.services.resolve(TimerManager);
|
||||
|
||||
// 创建定时器
|
||||
timerManager.schedule(1.0, false, null, (timer) => {
|
||||
console.log('1秒后执行');
|
||||
});
|
||||
```
|
||||
|
||||
### PerformanceMonitor
|
||||
|
||||
性能监控器,监控游戏性能并提供优化建议:
|
||||
|
||||
```typescript
|
||||
const monitor = Core.services.resolve(PerformanceMonitor);
|
||||
|
||||
// 启用性能监控
|
||||
monitor.enable();
|
||||
|
||||
// 获取性能数据
|
||||
const fps = monitor.getFPS();
|
||||
```
|
||||
|
||||
### SceneManager
|
||||
|
||||
场景管理器,管理单场景应用的场景生命周期:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// 设置当前场景
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// 获取当前场景
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// 延迟切换场景
|
||||
sceneManager.loadScene(new MenuScene());
|
||||
|
||||
// 更新场景
|
||||
sceneManager.update();
|
||||
```
|
||||
|
||||
### WorldManager
|
||||
|
||||
世界管理器,管理多个独立的 World 实例(高级用例):
|
||||
|
||||
```typescript
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 创建独立的游戏世界
|
||||
const gameWorld = worldManager.createWorld('game_room_001', {
|
||||
name: 'GameRoom',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 在World中创建场景
|
||||
const scene = gameWorld.createScene('battle', new BattleScene());
|
||||
gameWorld.setSceneActive('battle', true);
|
||||
|
||||
// 更新所有World
|
||||
worldManager.updateAll();
|
||||
```
|
||||
|
||||
**适用场景**:
|
||||
- SceneManager: 适用于 95% 的游戏(单人游戏、简单多人游戏)
|
||||
- WorldManager: 适用于 MMO 服务器、游戏房间系统等需要完全隔离的多世界应用
|
||||
|
||||
### PoolManager
|
||||
|
||||
对象池管理器,管理所有对象池:
|
||||
|
||||
```typescript
|
||||
const poolManager = Core.services.resolve(PoolManager);
|
||||
|
||||
// 创建对象池
|
||||
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
|
||||
```
|
||||
|
||||
### PluginManager
|
||||
|
||||
插件管理器,管理插件的安装和卸载:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// 获取所有已安装的插件
|
||||
const plugins = pluginManager.getAllPlugins();
|
||||
```
|
||||
|
||||
## 依赖注入
|
||||
|
||||
ECS Framework 提供了装饰器来简化依赖注入。
|
||||
|
||||
### @Injectable 装饰器
|
||||
|
||||
标记类为可注入的服务:
|
||||
|
||||
```typescript
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class GameService implements IService {
|
||||
constructor() {
|
||||
console.log('GameService created');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('GameService disposed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @InjectProperty 装饰器
|
||||
|
||||
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
||||
|
||||
```typescript
|
||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
@InjectProperty(DataService)
|
||||
private data!: DataService;
|
||||
|
||||
@InjectProperty(GameService)
|
||||
private game!: GameService;
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 EntitySystem 中使用属性注入:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class CombatSystem extends EntitySystem {
|
||||
@InjectProperty(TimeService)
|
||||
private timeService!: TimeService;
|
||||
|
||||
@InjectProperty(AudioService)
|
||||
private audio!: AudioService;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Attack));
|
||||
}
|
||||
|
||||
onInitialize(): void {
|
||||
// 此时属性已注入完成,可以安全使用
|
||||
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||
}
|
||||
|
||||
processEntity(entity: Entity): void {
|
||||
// 使用注入的服务
|
||||
this.audio.playSound('attack');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
||||
|
||||
### 注册可注入服务
|
||||
|
||||
使用 `registerInjectable` 自动处理依赖注入:
|
||||
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// 注册服务(会自动解析 @InjectProperty 依赖)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// 解析时会自动注入属性依赖
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
### @Updatable 装饰器
|
||||
|
||||
标记服务为可更新的,使其在每帧自动被调用:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@Updatable() // 默认优先级为0
|
||||
class PhysicsService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧更新物理模拟
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
// 指定更新优先级(数值越小越先执行)
|
||||
@Injectable()
|
||||
@Updatable(10)
|
||||
class RenderService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// 每帧渲染
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 `@Updatable` 装饰器的服务会被 Core 自动调用,无需手动管理:
|
||||
|
||||
```typescript
|
||||
// Core.update() 会自动调用所有@Updatable服务的update方法
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 自动更新所有可更新服务
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义服务
|
||||
|
||||
### 创建自定义服务
|
||||
|
||||
实现 `IService` 接口并注册到容器:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class AudioService implements IService {
|
||||
private sounds: Map<string, HTMLAudioElement> = new Map();
|
||||
|
||||
play(soundId: string) {
|
||||
const sound = this.sounds.get(soundId);
|
||||
if (sound) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
|
||||
load(soundId: string, url: string) {
|
||||
const audio = new Audio(url);
|
||||
this.sounds.set(soundId, audio);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 停止所有音效并清理
|
||||
for (const sound of this.sounds.values()) {
|
||||
sound.pause();
|
||||
sound.src = '';
|
||||
}
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义服务
|
||||
Core.services.registerSingleton(AudioService);
|
||||
|
||||
// 使用服务
|
||||
const audio = Core.services.resolve(AudioService);
|
||||
audio.load('jump', '/sounds/jump.mp3');
|
||||
audio.play('jump');
|
||||
```
|
||||
|
||||
### 服务间依赖
|
||||
|
||||
服务可以依赖其他服务:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class ConfigService implements IService {
|
||||
private config: any = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
constructor(
|
||||
@Inject(ConfigService) private config: ConfigService
|
||||
) {
|
||||
// 使用配置服务
|
||||
const apiUrl = this.config.get('apiUrl');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// 清理网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 注册服务(按依赖顺序)
|
||||
registerInjectable(Core.services, ConfigService);
|
||||
registerInjectable(Core.services, NetworkService);
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 接口与 Symbol 标识符模式
|
||||
|
||||
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
||||
|
||||
#### 为什么使用 Symbol.for
|
||||
|
||||
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
||||
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
||||
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
||||
|
||||
#### 定义接口和标识符
|
||||
|
||||
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
||||
|
||||
```typescript
|
||||
// IAudioService.ts - 定义接口和标识符
|
||||
export interface IAudioService {
|
||||
dispose(): void;
|
||||
playSound(id: string): void;
|
||||
playMusic(id: string, loop?: boolean): void;
|
||||
stopMusic(): void;
|
||||
setVolume(volume: number): void;
|
||||
preload(id: string, url: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
||||
export const IAudioService = Symbol.for('IAudioService');
|
||||
```
|
||||
|
||||
#### 实现接口
|
||||
|
||||
```typescript
|
||||
// WebAudioService.ts - Web 平台实现
|
||||
import { IAudioService } from './IAudioService';
|
||||
|
||||
export class WebAudioService implements IAudioService {
|
||||
private audioContext: AudioContext;
|
||||
private sounds: Map<string, AudioBuffer> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
playSound(id: string): void {
|
||||
const buffer = this.sounds.get(id);
|
||||
if (buffer) {
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
||||
this.sounds.set(id, audioBuffer);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
this.audioContext.close();
|
||||
this.sounds.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// WechatAudioService.ts - 微信小游戏平台实现
|
||||
export class WechatAudioService implements IAudioService {
|
||||
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
||||
|
||||
playSound(id: string): void {
|
||||
const ctx = this.innerAudioContexts.get(id);
|
||||
if (ctx) {
|
||||
ctx.play();
|
||||
}
|
||||
}
|
||||
|
||||
async preload(id: string, url: string): Promise<void> {
|
||||
const ctx = wx.createInnerAudioContext();
|
||||
ctx.src = url;
|
||||
this.innerAudioContexts.set(id, ctx);
|
||||
}
|
||||
|
||||
// ... 其他方法实现
|
||||
|
||||
dispose(): void {
|
||||
for (const ctx of this.innerAudioContexts.values()) {
|
||||
ctx.destroy();
|
||||
}
|
||||
this.innerAudioContexts.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 注册和使用
|
||||
|
||||
```typescript
|
||||
import { IAudioService } from './IAudioService';
|
||||
import { WebAudioService } from './WebAudioService';
|
||||
import { WechatAudioService } from './WechatAudioService';
|
||||
|
||||
// 根据平台注册不同实现
|
||||
if (typeof wx !== 'undefined') {
|
||||
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||
} else {
|
||||
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||
}
|
||||
|
||||
// 业务代码中使用 - 不关心具体实现
|
||||
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||
await audio.preload('explosion', '/sounds/explosion.mp3');
|
||||
audio.playSound('explosion');
|
||||
```
|
||||
|
||||
#### 跨模块使用
|
||||
|
||||
```typescript
|
||||
// 在游戏系统中使用
|
||||
import { IAudioService } from '@mygame/core';
|
||||
|
||||
class CombatSystem extends EntitySystem {
|
||||
private audio: IAudioService;
|
||||
|
||||
initialize(): void {
|
||||
// 获取音频服务,不需要知道具体实现
|
||||
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
||||
}
|
||||
|
||||
onEntityDeath(entity: Entity): void {
|
||||
this.audio.playSound('death');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Symbol vs Symbol.for
|
||||
|
||||
```typescript
|
||||
// Symbol() - 每次创建唯一的 Symbol
|
||||
const sym1 = Symbol('test');
|
||||
const sym2 = Symbol('test');
|
||||
console.log(sym1 === sym2); // false - 不同的 Symbol
|
||||
|
||||
// Symbol.for() - 在全局注册表中共享
|
||||
const sym3 = Symbol.for('test');
|
||||
const sym4 = Symbol.for('test');
|
||||
console.log(sym3 === sym4); // true - 同一个 Symbol
|
||||
|
||||
// 跨包场景
|
||||
// package-a/index.ts
|
||||
export const IMyService = Symbol.for('IMyService');
|
||||
|
||||
// package-b/index.ts (不同的包)
|
||||
const IMyService = Symbol.for('IMyService');
|
||||
// 与 package-a 中的是同一个 Symbol!
|
||||
```
|
||||
|
||||
### 循环依赖检测
|
||||
|
||||
服务容器会自动检测循环依赖:
|
||||
|
||||
```typescript
|
||||
// A 依赖 B,B 依赖 A
|
||||
@Injectable()
|
||||
class ServiceA implements IService {
|
||||
constructor(@Inject(ServiceB) b: ServiceB) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ServiceB implements IService {
|
||||
constructor(@Inject(ServiceA) a: ServiceA) {}
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// 解析时会抛出错误: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
||||
```
|
||||
|
||||
### 获取所有服务
|
||||
|
||||
```typescript
|
||||
// 获取所有已注册的服务类型
|
||||
const types = Core.services.getRegisteredServices();
|
||||
|
||||
// 获取所有已实例化的服务实例
|
||||
const instances = Core.services.getAll();
|
||||
```
|
||||
|
||||
### 服务清理
|
||||
|
||||
```typescript
|
||||
// 注销单个服务
|
||||
Core.services.unregister(MyService);
|
||||
|
||||
// 清空所有服务(会调用每个服务的dispose方法)
|
||||
Core.services.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 服务命名
|
||||
|
||||
服务类名应该以 `Service` 或 `Manager` 结尾,清晰表达其职责:
|
||||
|
||||
```typescript
|
||||
class PlayerService implements IService {}
|
||||
class AudioManager implements IService {}
|
||||
class NetworkService implements IService {}
|
||||
```
|
||||
|
||||
### 资源清理
|
||||
|
||||
始终在 `dispose()` 方法中清理资源:
|
||||
|
||||
```typescript
|
||||
class ResourceService implements IService {
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
// 释放所有资源
|
||||
for (const resource of this.resources.values()) {
|
||||
resource.release();
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 避免过度使用
|
||||
|
||||
不要把所有类都注册为服务,服务应该是:
|
||||
|
||||
- 全局单例或需要共享状态
|
||||
- 需要在多处使用
|
||||
- 生命周期需要管理
|
||||
- 需要依赖注入
|
||||
|
||||
对于简单的工具类或数据类,直接创建实例即可。
|
||||
|
||||
### 依赖方向
|
||||
|
||||
保持清晰的依赖方向,避免循环依赖:
|
||||
|
||||
```
|
||||
高层服务 -> 中层服务 -> 底层服务
|
||||
GameLogic -> DataService -> ConfigService
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 服务未注册错误
|
||||
|
||||
**问题**: `Error: Service MyService is not registered`
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 确保服务已注册
|
||||
Core.services.registerSingleton(MyService);
|
||||
|
||||
// 或者使用tryResolve
|
||||
const service = Core.services.tryResolve(MyService);
|
||||
if (!service) {
|
||||
console.log('Service not found');
|
||||
}
|
||||
```
|
||||
|
||||
### 循环依赖错误
|
||||
|
||||
**问题**: `Circular dependency detected`
|
||||
|
||||
**解决**: 重新设计服务依赖关系,引入中间服务或使用事件系统解耦。
|
||||
|
||||
### 何时使用单例 vs 瞬时
|
||||
|
||||
- **单例**: 管理器类、配置、缓存、状态管理
|
||||
- **瞬时**: 命令对象、请求处理器、临时任务
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [插件系统](./plugin-system.md) - 使用服务容器注册插件服务
|
||||
- [快速开始](./getting-started.md) - Core 初始化和基础使用
|
||||
- [系统架构](./system.md) - 在系统中使用服务
|
||||
1161
docs/guide/system.md
1161
docs/guide/system.md
File diff suppressed because it is too large
Load Diff
@@ -1,761 +0,0 @@
|
||||
# WorldManager
|
||||
|
||||
WorldManager 是 ECS Framework 提供的高级世界管理器,用于管理多个完全隔离的游戏世界(World)。每个 World 都是独立的 ECS 环境,可以包含多个场景。
|
||||
|
||||
## 适用场景
|
||||
|
||||
WorldManager 适合以下高级场景:
|
||||
- MMO 游戏服务器的多房间管理
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 服务器端的多游戏实例
|
||||
- 需要完全隔离的多个游戏环境
|
||||
- 需要同时运行多个独立世界的应用
|
||||
|
||||
## 特点
|
||||
|
||||
- 多 World 管理,每个 World 完全独立
|
||||
- 每个 World 可以包含多个 Scene
|
||||
- 支持 World 的激活/停用
|
||||
- 自动清理空 World
|
||||
- World 级别的全局系统
|
||||
- 批量操作和查询
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 初始化
|
||||
|
||||
WorldManager 是 Core 的内置服务,通过服务容器获取:
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化 Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// 从服务容器获取 WorldManager(Core 已自动创建并注册)
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
```
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 创建游戏房间 World
|
||||
const room1 = worldManager.createWorld('room_001', {
|
||||
name: 'GameRoom_001',
|
||||
maxScenes: 5,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 创建更多房间
|
||||
const room2 = worldManager.createWorld('room_002', {
|
||||
name: 'GameRoom_002',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
worldManager.setWorldActive('room_002', true);
|
||||
```
|
||||
|
||||
### 游戏循环
|
||||
|
||||
在游戏循环中更新所有活跃的 World:
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // 更新全局服务
|
||||
worldManager.updateAll(); // 更新所有活跃的 World
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
let lastTime = 0;
|
||||
setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
gameLoop(deltaTime);
|
||||
}, 16); // 60 FPS
|
||||
```
|
||||
|
||||
## World 管理
|
||||
|
||||
### 创建 World
|
||||
|
||||
```typescript
|
||||
// 基本创建
|
||||
const world = worldManager.createWorld('worldId');
|
||||
|
||||
// 带配置创建
|
||||
const world = worldManager.createWorld('worldId', {
|
||||
name: 'MyWorld',
|
||||
maxScenes: 10,
|
||||
autoCleanup: true,
|
||||
debug: true
|
||||
});
|
||||
```
|
||||
|
||||
**配置选项(IWorldConfig)**:
|
||||
- `name?: string` - World 名称
|
||||
- `maxScenes?: number` - 最大场景数量限制(默认 10)
|
||||
- `autoCleanup?: boolean` - 是否自动清理空场景(默认 true)
|
||||
- `debug?: boolean` - 是否启用调试模式(默认 false)
|
||||
|
||||
### 获取 World
|
||||
|
||||
```typescript
|
||||
// 通过 ID 获取
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (world) {
|
||||
console.log(`World: ${world.name}`);
|
||||
}
|
||||
|
||||
// 获取所有 World
|
||||
const allWorlds = worldManager.getAllWorlds();
|
||||
console.log(`共有 ${allWorlds.length} 个 World`);
|
||||
|
||||
// 获取所有 World ID
|
||||
const worldIds = worldManager.getWorldIds();
|
||||
console.log('World 列表:', worldIds);
|
||||
|
||||
// 通过名称查找
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
### 激活和停用 World
|
||||
|
||||
```typescript
|
||||
// 激活 World(开始运行和更新)
|
||||
worldManager.setWorldActive('room_001', true);
|
||||
|
||||
// 停用 World(停止更新但保留数据)
|
||||
worldManager.setWorldActive('room_001', false);
|
||||
|
||||
// 检查 World 是否激活
|
||||
if (worldManager.isWorldActive('room_001')) {
|
||||
console.log('房间正在运行');
|
||||
}
|
||||
|
||||
// 获取所有活跃的 World
|
||||
const activeWorlds = worldManager.getActiveWorlds();
|
||||
console.log(`当前有 ${activeWorlds.length} 个活跃 World`);
|
||||
```
|
||||
|
||||
### 移除 World
|
||||
|
||||
```typescript
|
||||
// 移除 World(会自动停用并销毁)
|
||||
const removed = worldManager.removeWorld('room_001');
|
||||
if (removed) {
|
||||
console.log('World 已移除');
|
||||
}
|
||||
```
|
||||
|
||||
## World 中的场景管理
|
||||
|
||||
每个 World 可以包含多个 Scene 并独立管理它们的生命周期。
|
||||
|
||||
### 创建场景
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 创建场景
|
||||
const mainScene = world.createScene('main', new MainScene());
|
||||
const uiScene = world.createScene('ui', new UIScene());
|
||||
const hudScene = world.createScene('hud', new HUDScene());
|
||||
|
||||
// 激活场景
|
||||
world.setSceneActive('main', true);
|
||||
world.setSceneActive('ui', true);
|
||||
world.setSceneActive('hud', false);
|
||||
```
|
||||
|
||||
### 查询场景
|
||||
|
||||
```typescript
|
||||
// 获取特定场景
|
||||
const mainScene = world.getScene<MainScene>('main');
|
||||
if (mainScene) {
|
||||
console.log(`场景名称: ${mainScene.name}`);
|
||||
}
|
||||
|
||||
// 获取所有场景
|
||||
const allScenes = world.getAllScenes();
|
||||
console.log(`World 中共有 ${allScenes.length} 个场景`);
|
||||
|
||||
// 获取所有场景 ID
|
||||
const sceneIds = world.getSceneIds();
|
||||
console.log('场景列表:', sceneIds);
|
||||
|
||||
// 获取活跃场景数量
|
||||
const activeCount = world.getActiveSceneCount();
|
||||
console.log(`当前有 ${activeCount} 个活跃场景`);
|
||||
|
||||
// 检查场景是否激活
|
||||
if (world.isSceneActive('main')) {
|
||||
console.log('主场景正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 场景切换
|
||||
|
||||
World 支持多场景同时运行,也支持场景切换:
|
||||
|
||||
```typescript
|
||||
class GameWorld {
|
||||
private world: World;
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.world = worldManager.createWorld('game', {
|
||||
name: 'GameWorld',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// 创建所有场景
|
||||
this.world.createScene('menu', new MenuScene());
|
||||
this.world.createScene('game', new GameScene());
|
||||
this.world.createScene('pause', new PauseScene());
|
||||
this.world.createScene('gameover', new GameOverScene());
|
||||
|
||||
// 激活 World
|
||||
worldManager.setWorldActive('game', true);
|
||||
}
|
||||
|
||||
public showMenu(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('menu', true);
|
||||
}
|
||||
|
||||
public startGame(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 游戏场景继续存在但停止更新
|
||||
this.world.setSceneActive('game', false);
|
||||
// 显示暂停界面
|
||||
this.world.setSceneActive('pause', true);
|
||||
}
|
||||
|
||||
public resumeGame(): void {
|
||||
this.world.setSceneActive('pause', false);
|
||||
this.world.setSceneActive('game', true);
|
||||
}
|
||||
|
||||
public showGameOver(): void {
|
||||
this.deactivateAllScenes();
|
||||
this.world.setSceneActive('gameover', true);
|
||||
}
|
||||
|
||||
private deactivateAllScenes(): void {
|
||||
const sceneIds = this.world.getSceneIds();
|
||||
sceneIds.forEach(id => this.world.setSceneActive(id, false));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 移除场景
|
||||
|
||||
```typescript
|
||||
// 移除不再需要的场景
|
||||
const removed = world.removeScene('oldScene');
|
||||
if (removed) {
|
||||
console.log('场景已移除');
|
||||
}
|
||||
|
||||
// 场景会自动调用 end() 方法进行清理
|
||||
```
|
||||
|
||||
## 全局系统
|
||||
|
||||
World 支持全局系统,这些系统在 World 级别运行,不依赖特定 Scene。
|
||||
|
||||
### 定义全局系统
|
||||
|
||||
```typescript
|
||||
import { IGlobalSystem } from '@esengine/ecs-framework';
|
||||
|
||||
// 网络系统(World 级别)
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
readonly name = 'NetworkSystem';
|
||||
|
||||
private connectionId: string;
|
||||
|
||||
constructor(connectionId: string) {
|
||||
this.connectionId = connectionId;
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
console.log(`网络系统初始化: ${this.connectionId}`);
|
||||
// 建立网络连接
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 处理网络消息,不依赖任何 Scene
|
||||
// 接收和发送网络包
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log(`网络系统销毁: ${this.connectionId}`);
|
||||
// 关闭网络连接
|
||||
}
|
||||
}
|
||||
|
||||
// 物理系统(World 级别)
|
||||
class PhysicsSystem implements IGlobalSystem {
|
||||
readonly name = 'PhysicsSystem';
|
||||
|
||||
initialize(): void {
|
||||
console.log('物理系统初始化');
|
||||
}
|
||||
|
||||
update(deltaTime?: number): void {
|
||||
// 物理模拟,作用于 World 中所有场景
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
console.log('物理系统销毁');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用全局系统
|
||||
|
||||
```typescript
|
||||
const world = worldManager.getWorld('room_001');
|
||||
if (!world) return;
|
||||
|
||||
// 添加全局系统
|
||||
const networkSystem = world.addGlobalSystem(new NetworkSystem('conn_001'));
|
||||
const physicsSystem = world.addGlobalSystem(new PhysicsSystem());
|
||||
|
||||
// 获取全局系统
|
||||
const network = world.getGlobalSystem(NetworkSystem);
|
||||
if (network) {
|
||||
console.log('找到网络系统');
|
||||
}
|
||||
|
||||
// 移除全局系统
|
||||
world.removeGlobalSystem(networkSystem);
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
### 更新所有 World
|
||||
|
||||
```typescript
|
||||
// 更新所有活跃的 World(应该在游戏循环中调用)
|
||||
worldManager.updateAll();
|
||||
|
||||
// 这会自动更新每个 World 的:
|
||||
// 1. 全局系统
|
||||
// 2. 所有活跃场景
|
||||
```
|
||||
|
||||
### 启动和停止
|
||||
|
||||
```typescript
|
||||
// 启动所有 World
|
||||
worldManager.startAll();
|
||||
|
||||
// 停止所有 World
|
||||
worldManager.stopAll();
|
||||
|
||||
// 检查是否正在运行
|
||||
if (worldManager.isRunning) {
|
||||
console.log('WorldManager 正在运行');
|
||||
}
|
||||
```
|
||||
|
||||
### 查找 World
|
||||
|
||||
```typescript
|
||||
// 使用条件查找
|
||||
const emptyWorlds = worldManager.findWorlds(world => {
|
||||
return world.sceneCount === 0;
|
||||
});
|
||||
|
||||
// 查找活跃的 World
|
||||
const activeWorlds = worldManager.findWorlds(world => {
|
||||
return world.isActive;
|
||||
});
|
||||
|
||||
// 查找特定名称的 World
|
||||
const world = worldManager.findWorldByName('GameRoom_001');
|
||||
```
|
||||
|
||||
## 统计和监控
|
||||
|
||||
### 获取统计信息
|
||||
|
||||
```typescript
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
console.log(`总 World 数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃 World 数: ${stats.activeWorlds}`);
|
||||
console.log(`总场景数: ${stats.totalScenes}`);
|
||||
console.log(`总实体数: ${stats.totalEntities}`);
|
||||
console.log(`总系统数: ${stats.totalSystems}`);
|
||||
|
||||
// 查看每个 World 的详细信息
|
||||
stats.worlds.forEach(worldInfo => {
|
||||
console.log(`World: ${worldInfo.name}`);
|
||||
console.log(` 场景数: ${worldInfo.sceneCount}`);
|
||||
console.log(` 是否活跃: ${worldInfo.isActive}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 获取详细状态
|
||||
|
||||
```typescript
|
||||
const status = worldManager.getDetailedStatus();
|
||||
|
||||
// 包含所有 World 的详细状态
|
||||
status.worlds.forEach(worldStatus => {
|
||||
console.log(`World ID: ${worldStatus.id}`);
|
||||
console.log(`状态:`, worldStatus.status);
|
||||
});
|
||||
```
|
||||
|
||||
## 自动清理
|
||||
|
||||
WorldManager 支持自动清理空的 World。
|
||||
|
||||
### 配置清理
|
||||
|
||||
```typescript
|
||||
// 创建带清理配置的 WorldManager
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// WorldManager 的配置在 Core 中设置:
|
||||
// {
|
||||
// maxWorlds: 50,
|
||||
// autoCleanup: true,
|
||||
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
|
||||
// }
|
||||
```
|
||||
|
||||
### 手动清理
|
||||
|
||||
```typescript
|
||||
// 手动触发清理
|
||||
const cleanedCount = worldManager.cleanup();
|
||||
console.log(`清理了 ${cleanedCount} 个 World`);
|
||||
```
|
||||
|
||||
**清理条件**:
|
||||
- World 未激活
|
||||
- 没有 Scene 或所有 Scene 都是空的
|
||||
- 创建时间超过 10 分钟
|
||||
|
||||
## API 参考
|
||||
|
||||
### WorldManager API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createWorld(worldId, config?)` | 创建新 World |
|
||||
| `removeWorld(worldId)` | 移除 World |
|
||||
| `getWorld(worldId)` | 获取 World |
|
||||
| `getAllWorlds()` | 获取所有 World |
|
||||
| `getWorldIds()` | 获取所有 World ID |
|
||||
| `setWorldActive(worldId, active)` | 设置 World 激活状态 |
|
||||
| `isWorldActive(worldId)` | 检查 World 是否激活 |
|
||||
| `getActiveWorlds()` | 获取所有活跃的 World |
|
||||
| `updateAll()` | 更新所有活跃 World |
|
||||
| `startAll()` | 启动所有 World |
|
||||
| `stopAll()` | 停止所有 World |
|
||||
| `findWorlds(predicate)` | 查找满足条件的 World |
|
||||
| `findWorldByName(name)` | 根据名称查找 World |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
| `getDetailedStatus()` | 获取详细状态信息 |
|
||||
| `cleanup()` | 清理空 World |
|
||||
| `destroy()` | 销毁 WorldManager |
|
||||
|
||||
### World API
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createScene(sceneId, sceneInstance?)` | 创建并添加 Scene |
|
||||
| `removeScene(sceneId)` | 移除 Scene |
|
||||
| `getScene(sceneId)` | 获取 Scene |
|
||||
| `getAllScenes()` | 获取所有 Scene |
|
||||
| `getSceneIds()` | 获取所有 Scene ID |
|
||||
| `setSceneActive(sceneId, active)` | 设置 Scene 激活状态 |
|
||||
| `isSceneActive(sceneId)` | 检查 Scene 是否激活 |
|
||||
| `getActiveSceneCount()` | 获取活跃 Scene 数量 |
|
||||
| `addGlobalSystem(system)` | 添加全局系统 |
|
||||
| `removeGlobalSystem(system)` | 移除全局系统 |
|
||||
| `getGlobalSystem(type)` | 获取全局系统 |
|
||||
| `start()` | 启动 World |
|
||||
| `stop()` | 停止 World |
|
||||
| `updateGlobalSystems()` | 更新全局系统 |
|
||||
| `updateScenes()` | 更新所有激活 Scene |
|
||||
| `destroy()` | 销毁 World |
|
||||
| `getStatus()` | 获取 World 状态 |
|
||||
| `getStats()` | 获取统计信息 |
|
||||
|
||||
### 属性
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| `worldCount` | World 总数 |
|
||||
| `activeWorldCount` | 活跃 World 数量 |
|
||||
| `isRunning` | 是否正在运行 |
|
||||
| `config` | 配置信息 |
|
||||
|
||||
## 完整示例
|
||||
|
||||
### MMO 游戏房间系统
|
||||
|
||||
```typescript
|
||||
import { Core, WorldManager, Scene, World } from '@esengine/ecs-framework';
|
||||
|
||||
// 初始化
|
||||
Core.create({ debug: true });
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// 房间管理器
|
||||
class RoomManager {
|
||||
private worldManager: WorldManager;
|
||||
private rooms: Map<string, World> = new Map();
|
||||
|
||||
constructor(worldManager: WorldManager) {
|
||||
this.worldManager = worldManager;
|
||||
}
|
||||
|
||||
// 创建游戏房间
|
||||
public createRoom(roomId: string, maxPlayers: number): World {
|
||||
const world = this.worldManager.createWorld(roomId, {
|
||||
name: `Room_${roomId}`,
|
||||
maxScenes: 3,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// 创建房间场景
|
||||
world.createScene('lobby', new LobbyScene());
|
||||
world.createScene('game', new GameScene());
|
||||
world.createScene('result', new ResultScene());
|
||||
|
||||
// 添加房间级别的系统
|
||||
world.addGlobalSystem(new NetworkSystem(roomId));
|
||||
world.addGlobalSystem(new RoomLogicSystem(maxPlayers));
|
||||
|
||||
// 激活 World 和初始场景
|
||||
this.worldManager.setWorldActive(roomId, true);
|
||||
world.setSceneActive('lobby', true);
|
||||
|
||||
this.rooms.set(roomId, world);
|
||||
console.log(`房间 ${roomId} 已创建`);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
// 玩家加入房间
|
||||
public joinRoom(roomId: string, playerId: string): boolean {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) {
|
||||
console.log(`房间 ${roomId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 在大厅场景中创建玩家实体
|
||||
const lobbyScene = world.getScene('lobby');
|
||||
if (lobbyScene) {
|
||||
const player = lobbyScene.createEntity(`Player_${playerId}`);
|
||||
// 添加玩家组件...
|
||||
console.log(`玩家 ${playerId} 加入房间 ${roomId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 开始游戏
|
||||
public startGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到游戏场景
|
||||
world.setSceneActive('lobby', false);
|
||||
world.setSceneActive('game', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏开始`);
|
||||
}
|
||||
|
||||
// 结束游戏
|
||||
public endGame(roomId: string): void {
|
||||
const world = this.rooms.get(roomId);
|
||||
if (!world) return;
|
||||
|
||||
// 切换到结果场景
|
||||
world.setSceneActive('game', false);
|
||||
world.setSceneActive('result', true);
|
||||
|
||||
console.log(`房间 ${roomId} 游戏结束`);
|
||||
}
|
||||
|
||||
// 关闭房间
|
||||
public closeRoom(roomId: string): void {
|
||||
this.worldManager.removeWorld(roomId);
|
||||
this.rooms.delete(roomId);
|
||||
console.log(`房间 ${roomId} 已关闭`);
|
||||
}
|
||||
|
||||
// 获取房间列表
|
||||
public getRoomList(): string[] {
|
||||
return Array.from(this.rooms.keys());
|
||||
}
|
||||
|
||||
// 获取房间统计
|
||||
public getRoomStats(roomId: string) {
|
||||
const world = this.rooms.get(roomId);
|
||||
return world?.getStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用房间管理器
|
||||
const roomManager = new RoomManager(worldManager);
|
||||
|
||||
// 创建多个游戏房间
|
||||
roomManager.createRoom('room_001', 4);
|
||||
roomManager.createRoom('room_002', 4);
|
||||
roomManager.createRoom('room_003', 2);
|
||||
|
||||
// 玩家加入
|
||||
roomManager.joinRoom('room_001', 'player_1');
|
||||
roomManager.joinRoom('room_001', 'player_2');
|
||||
|
||||
// 开始游戏
|
||||
roomManager.startGame('room_001');
|
||||
|
||||
// 游戏循环
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime);
|
||||
worldManager.updateAll(); // 更新所有房间
|
||||
}
|
||||
|
||||
// 定期清理空房间
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
console.log(`当前房间数: ${stats.totalWorlds}`);
|
||||
console.log(`活跃房间数: ${stats.activeWorlds}`);
|
||||
|
||||
worldManager.cleanup();
|
||||
}, 60000); // 每分钟清理一次
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理的 World 粒度
|
||||
|
||||
```typescript
|
||||
// 推荐:每个独立环境一个 World
|
||||
const room1 = worldManager.createWorld('room_1'); // 游戏房间1
|
||||
const room2 = worldManager.createWorld('room_2'); // 游戏房间2
|
||||
|
||||
// 不推荐:过度使用 World
|
||||
const world1 = worldManager.createWorld('ui'); // UI 不需要独立 World
|
||||
const world2 = worldManager.createWorld('menu'); // 菜单不需要独立 World
|
||||
```
|
||||
|
||||
### 2. 使用全局系统处理跨场景逻辑
|
||||
|
||||
```typescript
|
||||
// 推荐:World 级别的系统
|
||||
class NetworkSystem implements IGlobalSystem {
|
||||
update() {
|
||||
// 网络处理不依赖场景
|
||||
}
|
||||
}
|
||||
|
||||
// 不推荐:在每个场景中重复创建
|
||||
class GameScene extends Scene {
|
||||
initialize() {
|
||||
this.addSystem(new NetworkSystem()); // 不应该在场景级别
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 及时清理不用的 World
|
||||
|
||||
```typescript
|
||||
// 推荐:玩家离开时清理房间
|
||||
function onPlayerLeave(roomId: string) {
|
||||
const world = worldManager.getWorld(roomId);
|
||||
if (world && world.sceneCount === 0) {
|
||||
worldManager.removeWorld(roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// 或使用自动清理
|
||||
worldManager.cleanup();
|
||||
```
|
||||
|
||||
### 4. 监控资源使用
|
||||
|
||||
```typescript
|
||||
// 定期检查资源使用情况
|
||||
setInterval(() => {
|
||||
const stats = worldManager.getStats();
|
||||
|
||||
if (stats.totalWorlds > 100) {
|
||||
console.warn('World 数量过多,考虑清理');
|
||||
worldManager.cleanup();
|
||||
}
|
||||
|
||||
if (stats.totalEntities > 10000) {
|
||||
console.warn('实体数量过多,检查是否有泄漏');
|
||||
}
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
## 与 SceneManager 的对比
|
||||
|
||||
| 特性 | SceneManager | WorldManager |
|
||||
|------|--------------|--------------|
|
||||
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
|
||||
| 复杂度 | 简单 | 复杂 |
|
||||
| 场景数量 | 单场景(可切换) | 多 World,每个 World 多场景 |
|
||||
| 场景隔离 | 无(场景切换) | 完全隔离(每个 World 独立) |
|
||||
| 性能开销 | 最小 | 较高 |
|
||||
| 全局系统 | 无 | 支持(World 级别) |
|
||||
| 使用示例 | 单人游戏、移动游戏 | MMO 服务器、游戏房间系统 |
|
||||
|
||||
**何时使用 WorldManager**:
|
||||
- MMO 游戏服务器(每个房间一个 World)
|
||||
- 游戏大厅系统(每个游戏房间完全隔离)
|
||||
- 需要运行多个完全独立的游戏实例
|
||||
- 服务器端模拟多个游戏世界
|
||||
|
||||
**何时使用 SceneManager**:
|
||||
- 单人游戏
|
||||
- 简单的多人游戏
|
||||
- 移动游戏
|
||||
- 场景之间需要切换但不需要同时运行
|
||||
|
||||
## 架构层次
|
||||
|
||||
WorldManager 在 ECS Framework 中的位置:
|
||||
|
||||
```
|
||||
Core (全局服务)
|
||||
└── WorldManager (世界管理)
|
||||
├── World 1 (游戏房间1)
|
||||
│ ├── GlobalSystem (全局系统)
|
||||
│ ├── Scene 1 (场景1)
|
||||
│ │ ├── EntitySystem
|
||||
│ │ ├── Entity
|
||||
│ │ └── Component
|
||||
│ └── Scene 2 (场景2)
|
||||
├── World 2 (游戏房间2)
|
||||
│ ├── GlobalSystem
|
||||
│ └── Scene 1
|
||||
└── World 3 (游戏房间3)
|
||||
```
|
||||
|
||||
WorldManager 为需要多世界隔离的高级应用提供了强大的管理能力。如果你的应用不需要多世界隔离,建议使用更简单的 [SceneManager](./scene-manager.md)。
|
||||
317
docs/index.md
317
docs/index.md
@@ -1,317 +0,0 @@
|
||||
---
|
||||
layout: page
|
||||
title: ESEngine - 高性能 TypeScript ECS 框架
|
||||
---
|
||||
|
||||
<ParticleHero />
|
||||
|
||||
<section class="news-section">
|
||||
<div class="news-container">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">快速入口</h2>
|
||||
<a href="/guide/" class="news-more">查看文档</a>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
<a href="/guide/getting-started" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9zm6.82 6L12 12.72L5.18 9L12 5.28zM17 16l-5 2.72L7 16v-3.73L12 15l5-2.73z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">快速开始</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>5 分钟上手 ESEngine</h3>
|
||||
<p>从安装到创建第一个 ECS 应用,快速了解核心概念。</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/guide/behavior-tree/" class="news-card">
|
||||
<div class="news-card-image" style="background: linear-gradient(135deg, #1e3a5f 0%, #1e1e1e 100%);">
|
||||
<div class="news-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m3 20h-1v-7l-2-2l-2 2v7H9v-7.5l-2 2V22H6v-6l3-3l1-3.5c-.3.4-.6.7-1 1L6 9v1H4V8l5-3c.5-.3 1.1-.5 1.7-.5H11c.6 0 1.2.2 1.7.5l5 3v2h-2V9l-3 1.5c-.4-.3-.7-.6-1-1l1 3.5l3 3v6Z"/></svg>
|
||||
</div>
|
||||
<span class="news-badge">AI 系统</span>
|
||||
</div>
|
||||
<div class="news-card-content">
|
||||
<h3>行为树可视化编辑器</h3>
|
||||
<p>内置 AI 行为树系统,支持可视化编辑和实时调试。</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features-section">
|
||||
<div class="features-container">
|
||||
<h2 class="features-title">核心特性</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4fc1ff" d="M13 2.05v2.02c3.95.49 7 3.85 7 7.93c0 1.45-.39 2.79-1.06 3.95l1.59 1.09A9.94 9.94 0 0 0 22 12c0-5.18-3.95-9.45-9-9.95M12 19c-3.87 0-7-3.13-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95c0 5.52 4.47 10 9.99 10c3.31 0 6.24-1.61 8.06-4.09l-1.6-1.1A7.93 7.93 0 0 1 12 19"/><path fill="#4fc1ff" d="M12 6a6 6 0 0 0-6 6c0 3.31 2.69 6 6 6a6 6 0 0 0 0-12m0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4s4 1.79 4 4s-1.79 4-4 4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">高性能 ECS 架构</h3>
|
||||
<p class="feature-desc">基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。</p>
|
||||
<a href="/guide/entity" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#569cd6" d="M3 3h18v18H3zm16.525 13.707c0-.795-.272-1.425-.816-1.89c-.544-.465-1.404-.804-2.58-1.016l-1.704-.296c-.616-.104-1.052-.26-1.308-.468c-.256-.21-.384-.468-.384-.776c0-.392.168-.7.504-.924c.336-.224.8-.336 1.392-.336c.56 0 1.008.124 1.344.372c.336.248.536.584.6 1.008h2.016c-.08-.96-.464-1.716-1.152-2.268c-.688-.552-1.6-.828-2.736-.828c-1.2 0-2.148.3-2.844.9c-.696.6-1.044 1.38-1.044 2.34c0 .76.252 1.368.756 1.824c.504.456 1.308.792 2.412.996l1.704.312c.624.12 1.068.28 1.332.48c.264.2.396.46.396.78c0 .424-.192.756-.576.996c-.384.24-.9.36-1.548.36c-.672 0-1.2-.14-1.584-.42c-.384-.28-.608-.668-.672-1.164H8.868c.048 1.016.46 1.808 1.236 2.376c.776.568 1.796.852 3.06.852c1.24 0 2.22-.292 2.94-.876c.72-.584 1.08-1.364 1.08-2.34z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">完整类型支持</h3>
|
||||
<p class="feature-desc">100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。</p>
|
||||
<a href="/guide/component" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#4ec9b0" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10s10-4.5 10-10S17.5 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m-5-8l4-4v3h4v2h-4v3z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">可视化行为树</h3>
|
||||
<p class="feature-desc">内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。</p>
|
||||
<a href="/guide/behavior-tree/" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#c586c0" d="M4 6h18V4H4c-1.1 0-2 .9-2 2v11H0v3h14v-3H4zm19 2h-6c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1V9c0-.55-.45-1-1-1m-1 9h-4v-7h4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">多平台支持</h3>
|
||||
<p class="feature-desc">支持浏览器、Node.js、微信小游戏等多平台,可与主流游戏引擎无缝集成。</p>
|
||||
<a href="/guide/platform-adapter" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#dcdcaa" d="M4 3h6v2H4v14h6v2H4c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2m9 0h6c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2h-6v-2h6V5h-6zm-1 7h4v2h-4z"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">模块化设计</h3>
|
||||
<p class="feature-desc">核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。</p>
|
||||
<a href="/guide/plugin-system" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#9cdcfe" d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4"/></svg>
|
||||
</div>
|
||||
<h3 class="feature-title">开发者工具</h3>
|
||||
<p class="feature-desc">内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。</p>
|
||||
<a href="/guide/logging" class="feature-link">了解更多 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style scoped>
|
||||
/* 首页专用样式 | Home page specific styles */
|
||||
.news-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
}
|
||||
|
||||
.news-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-more:hover {
|
||||
background: #252525;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
display: flex;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
border-color: #3b9eff;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 200px;
|
||||
min-height: 140px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.news-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 16px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.news-card-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.news-card-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.news-card-content p {
|
||||
font-size: 0.875rem;
|
||||
color: #707070;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.features-section {
|
||||
background: #0d0d0d;
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.features-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #3b9eff;
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d0d0d;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #707070;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.feature-link {
|
||||
font-size: 14px;
|
||||
color: #3b9eff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feature-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.news-container,
|
||||
.features-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.news-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card-image {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
docs/package.json
Normal file
21
docs/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "docs-new",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.1",
|
||||
"@astrojs/vue": "^5.1.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.26"
|
||||
}
|
||||
}
|
||||
1
docs/public/favicon.svg
Normal file
1
docs/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||
|
After Width: | Height: | Size: 696 B |
BIN
docs/src/assets/houston.webp
Normal file
BIN
docs/src/assets/houston.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
45
docs/src/assets/logo.svg
Normal file
45
docs/src/assets/logo.svg
Normal file
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<!-- Dark gradient background -->
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d2d2d"/>
|
||||
<stop offset="100%" style="stop-color:#1a1a1a"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Clean white text -->
|
||||
<linearGradient id="whiteGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff"/>
|
||||
<stop offset="100%" style="stop-color:#e8e8e8"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Subtle inner shadow -->
|
||||
<filter id="innerShadow">
|
||||
<feOffset dx="0" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1" result="offset-blur"/>
|
||||
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
|
||||
<feFlood flood-color="black" flood-opacity="0.2" result="color"/>
|
||||
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
|
||||
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" fill="url(#bgGrad)"/>
|
||||
|
||||
<!-- Subtle border -->
|
||||
<rect x="1" y="1" width="510" height="510" fill="none" stroke="#3d3d3d" stroke-width="2"/>
|
||||
|
||||
<!-- ES Text -->
|
||||
<g filter="url(#innerShadow)">
|
||||
<!-- E -->
|
||||
<polygon points="72,120 72,392 240,392 240,340 140,340 140,282 220,282 220,230 140,230 140,172 240,172 240,120"
|
||||
fill="url(#whiteGrad)"/>
|
||||
|
||||
<!-- S -->
|
||||
<path d="M 280 172 Q 280 120 340 120 L 420 120 Q 450 120 450 160 L 450 186 L 398 186 L 398 168 Q 398 158 384 158 L 350 158 Q 320 158 320 188 Q 320 218 350 218 L 400 218 Q 450 218 450 274 L 450 332 Q 450 392 390 392 L 310 392 Q 270 392 270 340 L 270 314 L 322 314 L 322 340 Q 322 354 340 354 L 384 354 Q 404 354 404 324 L 404 290 Q 404 260 380 260 L 330 260 Q 280 260 280 208 Z"
|
||||
fill="url(#whiteGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- Accent line -->
|
||||
<rect x="72" y="424" width="368" height="4" fill="#ffffff" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
20
docs/src/components/Head.astro
Normal file
20
docs/src/components/Head.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import type { Props } from '@astrojs/starlight/props';
|
||||
import Default from '@astrojs/starlight/components/Head.astro';
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
|
||||
<!-- Preload fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Force dark mode -->
|
||||
<script is:inline>
|
||||
document.documentElement.dataset.theme = 'dark';
|
||||
localStorage.setItem('starlight-theme', 'dark');
|
||||
</script>
|
||||
3
docs/src/components/ThemeSelect.astro
Normal file
3
docs/src/components/ThemeSelect.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
// Empty component to disable theme selection (dark mode only)
|
||||
---
|
||||
7
docs/src/content.config.ts
Normal file
7
docs/src/content.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
64
docs/src/content/docs/api/index.md
Normal file
64
docs/src/content/docs/api/index.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: API 参考
|
||||
description: ESEngine 完整 API 文档
|
||||
---
|
||||
|
||||
# API 参考
|
||||
|
||||
ESEngine 提供完整的 TypeScript API 文档,涵盖所有核心类、接口和方法。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 基础类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Core](/api/classes/Core) | 框架核心单例,管理整个 ECS 生命周期 |
|
||||
| [Scene](/api/classes/Scene) | 场景类,包含实体和系统 |
|
||||
| [World](/api/classes/World) | 游戏世界,可包含多个场景 |
|
||||
| [Entity](/api/classes/Entity) | 实体类,组件的容器 |
|
||||
| [Component](/api/classes/Component) | 组件基类,纯数据容器 |
|
||||
|
||||
### 系统类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [EntitySystem](/api/classes/EntitySystem) | 实体系统基类 |
|
||||
| [ProcessingSystem](/api/classes/ProcessingSystem) | 处理系统,逐个处理实体 |
|
||||
| [IntervalSystem](/api/classes/IntervalSystem) | 间隔执行系统 |
|
||||
| [PassiveSystem](/api/classes/PassiveSystem) | 被动系统,不自动执行 |
|
||||
|
||||
### 工具类
|
||||
|
||||
| 类名 | 描述 |
|
||||
|------|------|
|
||||
| [Matcher](/api/classes/Matcher) | 实体匹配器,用于过滤实体 |
|
||||
| [Time](/api/classes/Time) | 时间管理器 |
|
||||
| [PerformanceMonitor](/api/classes/PerformanceMonitor) | 性能监控 |
|
||||
|
||||
## 装饰器
|
||||
|
||||
| 装饰器 | 描述 |
|
||||
|--------|------|
|
||||
| [@ECSComponent](/api/functions/ECSComponent) | 组件装饰器,用于注册组件 |
|
||||
| [@ECSSystem](/api/functions/ECSSystem) | 系统装饰器,用于注册系统 |
|
||||
|
||||
## 枚举
|
||||
|
||||
| 枚举 | 描述 |
|
||||
|------|------|
|
||||
| [ECSEventType](/api/enumerations/ECSEventType) | ECS 事件类型 |
|
||||
| [LogLevel](/api/enumerations/LogLevel) | 日志级别 |
|
||||
|
||||
## 接口
|
||||
|
||||
| 接口 | 描述 |
|
||||
|------|------|
|
||||
| [IScene](/api/interfaces/IScene) | 场景接口 |
|
||||
| [IComponent](/api/interfaces/IComponent) | 组件接口 |
|
||||
| [ISystemBase](/api/interfaces/ISystemBase) | 系统基础接口 |
|
||||
| [ICoreConfig](/api/interfaces/ICoreConfig) | Core 配置接口 |
|
||||
|
||||
:::tip[API 文档生成]
|
||||
完整 API 文档由 TypeDoc 自动生成,详见 GitHub 仓库中的 `/docs/api` 目录。
|
||||
:::
|
||||
20
docs/src/content/docs/en/examples/index.md
Normal file
20
docs/src/content/docs/en/examples/index.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: "Examples"
|
||||
description: "ESEngine example projects and demos"
|
||||
---
|
||||
|
||||
Explore example projects to learn ESEngine best practices.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### [Worker System Demo](/en/examples/worker-system-demo/)
|
||||
Demonstrates how to use Web Workers for parallel processing, offloading heavy computations from the main thread.
|
||||
|
||||
## External Examples
|
||||
|
||||
### [Lawn Mower Demo](https://github.com/esengine/lawn-mower-demo)
|
||||
A complete game demo showcasing ESEngine features including:
|
||||
- Entity-Component-System architecture
|
||||
- Behavior tree AI
|
||||
- Scene management
|
||||
- Platform adaptation
|
||||
312
docs/src/content/docs/en/guide/component/best-practices.md
Normal file
312
docs/src/content/docs/en/guide/component/best-practices.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Component design patterns and complex examples"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Keep Components Simple
|
||||
|
||||
```typescript
|
||||
// ✅ Good component design - single responsibility
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ Avoid this design - too many responsibilities
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// Too many unrelated properties
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Constructor for Initialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clear Type Definitions
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Referencing Other Entities
|
||||
|
||||
When components need to reference other entities (like parent-child relationships, follow targets), **the recommended approach is to store entity IDs**, then look up in System:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up target entity and handle logic in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// Look up target entity through scene
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// Follow logic
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// Move closer to target
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Advantages of this approach:
|
||||
- Components stay simple, only store basic data types
|
||||
- Follows data-oriented design
|
||||
- Unified lookup and logic handling in System
|
||||
- Easy to understand and maintain
|
||||
|
||||
**Avoid storing entity references directly in components**:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong example: Storing entity reference directly
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // Still holds reference after entity destroyed, may cause memory leak
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Component Examples
|
||||
|
||||
### State Machine Component
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Data Component
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // Deep copy to avoid shared reference
|
||||
}
|
||||
|
||||
// Provide convenience methods
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Components
|
||||
|
||||
```typescript
|
||||
// Tag components: No data, only for identification
|
||||
@ECSComponent('Player')
|
||||
class PlayerTag extends Component {}
|
||||
|
||||
@ECSComponent('Enemy')
|
||||
class EnemyTag extends Component {}
|
||||
|
||||
@ECSComponent('Dead')
|
||||
class DeadTag extends Component {}
|
||||
|
||||
// Use tags for querying
|
||||
class EnemySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(EnemyTag, Health).none(DeadTag));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Components
|
||||
|
||||
```typescript
|
||||
// Event/command buffer component
|
||||
@ECSComponent('DamageBuffer')
|
||||
class DamageBuffer extends Component {
|
||||
damages: { amount: number; source: number; timestamp: number }[] = [];
|
||||
|
||||
addDamage(amount: number, sourceId: number): void {
|
||||
this.damages.push({
|
||||
amount,
|
||||
source: sourceId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.damages.length = 0;
|
||||
}
|
||||
|
||||
getTotalDamage(): number {
|
||||
return this.damages.reduce((sum, d) => sum + d.amount, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How large should a component be?
|
||||
|
||||
**A**: Follow single responsibility principle. If a component contains unrelated data, split into multiple components.
|
||||
|
||||
### Q: Can components have methods?
|
||||
|
||||
**A**: Yes, but they should be data-related helper methods (like `isDead()`), not business logic. Business logic goes in Systems.
|
||||
|
||||
### Q: How to handle dependencies between components?
|
||||
|
||||
**A**: Handle inter-component interactions in Systems, don't directly access other components within a component.
|
||||
|
||||
### Q: When to use EntityRef?
|
||||
|
||||
**A**: Only when you need frequent access to referenced entity and the reference relationship is stable (like parent-child). Storing IDs is better for most cases.
|
||||
|
||||
---
|
||||
|
||||
Components are the data carriers of ECS architecture. Properly designing components makes your game code more modular, maintainable, and performant.
|
||||
228
docs/src/content/docs/en/guide/component/entity-ref.md
Normal file
228
docs/src/content/docs/en/guide/component/entity-ref.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "EntityRef Decorator"
|
||||
description: "Safe entity reference tracking mechanism"
|
||||
---
|
||||
|
||||
The framework provides the `@EntityRef` decorator for **special scenarios** to safely store entity references. This is an advanced feature; storing IDs is recommended for most cases.
|
||||
|
||||
## When Do You Need EntityRef?
|
||||
|
||||
`@EntityRef` can simplify code in these scenarios:
|
||||
|
||||
1. **Parent-Child Relationships**: Need to directly access parent or child entities in components
|
||||
2. **Complex Associations**: Multiple reference relationships between entities
|
||||
3. **Frequent Access**: Need to access referenced entity in multiple places, ID lookup has performance overhead
|
||||
|
||||
## Core Features
|
||||
|
||||
The `@EntityRef` decorator automatically tracks references through **ReferenceTracker**:
|
||||
|
||||
- When the referenced entity is destroyed, all `@EntityRef` properties pointing to it are automatically set to `null`
|
||||
- Prevents cross-scene references (outputs warning and refuses to set)
|
||||
- Prevents references to destroyed entities (outputs warning and sets to `null`)
|
||||
- Uses WeakRef to avoid memory leaks (automatic GC support)
|
||||
- Automatically cleans up reference registration when component is removed
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// When parent is destroyed, comp.parent automatically becomes null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Multiple Reference Properties
|
||||
|
||||
A component can have multiple `@EntityRef` properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// After enemy is destroyed, only target becomes null, ally remains valid
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
## Safety Checks
|
||||
|
||||
`@EntityRef` provides multiple safety checks:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// Cross-scene reference fails
|
||||
comp.parent = entity2; // Outputs error log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// Reference to destroyed entity fails
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // Outputs warning log, comp.parent is null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## Implementation Principle
|
||||
|
||||
`@EntityRef` uses the following mechanisms for automatic reference tracking:
|
||||
|
||||
1. **ReferenceTracker**: Scene holds a reference tracker that records all entity reference relationships
|
||||
2. **WeakRef**: Uses weak references to store components, avoiding memory leaks from circular references
|
||||
3. **Property Interception**: Intercepts getter/setter through `Object.defineProperty`
|
||||
4. **Automatic Cleanup**: When entity is destroyed, ReferenceTracker traverses all references and sets them to null
|
||||
|
||||
```typescript
|
||||
// Simplified implementation principle
|
||||
class ReferenceTracker {
|
||||
// entityId -> all component records referencing this entity
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// Called when entity is destroyed
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// Set component's reference property to null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
`@EntityRef` introduces some performance overhead:
|
||||
|
||||
- **Write Overhead**: Need to update ReferenceTracker each time a reference is set
|
||||
- **Memory Overhead**: ReferenceTracker needs to maintain reference mapping table
|
||||
- **Destroy Overhead**: Need to traverse all references and clean up when entity is destroyed
|
||||
|
||||
For most scenarios, this overhead is acceptable. But with **many entities and frequent reference changes**, storing IDs may be more efficient.
|
||||
|
||||
## Debug Support
|
||||
|
||||
ReferenceTracker provides debug interfaces:
|
||||
|
||||
```typescript
|
||||
// View which components reference an entity
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`Entity ${entity.name} is referenced by ${references.length} components`);
|
||||
|
||||
// Get complete debug info
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Comparison with Storing IDs
|
||||
|
||||
### Storing IDs (Recommended for Most Cases)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// Look up in System
|
||||
class FollowerSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
// Follow logic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using EntityRef (For Complex Associations)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Can directly access parent entity's component
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Approach | Use Case | Pros | Cons |
|
||||
|----------|----------|------|------|
|
||||
| Store ID | Most cases | Simple, no extra overhead | Need to lookup in System |
|
||||
| @EntityRef | Parent-child, complex associations | Auto-cleanup, cleaner code | Has performance overhead |
|
||||
|
||||
- **Recommended**: Use store ID + System lookup for most cases
|
||||
- **EntityRef Use Cases**: Parent-child relationships, complex associations, when component needs direct access to referenced entity
|
||||
- **Core Advantage**: Automatic cleanup, prevents dangling references, cleaner code
|
||||
- **Considerations**: Has performance overhead, not suitable for many dynamic references
|
||||
226
docs/src/content/docs/en/guide/component/index.md
Normal file
226
docs/src/content/docs/en/guide/component/index.md
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "Component System"
|
||||
description: "ECS component basics and creation methods"
|
||||
---
|
||||
|
||||
In ECS architecture, Components are carriers of data and behavior. Components define the properties and functionality that entities possess, and are the core building blocks of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Components are concrete classes that inherit from the `Component` abstract base class, used for:
|
||||
- Storing entity data (such as position, velocity, health, etc.)
|
||||
- Defining behavior methods related to the data
|
||||
- Providing lifecycle callback hooks
|
||||
- Supporting serialization and debugging
|
||||
|
||||
## Creating Components
|
||||
|
||||
### Basic Component Definition
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// Components can contain behavior methods
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @ECSComponent Decorator
|
||||
|
||||
`@ECSComponent` is a required decorator for component classes, providing type identification and metadata management.
|
||||
|
||||
### Why It's Required
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable type name that remains correct after code obfuscation |
|
||||
| **Serialization Support** | Uses this name as type identifier during serialization/deserialization |
|
||||
| **Component Registration** | Auto-registers to ComponentRegistry, assigns unique bitmask |
|
||||
| **Debug Support** | Shows readable component names in debug tools and logs |
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: Component's type name, recommended to use same or similar name as class name
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```typescript
|
||||
// ✅ Correct usage
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ Recommended: Keep type name consistent with class name
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ Wrong usage - no decorator
|
||||
class BadComponent extends Component {
|
||||
// Components defined this way may have issues in production:
|
||||
// 1. Class name changes after minification, can't serialize correctly
|
||||
// 2. Component not registered to framework, queries may fail
|
||||
}
|
||||
```
|
||||
|
||||
### Using with @Serializable
|
||||
|
||||
When components need serialization support, use `@ECSComponent` and `@Serializable` together:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: `@ECSComponent`'s `typeName` and `@Serializable`'s `typeId` can differ. If `@Serializable` doesn't specify `typeId`, it defaults to `@ECSComponent`'s `typeName`.
|
||||
|
||||
### Type Name Uniqueness
|
||||
|
||||
Each component's type name should be unique:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Two components using the same type name
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // Conflict!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ Correct: Use different type names
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## Component Properties
|
||||
|
||||
Each component has some built-in properties:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ID: ${this.id}`); // Unique component ID
|
||||
console.log(`Entity ID: ${this.entityId}`); // Owning entity's ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component and Entity Relationship
|
||||
|
||||
Components store the owning entity's ID (`entityId`), not a direct entity reference. This is a reflection of ECS's data-oriented design, avoiding circular references.
|
||||
|
||||
In practice, **entity and component interactions should be handled in Systems**, not within components:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Recommended: Handle logic in System
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// Remove Damage component after applying damage
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Lifecycle](/en/guide/component/lifecycle) - Component lifecycle hooks
|
||||
- [EntityRef Decorator](/en/guide/component/entity-ref) - Safe entity references
|
||||
- [Best Practices](/en/guide/component/best-practices) - Component design patterns and examples
|
||||
182
docs/src/content/docs/en/guide/component/lifecycle.md
Normal file
182
docs/src/content/docs/en/guide/component/lifecycle.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "Component Lifecycle"
|
||||
description: "Component lifecycle hooks and events"
|
||||
---
|
||||
|
||||
Components provide lifecycle hooks that can be overridden to execute specific logic.
|
||||
|
||||
## Lifecycle Methods
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* Called when component is added to entity
|
||||
* Use for initializing resources, establishing references, etc.
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} added, Entity ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when component is removed from entity
|
||||
* Use for cleaning up resources, breaking references, etc.
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`Component ${this.constructor.name} removed`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Order
|
||||
|
||||
```
|
||||
Entity created
|
||||
↓
|
||||
addComponent() called
|
||||
↓
|
||||
onAddedToEntity() triggered
|
||||
↓
|
||||
Component in normal use...
|
||||
↓
|
||||
removeComponent() or entity.destroy() called
|
||||
↓
|
||||
onRemovedFromEntity() triggered
|
||||
↓
|
||||
Component removed/destroyed
|
||||
```
|
||||
|
||||
## Practical Use Cases
|
||||
|
||||
### Resource Management
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TextureComponent')
|
||||
class TextureComponent extends Component {
|
||||
private _texture: Texture | null = null;
|
||||
texturePath: string = '';
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Load texture resource
|
||||
this._texture = TextureManager.load(this.texturePath);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Release texture resource
|
||||
if (this._texture) {
|
||||
TextureManager.release(this._texture);
|
||||
this._texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
get texture(): Texture | null {
|
||||
return this._texture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Listening
|
||||
|
||||
```typescript
|
||||
@ECSComponent('InputListener')
|
||||
class InputListener extends Component {
|
||||
private _boundHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
this._boundHandler = this.handleKeyDown.bind(this);
|
||||
window.addEventListener('keydown', this._boundHandler);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
if (this._boundHandler) {
|
||||
window.removeEventListener('keydown', this._boundHandler);
|
||||
this._boundHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent): void {
|
||||
// Handle keyboard input
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering with External Systems
|
||||
|
||||
```typescript
|
||||
@ECSComponent('PhysicsBody')
|
||||
class PhysicsBody extends Component {
|
||||
private _body: PhysicsWorld.Body | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// Create rigid body in physics world
|
||||
this._body = PhysicsWorld.createBody({
|
||||
entityId: this.entityId,
|
||||
type: 'dynamic'
|
||||
});
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// Remove rigid body from physics world
|
||||
if (this._body) {
|
||||
PhysicsWorld.removeBody(this._body);
|
||||
this._body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Avoid Accessing Other Components in Lifecycle
|
||||
|
||||
```typescript
|
||||
@ECSComponent('BadComponent')
|
||||
class BadComponent extends Component {
|
||||
onAddedToEntity(): void {
|
||||
// ⚠️ Not recommended: Other components may not be added yet
|
||||
const other = this.entity?.getComponent(OtherComponent);
|
||||
if (other) {
|
||||
// May be null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recommended: Use System to Handle Inter-Component Interactions
|
||||
|
||||
```typescript
|
||||
@ECSSystem('InitializationSystem')
|
||||
class InitializationSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(ComponentA, ComponentB));
|
||||
}
|
||||
|
||||
// Use onAdded event to ensure both components exist
|
||||
onAdded(entity: Entity): void {
|
||||
const a = entity.getComponent(ComponentA)!;
|
||||
const b = entity.getComponent(ComponentB)!;
|
||||
// Safely initialize interaction
|
||||
a.linkTo(b);
|
||||
}
|
||||
|
||||
onRemoved(entity: Entity): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison with System Lifecycle
|
||||
|
||||
| Feature | Component Lifecycle | System Lifecycle |
|
||||
|---------|---------------------|------------------|
|
||||
| Trigger Timing | When component added/removed | When match conditions met |
|
||||
| Use Case | Resource init/cleanup | Business logic processing |
|
||||
| Access Other Components | Not recommended | Safe |
|
||||
| Access Scene | Limited | Full |
|
||||
225
docs/src/content/docs/en/guide/entity-query/best-practices.md
Normal file
225
docs/src/content/docs/en/guide/entity-query/best-practices.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "Entity query optimization and practical applications"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Prefer EntitySystem
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Use EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Automatically get matching entities, updated each frame
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Not recommended: Manual query in update
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Manual query each frame wastes performance
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use none() for Exclusions
|
||||
|
||||
```typescript
|
||||
// Exclude dead enemies
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // Don't process dead enemies
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Tags for Query Optimization
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Query all entities then filter
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// ✅ Good: Query directly by tag
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. Avoid Overly Complex Query Conditions
|
||||
|
||||
```typescript
|
||||
// ❌ Not recommended: Too complex
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// ✅ Recommended: Split into multiple simple systems
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Applications
|
||||
|
||||
### Scenario 1: Physics System
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// Apply gravity
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// Update position
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 2: Render System
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // Exclude invisible entities
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Sort by z-order
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// Render entities
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 3: Collision Detection
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple O(n²) collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// Trigger collision event
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario 4: One-Time Query
|
||||
|
||||
```typescript
|
||||
// Execute one-time query outside of systems
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Statistics
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('Total queries:', stats.queryStats.totalQueries);
|
||||
console.log('Cache hit rate:', stats.queryStats.cacheHitRate);
|
||||
console.log('Cache size:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## Related APIs
|
||||
|
||||
- [Matcher](/api/classes/Matcher/) - Query condition descriptor API reference
|
||||
- [QuerySystem](/api/classes/QuerySystem/) - Query system API reference
|
||||
- [EntitySystem](/api/classes/EntitySystem/) - Entity system API reference
|
||||
- [Entity](/api/classes/Entity/) - Entity API reference
|
||||
133
docs/src/content/docs/en/guide/entity-query/compiled-query.md
Normal file
133
docs/src/content/docs/en/guide/entity-query/compiled-query.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Compiled Query"
|
||||
description: "CompiledQuery type-safe query tool"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery is a lightweight query tool providing type-safe component access and change detection support. Suitable for ad-hoc queries, tool development, and simple iteration scenarios.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
// Create compiled query
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// Type-safe iteration - component parameters auto-infer types
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// Get entity count
|
||||
console.log(`Matched entities: ${query.count}`);
|
||||
|
||||
// Get first matched entity
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`First entity: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Change Detection
|
||||
|
||||
CompiledQuery supports epoch-based change detection:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities where Transform or Sprite changed
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// Save current epoch as next check starting point
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// Update render data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Functional API
|
||||
|
||||
CompiledQuery provides rich functional APIs:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - Transform entity data
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - Filter entities
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - Find first matching entity
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - Convert to array
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - Check for matches
|
||||
if (query.any()) {
|
||||
console.log('Has matching entities');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('No matching entities');
|
||||
}
|
||||
```
|
||||
|
||||
## CompiledQuery vs EntitySystem
|
||||
|
||||
| Feature | CompiledQuery | EntitySystem |
|
||||
|---------|---------------|--------------|
|
||||
| **Purpose** | Lightweight query tool | Complete system logic |
|
||||
| **Lifecycle** | None | Full (onInitialize, onDestroy, etc.) |
|
||||
| **Scheduling Integration** | None | Supports @Stage, @Before, @After |
|
||||
| **Change Detection** | forEachChanged | forEachChanged |
|
||||
| **Event Listening** | None | addEventListener |
|
||||
| **Command Buffer** | None | this.commands |
|
||||
| **Type-Safe Components** | forEach params auto-infer | Need manual getComponent |
|
||||
| **Use Cases** | Ad-hoc queries, tools, prototypes | Core game logic |
|
||||
|
||||
**Selection Advice**:
|
||||
|
||||
- Use **EntitySystem** for core game logic (movement, combat, AI, etc.)
|
||||
- Use **CompiledQuery** for one-time queries, tool development, or simple iteration
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `forEach(callback)` | Iterate all matched entities, type-safe component params |
|
||||
| `forEachChanged(sinceEpoch, callback)` | Only iterate changed entities |
|
||||
| `first()` | Get first matched entity and components |
|
||||
| `toArray()` | Convert to [entity, ...components] array |
|
||||
| `map(callback)` | Map transformation |
|
||||
| `filter(predicate)` | Filter entities |
|
||||
| `find(predicate)` | Find first entity meeting condition |
|
||||
| `any()` | Whether any matches exist |
|
||||
| `empty()` | Whether no matches exist |
|
||||
| `count` | Number of matched entities |
|
||||
| `entities` | Matched entity list (read-only) |
|
||||
244
docs/src/content/docs/en/guide/entity-query/index.md
Normal file
244
docs/src/content/docs/en/guide/entity-query/index.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: "Entity Query System"
|
||||
description: "ECS entity query core concepts and basic usage"
|
||||
---
|
||||
|
||||
Entity querying is one of the core features of ECS architecture. This guide introduces how to use Matcher and QuerySystem to query and filter entities.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Matcher - Query Condition Descriptor
|
||||
|
||||
Matcher is a chainable API used to describe entity query conditions. It doesn't execute queries itself but passes conditions to EntitySystem or QuerySystem.
|
||||
|
||||
### QuerySystem - Query Execution Engine
|
||||
|
||||
QuerySystem is responsible for actually executing queries, using reactive query mechanisms internally for automatic performance optimization.
|
||||
|
||||
## Using Matcher in EntitySystem
|
||||
|
||||
This is the most common usage. EntitySystem automatically filters and processes entities matching conditions through Matcher.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Method 1: Use Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// Method 2: Use Matcher.all() directly (equivalent)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to scene
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher Chainable API
|
||||
|
||||
#### all() - Must Include All Components
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have both Health and Position components
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities with both components
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - Include At Least One Component
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must have at least Health or Shield
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process entities with health or shield
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - Must Not Include Components
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Entity must not have DeadTag component
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process living entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Combined Conditions
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // Must have position and health
|
||||
.any(WeaponComponent, MagicComponent) // At least weapon or magic
|
||||
.none(DeadTag, FrozenTag) // Not dead or frozen
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Process living entities that can fight
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - Match No Entities
|
||||
|
||||
Used for systems that only need lifecycle methods (`onBegin`, `onEnd`) but don't process entities.
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Match no entities
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at frame start
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at frame end
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing()
|
||||
|
||||
| Method | Behavior | Use Case |
|
||||
|--------|----------|----------|
|
||||
| `Matcher.empty()` | Match **all** entities | Process all entities in scene |
|
||||
| `Matcher.nothing()` | Match **no** entities | Only need lifecycle callbacks |
|
||||
|
||||
## Using QuerySystem Directly
|
||||
|
||||
If you don't need to create a system, you can use Scene's querySystem directly.
|
||||
|
||||
### Basic Query Methods
|
||||
|
||||
```typescript
|
||||
// Get scene's query system
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// Query entities with all specified components
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`Found ${result1.count} moving entities`);
|
||||
console.log(`Query time: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// Query entities with any specified component
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`Found ${result2.count} combat units`);
|
||||
|
||||
// Query entities without specified components
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`Found ${result3.count} living entities`);
|
||||
```
|
||||
|
||||
### Query by Tag and Name
|
||||
|
||||
```typescript
|
||||
// Query by tag
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('Player:', player.name);
|
||||
}
|
||||
|
||||
// Query by name
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('Found Boss:', boss);
|
||||
}
|
||||
|
||||
// Query by single component
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`${healthResult.count} entities have health`);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Automatic Caching
|
||||
|
||||
QuerySystem uses reactive queries internally with automatic caching:
|
||||
|
||||
```typescript
|
||||
// First query, executes actual query
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// Second same query, uses cache
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### Automatic Updates on Entity Changes
|
||||
|
||||
Query cache updates automatically when entities add/remove components:
|
||||
|
||||
```typescript
|
||||
// Query entities with weapons
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('Before:', before.count); // Assume 5
|
||||
|
||||
// Add weapon to entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// Query again, automatically includes new entity
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('After:', after.count); // Now 6
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Matcher API](/en/guide/entity-query/matcher-api) - Complete Matcher API reference
|
||||
- [Compiled Query](/en/guide/entity-query/compiled-query) - CompiledQuery advanced usage
|
||||
- [Best Practices](/en/guide/entity-query/best-practices) - Query optimization and practical applications
|
||||
118
docs/src/content/docs/en/guide/entity-query/matcher-api.md
Normal file
118
docs/src/content/docs/en/guide/entity-query/matcher-api.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "Matcher API"
|
||||
description: "Complete Matcher API reference"
|
||||
---
|
||||
|
||||
## Static Creation Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `Matcher.all(...types)` | Must include all specified components | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | Include at least one specified component | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | Must not include any specified components | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | Query by tag | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | Query by name | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | Query by single component | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | Create empty matcher (matches all entities) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | Match no entities | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | Create complex query builder | `Matcher.complex()` |
|
||||
|
||||
## Chainable Methods
|
||||
|
||||
| Method | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `.all(...types)` | Add required components | `.all(Position)` |
|
||||
| `.any(...types)` | Add optional components (at least one) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | Add excluded components | `.none(Dead)` |
|
||||
| `.exclude(...types)` | Alias for `.none()` | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | Alias for `.any()` | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | Add tag condition | `.withTag(1)` |
|
||||
| `.withName(name)` | Add name condition | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | Add single component condition | `.withComponent(Health)` |
|
||||
|
||||
## Utility Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `.getCondition()` | Get query condition (read-only) |
|
||||
| `.isEmpty()` | Check if empty condition |
|
||||
| `.isNothing()` | Check if nothing matcher |
|
||||
| `.clone()` | Clone matcher |
|
||||
| `.reset()` | Reset all conditions |
|
||||
| `.toString()` | Get string representation |
|
||||
|
||||
## Common Combination Examples
|
||||
|
||||
```typescript
|
||||
// Basic movement system
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// Attackable living entities
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// All tagged enemies
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// System only needing lifecycle
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## Query by Tag
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific tag
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process player entities
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Query by Name
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Query entities with specific name
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only process entities named 'Boss'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Matcher is Immutable
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// Chain calls return new Matcher instances
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// Original matcher unchanged
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
### Query Results are Read-Only
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// Don't modify returned array
|
||||
result.entities.push(someEntity); // Wrong!
|
||||
|
||||
// If modification needed, copy first
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // Correct
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
# Entity
|
||||
---
|
||||
title: "Entity"
|
||||
---
|
||||
|
||||
In ECS architecture, an Entity is the basic object in the game world. An entity itself does not contain game logic or data - it's just a container that combines different components to achieve various functionalities.
|
||||
|
||||
@@ -12,7 +14,7 @@ An entity is a lightweight object mainly used for:
|
||||
::: tip About Parent-Child Hierarchy
|
||||
Parent-child hierarchy relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles - only entities that need hierarchy relationships add this component.
|
||||
|
||||
See [Hierarchy System](./hierarchy.md) documentation.
|
||||
See [Hierarchy System](./hierarchy/) documentation.
|
||||
:::
|
||||
|
||||
## Creating Entities
|
||||
@@ -439,6 +441,6 @@ const emptyHandle = NULL_HANDLE;
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Learn about [Hierarchy System](./hierarchy.md) to establish parent-child relationships
|
||||
- Learn about [Component System](./component.md) to add functionality to entities
|
||||
- Learn about [Scene Management](./scene.md) to organize and manage entities
|
||||
- Learn about [Hierarchy System](./hierarchy/) to establish parent-child relationships
|
||||
- Learn about [Component System](./component/) to add functionality to entities
|
||||
- Learn about [Scene Management](./scene/) to organize and manage entities
|
||||
260
docs/src/content/docs/en/guide/event-system.md
Normal file
260
docs/src/content/docs/en/guide/event-system.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
title: "Event System"
|
||||
description: "Type-safe event system with sync/async events, priorities, and batching"
|
||||
---
|
||||
|
||||
The ECS framework includes a powerful type-safe event system supporting sync/async events, priorities, batching, and more advanced features. The event system is the core mechanism for inter-component and inter-system communication.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
The event system implements a publish-subscribe pattern with these core concepts:
|
||||
- **Event Publisher**: Object that emits events
|
||||
- **Event Listener**: Object that listens for and handles specific events
|
||||
- **Event Type**: String identifier to distinguish different event types
|
||||
- **Event Data**: Related information carried by the event
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Using Event System in Scene
|
||||
|
||||
Each scene has a built-in event system:
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Listen for events
|
||||
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
|
||||
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
|
||||
this.eventSystem.on('score_changed', this.onScoreChanged.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: { player: Entity, cause: string }): void {
|
||||
console.log(`Player died, cause: ${data.cause}`);
|
||||
}
|
||||
|
||||
private onEnemySpawned(data: { enemy: Entity, position: { x: number, y: number } }): void {
|
||||
console.log('Enemy spawned at:', data.position);
|
||||
}
|
||||
|
||||
private onScoreChanged(data: { newScore: number, oldScore: number }): void {
|
||||
console.log(`Score changed: ${data.oldScore} -> ${data.newScore}`);
|
||||
}
|
||||
|
||||
// Emit events in systems
|
||||
someGameLogic(): void {
|
||||
this.eventSystem.emitSync('score_changed', {
|
||||
newScore: 1000,
|
||||
oldScore: 800
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Events in Systems
|
||||
|
||||
Systems can conveniently listen for and send events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Combat));
|
||||
}
|
||||
|
||||
protected onInitialize(): void {
|
||||
// Use system's event listener method (auto-cleanup)
|
||||
this.addEventListener('player_attack', this.onPlayerAttack.bind(this));
|
||||
this.addEventListener('enemy_death', this.onEnemyDeath.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerAttack(data: { damage: number, target: Entity }): void {
|
||||
const health = data.target.getComponent(Health);
|
||||
if (health) {
|
||||
health.current -= data.damage;
|
||||
|
||||
if (health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('enemy_death', {
|
||||
enemy: data.target,
|
||||
killer: 'player'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onEnemyDeath(data: { enemy: Entity, killer: string }): void {
|
||||
data.enemy.destroy();
|
||||
this.scene?.eventSystem.emitSync('experience_gained', {
|
||||
amount: 100,
|
||||
source: 'enemy_kill'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### One-Time Listeners
|
||||
|
||||
```typescript
|
||||
// Listen only once
|
||||
this.eventSystem.once('game_start', this.onGameStart.bind(this));
|
||||
|
||||
// Or use configuration object
|
||||
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this), {
|
||||
once: true
|
||||
});
|
||||
```
|
||||
|
||||
### Priority Control
|
||||
|
||||
```typescript
|
||||
// Higher priority listeners execute first
|
||||
this.eventSystem.on('damage_dealt', this.onDamageDealt.bind(this), {
|
||||
priority: 100 // High priority
|
||||
});
|
||||
|
||||
this.eventSystem.on('damage_dealt', this.updateUI.bind(this), {
|
||||
priority: 0 // Default priority
|
||||
});
|
||||
|
||||
this.eventSystem.on('damage_dealt', this.logDamage.bind(this), {
|
||||
priority: -100 // Low priority, executes last
|
||||
});
|
||||
```
|
||||
|
||||
### Async Event Handling
|
||||
|
||||
```typescript
|
||||
protected initialize(): void {
|
||||
this.eventSystem.onAsync('save_game', this.onSaveGame.bind(this));
|
||||
}
|
||||
|
||||
private async onSaveGame(data: { saveSlot: number }): Promise<void> {
|
||||
console.log(`Saving game to slot ${data.saveSlot}`);
|
||||
await this.saveGameData(data.saveSlot);
|
||||
console.log('Game saved');
|
||||
}
|
||||
|
||||
// Emit async event
|
||||
public async triggerSave(): Promise<void> {
|
||||
await this.eventSystem.emit('save_game', { saveSlot: 1 });
|
||||
console.log('All async save operations complete');
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
For high-frequency events, use batching to improve performance:
|
||||
|
||||
```typescript
|
||||
protected onInitialize(): void {
|
||||
// Configure batch processing for position updates
|
||||
this.scene?.eventSystem.setBatchConfig('position_updated', {
|
||||
batchSize: 50,
|
||||
delay: 16,
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// Listen for batch events
|
||||
this.addEventListener('position_updated:batch', this.onPositionBatch.bind(this));
|
||||
}
|
||||
|
||||
private onPositionBatch(batchData: any): void {
|
||||
console.log(`Batch processing ${batchData.count} position updates`);
|
||||
for (const event of batchData.events) {
|
||||
this.updateMinimap(event.entityId, event.position);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global Event Bus
|
||||
|
||||
For cross-scene event communication:
|
||||
|
||||
```typescript
|
||||
import { GlobalEventBus } from '@esengine/ecs-framework';
|
||||
|
||||
class GameManager {
|
||||
private eventBus = GlobalEventBus.getInstance();
|
||||
|
||||
constructor() {
|
||||
this.eventBus.on('player_level_up', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('achievement_unlocked', this.onAchievementUnlocked.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: { level: number }): void {
|
||||
console.log(`Player leveled up to ${data.level}!`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Event Naming Convention
|
||||
|
||||
```typescript
|
||||
// ✅ Good naming
|
||||
this.eventSystem.emitSync('player:health_changed', data);
|
||||
this.eventSystem.emitSync('enemy:spawned', data);
|
||||
this.eventSystem.emitSync('ui:score_updated', data);
|
||||
|
||||
// ❌ Avoid
|
||||
this.eventSystem.emitSync('event1', data);
|
||||
this.eventSystem.emitSync('update', data);
|
||||
```
|
||||
|
||||
### 2. Type-Safe Event Data
|
||||
|
||||
```typescript
|
||||
interface PlayerHealthChangedEvent {
|
||||
entityId: number;
|
||||
oldHealth: number;
|
||||
newHealth: number;
|
||||
cause: 'damage' | 'healing';
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
private onHealthChanged(data: PlayerHealthChangedEvent): void {
|
||||
// TypeScript provides full type checking
|
||||
console.log(`Health changed: ${data.oldHealth} -> ${data.newHealth}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Avoid Event Loops
|
||||
|
||||
```typescript
|
||||
// ❌ Avoid: May cause infinite loop
|
||||
private onScoreChanged(data: any): void {
|
||||
this.scene?.eventSystem.emitSync('score_changed', newData); // Dangerous!
|
||||
}
|
||||
|
||||
// ✅ Correct: Use guard flag
|
||||
private isProcessingScore = false;
|
||||
|
||||
private onScoreChanged(data: any): void {
|
||||
if (this.isProcessingScore) return;
|
||||
|
||||
this.isProcessingScore = true;
|
||||
this.updateUI(data);
|
||||
this.isProcessingScore = false;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Clean Up Event Listeners
|
||||
|
||||
```typescript
|
||||
class TemporaryUI {
|
||||
private listenerId: string;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.listenerId = scene.eventSystem.on('ui_update', this.onUpdate.bind(this));
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.listenerId) {
|
||||
scene.eventSystem.off('ui_update', this.listenerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
# Quick Start
|
||||
---
|
||||
title: "Quick Start"
|
||||
---
|
||||
|
||||
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
|
||||
|
||||
202
docs/src/content/docs/en/guide/hierarchy.md
Normal file
202
docs/src/content/docs/en/guide/hierarchy.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: "Hierarchy System"
|
||||
description: "Parent-child entity relationships using component-based design"
|
||||
---
|
||||
|
||||
In game development, parent-child hierarchy relationships between entities are common requirements. ECS Framework manages hierarchy relationships through a component-based approach using `HierarchyComponent` and `HierarchySystem`, fully adhering to ECS composition principles.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### Why Not Built-in Hierarchy in Entity?
|
||||
|
||||
Traditional game object models build hierarchy into entities. ECS Framework chose a component-based approach because:
|
||||
|
||||
1. **ECS Composition Principle**: Hierarchy is a "feature" that should be added through components, not inherent to all entities
|
||||
2. **On-Demand Usage**: Only entities that need hierarchy add `HierarchyComponent`
|
||||
3. **Data-Logic Separation**: `HierarchyComponent` stores data, `HierarchySystem` handles logic
|
||||
4. **Serialization-Friendly**: Hierarchy as component data can be easily serialized/deserialized
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
### HierarchyComponent
|
||||
|
||||
Component storing hierarchy relationship data:
|
||||
|
||||
```typescript
|
||||
import { HierarchyComponent } from '@esengine/ecs-framework';
|
||||
|
||||
interface HierarchyComponent {
|
||||
parentId: number | null; // Parent entity ID, null means root
|
||||
childIds: number[]; // Child entity ID list
|
||||
depth: number; // Depth in hierarchy (maintained by system)
|
||||
bActiveInHierarchy: boolean; // Active in hierarchy (maintained by system)
|
||||
}
|
||||
```
|
||||
|
||||
### HierarchySystem
|
||||
|
||||
System handling hierarchy logic, provides all hierarchy operation APIs:
|
||||
|
||||
```typescript
|
||||
import { HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Add System to Scene
|
||||
|
||||
```typescript
|
||||
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
this.addSystem(new HierarchySystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Establish Parent-Child Relationships
|
||||
|
||||
```typescript
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child1 = scene.createEntity("Child1");
|
||||
const child2 = scene.createEntity("Child2");
|
||||
|
||||
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
|
||||
|
||||
// Set parent-child relationship (auto-adds HierarchyComponent)
|
||||
hierarchySystem.setParent(child1, parent);
|
||||
hierarchySystem.setParent(child2, parent);
|
||||
```
|
||||
|
||||
### Query Hierarchy
|
||||
|
||||
```typescript
|
||||
// Get parent entity
|
||||
const parentEntity = hierarchySystem.getParent(child1);
|
||||
|
||||
// Get all children
|
||||
const children = hierarchySystem.getChildren(parent);
|
||||
|
||||
// Get child count
|
||||
const count = hierarchySystem.getChildCount(parent);
|
||||
|
||||
// Check if has children
|
||||
const hasKids = hierarchySystem.hasChildren(parent);
|
||||
|
||||
// Get depth in hierarchy
|
||||
const depth = hierarchySystem.getDepth(child1); // Returns 1
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Parent-Child Operations
|
||||
|
||||
```typescript
|
||||
// Set parent
|
||||
hierarchySystem.setParent(child, parent);
|
||||
|
||||
// Move to root (no parent)
|
||||
hierarchySystem.setParent(child, null);
|
||||
|
||||
// Insert child at position
|
||||
hierarchySystem.insertChildAt(parent, child, 0);
|
||||
|
||||
// Remove child (becomes root)
|
||||
hierarchySystem.removeChild(parent, child);
|
||||
|
||||
// Remove all children
|
||||
hierarchySystem.removeAllChildren(parent);
|
||||
```
|
||||
|
||||
### Hierarchy Queries
|
||||
|
||||
```typescript
|
||||
// Get root of entity
|
||||
const root = hierarchySystem.getRoot(deepChild);
|
||||
|
||||
// Get all root entities
|
||||
const roots = hierarchySystem.getRootEntities();
|
||||
|
||||
// Check ancestor/descendant relationships
|
||||
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child);
|
||||
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent);
|
||||
```
|
||||
|
||||
### Hierarchy Traversal
|
||||
|
||||
```typescript
|
||||
// Find child by name
|
||||
const child = hierarchySystem.findChild(parent, "ChildName");
|
||||
|
||||
// Recursive search
|
||||
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
|
||||
|
||||
// Find children by tag
|
||||
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
|
||||
|
||||
// Iterate children
|
||||
hierarchySystem.forEachChild(parent, (child) => {
|
||||
console.log(child.name);
|
||||
}, true); // true for recursive
|
||||
```
|
||||
|
||||
### Hierarchy State
|
||||
|
||||
```typescript
|
||||
// Check if active in hierarchy (considers all ancestors)
|
||||
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
|
||||
|
||||
// Get depth (root = 0)
|
||||
const depth = hierarchySystem.getDepth(entity);
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
class GameScene extends Scene {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
protected initialize(): void {
|
||||
this.hierarchySystem = new HierarchySystem();
|
||||
this.addSystem(this.hierarchySystem);
|
||||
this.createPlayerHierarchy();
|
||||
}
|
||||
|
||||
private createPlayerHierarchy(): void {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new Transform(0, 0));
|
||||
|
||||
const body = this.createEntity("Body");
|
||||
body.addComponent(new Sprite("body.png"));
|
||||
this.hierarchySystem.setParent(body, player);
|
||||
|
||||
const weapon = this.createEntity("Weapon");
|
||||
weapon.addComponent(new Sprite("sword.png"));
|
||||
this.hierarchySystem.setParent(weapon, body);
|
||||
|
||||
console.log(`Player depth: ${this.hierarchySystem.getDepth(player)}`); // 0
|
||||
console.log(`Weapon depth: ${this.hierarchySystem.getDepth(weapon)}`); // 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Avoid Deep Nesting**: System limits max depth to 32 levels
|
||||
2. **Batch Operations**: Set up all parent-child relationships at once when building complex hierarchies
|
||||
3. **On-Demand Addition**: Only add `HierarchyComponent` to entities that truly need hierarchy
|
||||
4. **Cache System Reference**: Avoid getting `HierarchySystem` on every call
|
||||
|
||||
```typescript
|
||||
// Good practice
|
||||
class MySystem extends EntitySystem {
|
||||
private hierarchySystem!: HierarchySystem;
|
||||
|
||||
onAddedToScene() {
|
||||
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
# Guide
|
||||
---
|
||||
title: "Guide"
|
||||
---
|
||||
|
||||
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
|
||||
|
||||
225
docs/src/content/docs/en/guide/logging.md
Normal file
225
docs/src/content/docs/en/guide/logging.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Logging System"
|
||||
description: "Multi-level logging with colors, prefixes, and flexible configuration"
|
||||
---
|
||||
|
||||
The ECS framework provides a powerful hierarchical logging system supporting multiple log levels, color output, custom prefixes, and flexible configuration options.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
- **Log Levels**: Debug < Info < Warn < Error < Fatal < None
|
||||
- **Logger**: Named log outputter, each module can have its own logger
|
||||
- **Logger Manager**: Singleton managing all loggers globally
|
||||
- **Color Config**: Supports console color output
|
||||
|
||||
## Log Levels
|
||||
|
||||
```typescript
|
||||
import { LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
LogLevel.Debug // 0 - Debug information
|
||||
LogLevel.Info // 1 - General information
|
||||
LogLevel.Warn // 2 - Warning information
|
||||
LogLevel.Error // 3 - Error information
|
||||
LogLevel.Fatal // 4 - Fatal errors
|
||||
LogLevel.None // 5 - No output
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Using Default Logger
|
||||
|
||||
```typescript
|
||||
import { Logger } from '@esengine/ecs-framework';
|
||||
|
||||
class GameSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
Logger.debug('Processing entities:', entities.length);
|
||||
Logger.info('System running normally');
|
||||
Logger.warn('Performance issue detected');
|
||||
Logger.error('Error during processing', new Error('Example'));
|
||||
Logger.fatal('Fatal error, system stopping');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Named Logger
|
||||
|
||||
```typescript
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
private logger = createLogger('MovementSystem');
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
this.logger.info(`Processing ${entities.length} moving entities`);
|
||||
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
this.logger.debug(`Entity ${entity.id} moved to (${position.x}, ${position.y})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Log Configuration
|
||||
|
||||
### Set Global Log Level
|
||||
|
||||
```typescript
|
||||
import { setGlobalLogLevel, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// Development: show all logs
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
|
||||
// Production: show warnings and above
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
|
||||
// Disable all logs
|
||||
setGlobalLogLevel(LogLevel.None);
|
||||
```
|
||||
|
||||
### Custom Logger Configuration
|
||||
|
||||
```typescript
|
||||
import { ConsoleLogger, LogLevel } from '@esengine/ecs-framework';
|
||||
|
||||
// Development logger
|
||||
const debugLogger = new ConsoleLogger({
|
||||
level: LogLevel.Debug,
|
||||
enableTimestamp: true,
|
||||
enableColors: true,
|
||||
prefix: 'DEV'
|
||||
});
|
||||
|
||||
// Production logger
|
||||
const productionLogger = new ConsoleLogger({
|
||||
level: LogLevel.Error,
|
||||
enableTimestamp: true,
|
||||
enableColors: false,
|
||||
prefix: 'PROD'
|
||||
});
|
||||
```
|
||||
|
||||
## Color Configuration
|
||||
|
||||
```typescript
|
||||
import { Colors, setLoggerColors } from '@esengine/ecs-framework';
|
||||
|
||||
setLoggerColors({
|
||||
debug: Colors.BRIGHT_BLACK,
|
||||
info: Colors.BLUE,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Hierarchical Loggers
|
||||
|
||||
```typescript
|
||||
import { LoggerManager } from '@esengine/ecs-framework';
|
||||
|
||||
const manager = LoggerManager.getInstance();
|
||||
|
||||
// Create child loggers
|
||||
const movementLogger = manager.createChildLogger('GameSystems', 'Movement');
|
||||
const renderLogger = manager.createChildLogger('GameSystems', 'Render');
|
||||
|
||||
// Child logger shows full path: [GameSystems.Movement]
|
||||
movementLogger.debug('Movement system initialized');
|
||||
```
|
||||
|
||||
### Third-Party Logger Integration
|
||||
|
||||
```typescript
|
||||
import { setLoggerFactory } from '@esengine/ecs-framework';
|
||||
|
||||
// Integrate with Winston
|
||||
setLoggerFactory((name?: string) => winston.createLogger({ /* ... */ }));
|
||||
|
||||
// Integrate with Pino
|
||||
setLoggerFactory((name?: string) => pino({ name }));
|
||||
|
||||
// Integrate with NestJS Logger
|
||||
setLoggerFactory((name?: string) => new Logger(name));
|
||||
```
|
||||
|
||||
### Custom Output
|
||||
|
||||
```typescript
|
||||
const fileLogger = new ConsoleLogger({
|
||||
level: LogLevel.Info,
|
||||
output: (level: LogLevel, message: string) => {
|
||||
this.writeToFile(LogLevel[level], message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Choose Appropriate Log Levels
|
||||
|
||||
```typescript
|
||||
// Debug - Detailed debug info
|
||||
this.logger.debug('Variable values', { x: 10, y: 20 });
|
||||
|
||||
// Info - Important state changes
|
||||
this.logger.info('System startup complete');
|
||||
|
||||
// Warn - Abnormal but non-fatal
|
||||
this.logger.warn('Resource not found, using default');
|
||||
|
||||
// Error - Errors but program can continue
|
||||
this.logger.error('Save failed, will retry', new Error('Network timeout'));
|
||||
|
||||
// Fatal - Fatal errors, program cannot continue
|
||||
this.logger.fatal('Out of memory, exiting');
|
||||
```
|
||||
|
||||
### 2. Structured Log Data
|
||||
|
||||
```typescript
|
||||
this.logger.info('User action', {
|
||||
userId: 12345,
|
||||
action: 'move',
|
||||
position: { x: 100, y: 200 },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Avoid Performance Issues
|
||||
|
||||
```typescript
|
||||
// ✅ Check log level before expensive computation
|
||||
if (this.logger.debug) {
|
||||
const expensiveData = this.calculateExpensiveDebugInfo();
|
||||
this.logger.debug('Debug info', expensiveData);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Environment-Based Configuration
|
||||
|
||||
```typescript
|
||||
class LoggingConfiguration {
|
||||
public static setupLogging(): void {
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
if (isDevelopment) {
|
||||
setGlobalLogLevel(LogLevel.Debug);
|
||||
setLoggerColors({
|
||||
debug: Colors.CYAN,
|
||||
info: Colors.GREEN,
|
||||
warn: Colors.YELLOW,
|
||||
error: Colors.RED,
|
||||
fatal: Colors.BRIGHT_RED
|
||||
});
|
||||
} else {
|
||||
setGlobalLogLevel(LogLevel.Warn);
|
||||
LoggerManager.getInstance().resetColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
title: "persistent-entity"
|
||||
---
|
||||
|
||||
# Persistent Entity
|
||||
|
||||
> **Version**: v2.3.0+
|
||||
291
docs/src/content/docs/en/guide/platform-adapter.md
Normal file
291
docs/src/content/docs/en/guide/platform-adapter.md
Normal file
@@ -0,0 +1,291 @@
|
||||
---
|
||||
title: "Platform Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The ECS framework provides a platform adapter interface that allows users to implement custom platform adapters for different runtime environments.
|
||||
|
||||
**The core library only provides interface definitions. Platform adapter implementations should be copied from the documentation.**
|
||||
|
||||
## Why No Separate Adapter Packages?
|
||||
|
||||
1. **Flexibility**: Different projects may have different platform adaptation needs. Copying code allows users to freely modify as needed
|
||||
2. **Reduce Dependencies**: Avoid introducing unnecessary dependency packages, keeping the core framework lean
|
||||
3. **Customization**: Users can customize according to specific runtime environments and requirements
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
### [Browser Adapter](./platform-adapter/browser/)
|
||||
|
||||
Supports all modern browser environments, including Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker (Web Worker)
|
||||
- SharedArrayBuffer (requires COOP/COEP)
|
||||
- Transferable Objects
|
||||
- Module Worker (modern browsers)
|
||||
|
||||
**Use Cases**: Web games, Web applications, PWA
|
||||
|
||||
---
|
||||
|
||||
### [WeChat Mini Game Adapter](./platform-adapter/wechat-minigame/)
|
||||
|
||||
Designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker (max 1, requires game.json configuration)
|
||||
- SharedArrayBuffer (not supported)
|
||||
- Transferable Objects (not supported)
|
||||
- WeChat Device Info API
|
||||
|
||||
**Use Cases**: WeChat Mini Game development
|
||||
|
||||
---
|
||||
|
||||
### [Node.js Adapter](./platform-adapter/nodejs/)
|
||||
|
||||
Provides support for Node.js server environments, suitable for game servers and compute servers.
|
||||
|
||||
**Feature Support**:
|
||||
- Worker Threads
|
||||
- SharedArrayBuffer
|
||||
- Transferable Objects
|
||||
- Complete system information
|
||||
|
||||
**Use Cases**: Game servers, compute servers, CLI tools
|
||||
|
||||
---
|
||||
|
||||
## Core Interfaces
|
||||
|
||||
### IPlatformAdapter
|
||||
|
||||
```typescript
|
||||
export interface IPlatformAdapter {
|
||||
readonly name: string;
|
||||
readonly version?: string;
|
||||
|
||||
isWorkerSupported(): boolean;
|
||||
isSharedArrayBufferSupported(): boolean;
|
||||
getHardwareConcurrency(): number;
|
||||
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker;
|
||||
createSharedArrayBuffer(length: number): SharedArrayBuffer | null;
|
||||
getHighResTimestamp(): number;
|
||||
getPlatformConfig(): PlatformConfig;
|
||||
getPlatformConfigAsync?(): Promise<PlatformConfig>;
|
||||
}
|
||||
```
|
||||
|
||||
### PlatformWorker Interface
|
||||
|
||||
```typescript
|
||||
export interface PlatformWorker {
|
||||
postMessage(message: any, transfer?: Transferable[]): void;
|
||||
onMessage(handler: (event: { data: any }) => void): void;
|
||||
onError(handler: (error: ErrorEvent) => void): void;
|
||||
terminate(): void;
|
||||
readonly state: 'running' | 'terminated';
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Choose the Appropriate Platform Adapter
|
||||
|
||||
Select the corresponding adapter based on your runtime environment:
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new BrowserAdapter());
|
||||
}
|
||||
|
||||
// WeChat Mini Game environment
|
||||
else if (typeof wx !== 'undefined') {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new WeChatMiniGameAdapter());
|
||||
}
|
||||
|
||||
// Node.js environment
|
||||
else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
PlatformManager.getInstance().registerAdapter(new NodeAdapter());
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Check Adapter Status
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
// Check if adapter is registered
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
console.log('Current platform:', adapter.name);
|
||||
console.log('Platform version:', adapter.version);
|
||||
|
||||
// Check feature support
|
||||
console.log('Worker support:', manager.supportsFeature('worker'));
|
||||
console.log('SharedArrayBuffer support:', manager.supportsFeature('shared-array-buffer'));
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Adapters
|
||||
|
||||
If existing platform adapters don't meet your needs, you can create custom adapters:
|
||||
|
||||
### 1. Implement the Interface
|
||||
|
||||
```typescript
|
||||
import type { IPlatformAdapter, PlatformWorker, WorkerCreationOptions, PlatformConfig } from '@esengine/ecs-framework';
|
||||
|
||||
export class CustomAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'custom';
|
||||
public readonly version = '1.0.0';
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
// Implement your Worker support check logic
|
||||
return false;
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Implement your SharedArrayBuffer support check logic
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
// Return your platform's concurrency count
|
||||
return 1;
|
||||
}
|
||||
|
||||
public createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
|
||||
throw new Error('Worker not supported on this platform');
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
limitations: {
|
||||
workerNotSupported: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register Custom Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { CustomAdapter } from './CustomAdapter';
|
||||
|
||||
const customAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(customAdapter);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Platform Detection Order
|
||||
|
||||
Recommend detecting and registering platform adapters in this order:
|
||||
|
||||
```typescript
|
||||
async function initializePlatform() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
try {
|
||||
// 1. WeChat Mini Game (highest priority, most distinctive environment)
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
const { WeChatMiniGameAdapter } = await import('./platform/WeChatMiniGameAdapter');
|
||||
manager.registerAdapter(new WeChatMiniGameAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Node.js environment
|
||||
if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
const { NodeAdapter } = await import('./platform/NodeAdapter');
|
||||
manager.registerAdapter(new NodeAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Browser environment (last check, broadest coverage)
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const { BrowserAdapter } = await import('./platform/BrowserAdapter');
|
||||
manager.registerAdapter(new BrowserAdapter());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Unknown environment, use default adapter
|
||||
console.warn('Unrecognized platform environment, using default adapter');
|
||||
manager.registerAdapter(new CustomAdapter());
|
||||
|
||||
} catch (error) {
|
||||
console.error('Platform adapter initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Feature Degradation Handling
|
||||
|
||||
```typescript
|
||||
function createWorkerSystem() {
|
||||
const manager = PlatformManager.getInstance();
|
||||
|
||||
if (!manager.hasAdapter()) {
|
||||
throw new Error('No platform adapter registered');
|
||||
}
|
||||
|
||||
const config: WorkerSystemConfig = {
|
||||
enableWorker: manager.supportsFeature('worker'),
|
||||
workerCount: manager.supportsFeature('worker') ?
|
||||
manager.getAdapter().getHardwareConcurrency() : 1,
|
||||
useSharedArrayBuffer: manager.supportsFeature('shared-array-buffer')
|
||||
};
|
||||
|
||||
// If Worker not supported, automatically degrade to synchronous processing
|
||||
if (!config.enableWorker) {
|
||||
console.info('Current platform does not support Worker, using synchronous processing mode');
|
||||
}
|
||||
|
||||
return new PhysicsSystem(config);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await initializePlatform();
|
||||
|
||||
// Validate adapter functionality
|
||||
const manager = PlatformManager.getInstance();
|
||||
const adapter = manager.getAdapter();
|
||||
|
||||
console.log(`Platform adapter initialized: ${adapter.name} v${adapter.version}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Platform initialization failed:', error);
|
||||
|
||||
// Provide fallback solution
|
||||
const fallbackAdapter = new CustomAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(fallbackAdapter);
|
||||
|
||||
console.warn('Using fallback adapter to continue running');
|
||||
}
|
||||
```
|
||||
372
docs/src/content/docs/en/guide/platform-adapter/browser.md
Normal file
372
docs/src/content/docs/en/guide/platform-adapter/browser.md
Normal file
@@ -0,0 +1,372 @@
|
||||
---
|
||||
title: "Browser Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The browser platform adapter provides support for standard web browser environments, including modern browsers like Chrome, Firefox, Safari, Edge, etc.
|
||||
|
||||
## Feature Support
|
||||
|
||||
- **Worker**: Supports Web Worker and Module Worker
|
||||
- **SharedArrayBuffer**: Supported (requires COOP/COEP headers)
|
||||
- **Transferable Objects**: Fully supported
|
||||
- **High-Resolution Time**: Uses `performance.now()`
|
||||
- **Basic Info**: Browser version and basic configuration
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Browser platform adapter
|
||||
* Supports standard web browser environments
|
||||
*/
|
||||
export class BrowserAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'browser';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = this.getBrowserInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker is supported
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof Worker !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is supported
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hardware concurrency (CPU core count)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return navigator.hardwareConcurrency || 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Browser does not support Worker');
|
||||
}
|
||||
|
||||
try {
|
||||
return new BrowserWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create browser Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer creation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high-resolution timestamp
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false,
|
||||
requiresWorkerInit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser information
|
||||
*/
|
||||
private getBrowserInfo(): string {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (userAgent.includes('Chrome')) {
|
||||
const match = userAgent.match(/Chrome\/([0-9.]+)/);
|
||||
return match ? `Chrome ${match[1]}` : 'Chrome';
|
||||
} else if (userAgent.includes('Firefox')) {
|
||||
const match = userAgent.match(/Firefox\/([0-9.]+)/);
|
||||
if (match) return `Firefox ${match[1]}`;
|
||||
} else if (userAgent.includes('Safari')) {
|
||||
const match = userAgent.match(/Version\/([0-9.]+)/);
|
||||
if (match) return `Safari ${match[1]}`;
|
||||
}
|
||||
return 'Unknown Browser';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is actually available
|
||||
*/
|
||||
private checkSharedArrayBufferEnabled(): boolean {
|
||||
try {
|
||||
new SharedArrayBuffer(8);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser Worker wrapper
|
||||
*/
|
||||
class BrowserWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
this.worker = this.createBrowserWorker(script, options);
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message to Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onmessage = (event: MessageEvent) => {
|
||||
handler({ data: event.data });
|
||||
};
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.onerror = handler;
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
} catch (error) {
|
||||
console.error('Failed to terminate Worker:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create browser Worker
|
||||
*/
|
||||
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
|
||||
try {
|
||||
// Create Blob URL
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create Worker
|
||||
const worker = new Worker(url, {
|
||||
type: options.type || 'classic',
|
||||
credentials: options.credentials,
|
||||
name: options.name
|
||||
});
|
||||
|
||||
// Clean up Blob URL (delayed to ensure Worker has loaded)
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot create browser Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Copy the Code
|
||||
|
||||
Copy the above code to your project, e.g., `src/platform/BrowserAdapter.ts`.
|
||||
|
||||
### 2. Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { BrowserAdapter } from './platform/BrowserAdapter';
|
||||
|
||||
// Create and register browser adapter
|
||||
const browserAdapter = new BrowserAdapter();
|
||||
PlatformManager.registerAdapter(browserAdapter);
|
||||
|
||||
// Framework will automatically detect and use the appropriate adapter
|
||||
```
|
||||
|
||||
### 3. Use WorkerEntitySystem
|
||||
|
||||
The browser adapter works with WorkerEntitySystem, and the framework automatically handles Worker script creation:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: navigator.hardwareConcurrency || 4,
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// This function is automatically serialized and executed in Worker
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Verify Adapter Status
|
||||
|
||||
```typescript
|
||||
// Verify adapter is working properly
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Adapter name:', adapter.name);
|
||||
console.log('Browser version:', adapter.version);
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU core count:', adapter.getHardwareConcurrency());
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### SharedArrayBuffer Support
|
||||
|
||||
SharedArrayBuffer requires special security configuration:
|
||||
|
||||
1. **HTTPS**: Must be used in a secure context
|
||||
2. **COOP/COEP Headers**: Requires correct cross-origin isolation headers
|
||||
|
||||
```html
|
||||
<!-- Set in HTML -->
|
||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
||||
```
|
||||
|
||||
Or set in server configuration:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### Browser Compatibility
|
||||
|
||||
- **Worker**: All modern browsers supported
|
||||
- **Module Worker**: Chrome 80+, Firefox 114+
|
||||
- **SharedArrayBuffer**: Chrome 68+, Firefox 79+ (requires COOP/COEP)
|
||||
- **Transferable Objects**: All modern browsers supported
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
1. **Worker Pool**: Reuse Worker instances to avoid frequent creation and destruction
|
||||
2. **Data Transfer**: Use Transferable Objects to reduce data copying
|
||||
3. **SharedArrayBuffer**: Use SharedArrayBuffer for large data sharing
|
||||
4. **Module Worker**: Use module Workers in supported browsers for better code organization
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check browser support
|
||||
const adapter = new BrowserAdapter();
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('Hardware concurrency:', adapter.getHardwareConcurrency());
|
||||
console.log('Platform config:', adapter.getPlatformConfig());
|
||||
```
|
||||
560
docs/src/content/docs/en/guide/platform-adapter/nodejs.md
Normal file
560
docs/src/content/docs/en/guide/platform-adapter/nodejs.md
Normal file
@@ -0,0 +1,560 @@
|
||||
---
|
||||
title: "Node.js Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Node.js platform adapter provides support for Node.js server environments, suitable for game servers, compute servers, or other server applications that need ECS architecture.
|
||||
|
||||
## Feature Support
|
||||
|
||||
- **Worker**: Supported (via `worker_threads` module)
|
||||
- **SharedArrayBuffer**: Supported (Node.js 16.17.0+)
|
||||
- **Transferable Objects**: Fully supported
|
||||
- **High-Resolution Time**: Uses `process.hrtime.bigint()`
|
||||
- **Device Info**: Complete system and process information
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
import { worker_threads, Worker, isMainThread, parentPort } from 'worker_threads';
|
||||
import * as os from 'os';
|
||||
import * as process from 'process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig,
|
||||
NodeDeviceInfo
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Node.js platform adapter
|
||||
* Supports Node.js server environments
|
||||
*/
|
||||
export class NodeAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'nodejs';
|
||||
public readonly version: string;
|
||||
|
||||
constructor() {
|
||||
this.version = process.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Worker is supported
|
||||
*/
|
||||
public isWorkerSupported(): boolean {
|
||||
try {
|
||||
// Check if worker_threads module is available
|
||||
return typeof worker_threads !== 'undefined' && typeof Worker !== 'undefined';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SharedArrayBuffer is supported
|
||||
*/
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
// Node.js supports SharedArrayBuffer
|
||||
return typeof SharedArrayBuffer !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hardware concurrency (CPU core count)
|
||||
*/
|
||||
public getHardwareConcurrency(): number {
|
||||
return os.cpus().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Worker
|
||||
*/
|
||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('Node.js environment does not support Worker Threads');
|
||||
}
|
||||
|
||||
try {
|
||||
return new NodeWorker(script, options);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SharedArrayBuffer
|
||||
*/
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
if (!this.isSharedArrayBufferSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SharedArrayBuffer(length);
|
||||
} catch (error) {
|
||||
console.warn('SharedArrayBuffer creation failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get high-resolution timestamp (nanoseconds)
|
||||
*/
|
||||
public getHighResTimestamp(): number {
|
||||
// Return milliseconds, consistent with browser performance.now()
|
||||
return Number(process.hrtime.bigint()) / 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform configuration
|
||||
*/
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: this.getHardwareConcurrency(),
|
||||
supportsModuleWorker: true, // Node.js supports ES modules
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: this.getMaxSharedArrayBufferSize(),
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: false, // Node.js supports eval
|
||||
requiresWorkerInit: false
|
||||
},
|
||||
extensions: {
|
||||
platform: 'nodejs',
|
||||
nodeVersion: process.version,
|
||||
v8Version: process.versions.v8,
|
||||
uvVersion: process.versions.uv,
|
||||
zlibVersion: process.versions.zlib,
|
||||
opensslVersion: process.versions.openssl,
|
||||
architecture: process.arch,
|
||||
endianness: os.endianness(),
|
||||
pid: process.pid,
|
||||
ppid: process.ppid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Node.js device information
|
||||
*/
|
||||
public getDeviceInfo(): NodeDeviceInfo {
|
||||
const cpus = os.cpus();
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
const userInfo = os.userInfo();
|
||||
|
||||
return {
|
||||
// System info
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
type: os.type(),
|
||||
release: os.release(),
|
||||
version: os.version(),
|
||||
hostname: os.hostname(),
|
||||
|
||||
// CPU info
|
||||
cpus: cpus.map(cpu => ({
|
||||
model: cpu.model,
|
||||
speed: cpu.speed,
|
||||
times: cpu.times
|
||||
})),
|
||||
|
||||
// Memory info
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
usedMemory: os.totalmem() - os.freemem(),
|
||||
|
||||
// Load info
|
||||
loadAverage: os.loadavg(),
|
||||
|
||||
// Network interfaces
|
||||
networkInterfaces: Object.fromEntries(
|
||||
Object.entries(networkInterfaces).map(([name, interfaces]) => [
|
||||
name,
|
||||
(interfaces || []).map(iface => ({
|
||||
address: iface.address,
|
||||
netmask: iface.netmask,
|
||||
family: iface.family as 'IPv4' | 'IPv6',
|
||||
mac: iface.mac,
|
||||
internal: iface.internal,
|
||||
cidr: iface.cidr,
|
||||
scopeid: iface.scopeid
|
||||
}))
|
||||
])
|
||||
),
|
||||
|
||||
// Process info
|
||||
process: {
|
||||
pid: process.pid,
|
||||
ppid: process.ppid,
|
||||
version: process.version,
|
||||
versions: process.versions,
|
||||
uptime: process.uptime()
|
||||
},
|
||||
|
||||
// User info
|
||||
userInfo: {
|
||||
uid: userInfo.uid,
|
||||
gid: userInfo.gid,
|
||||
username: userInfo.username,
|
||||
homedir: userInfo.homedir,
|
||||
shell: userInfo.shell
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SharedArrayBuffer maximum size limit
|
||||
*/
|
||||
private getMaxSharedArrayBufferSize(): number {
|
||||
const totalMemory = os.totalmem();
|
||||
// Limit to 50% of total system memory
|
||||
return Math.floor(totalMemory * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node.js Worker wrapper
|
||||
*/
|
||||
class NodeWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: Worker;
|
||||
private isTemporaryFile: boolean = false;
|
||||
private scriptPath: string;
|
||||
|
||||
constructor(script: string, options: WorkerCreationOptions = {}) {
|
||||
try {
|
||||
// Determine if script is a file path or script content
|
||||
if (this.isFilePath(script)) {
|
||||
// Use file path directly
|
||||
this.scriptPath = script;
|
||||
this.isTemporaryFile = false;
|
||||
} else {
|
||||
// Write script content to temporary file
|
||||
this.scriptPath = this.writeScriptToFile(script, options.name);
|
||||
this.isTemporaryFile = true;
|
||||
}
|
||||
|
||||
// Create Worker
|
||||
this.worker = new Worker(this.scriptPath, {
|
||||
// Node.js Worker options
|
||||
workerData: options.name ? { name: options.name } : undefined
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if string is a file path
|
||||
*/
|
||||
private isFilePath(script: string): boolean {
|
||||
// Check if it looks like a file path
|
||||
return (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.ts')) &&
|
||||
!script.includes('\n') &&
|
||||
!script.includes(';') &&
|
||||
script.length < 500; // File paths are typically not too long
|
||||
}
|
||||
|
||||
/**
|
||||
* Write script content to temporary file
|
||||
*/
|
||||
private writeScriptToFile(script: string, name?: string): string {
|
||||
const tmpDir = os.tmpdir();
|
||||
const fileName = name ? `worker-${name}-${Date.now()}.js` : `worker-${Date.now()}.js`;
|
||||
const filePath = path.join(tmpDir, fileName);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, script, 'utf8');
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to write Worker script file: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
|
||||
try {
|
||||
if (transfer && transfer.length > 0) {
|
||||
// Node.js Worker supports Transferable Objects
|
||||
this.worker.postMessage(message, transfer);
|
||||
} else {
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message to Node.js Worker: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.on('message', (data: any) => {
|
||||
handler({ data });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
this.worker.on('error', (error: Error) => {
|
||||
// Convert Error to ErrorEvent format
|
||||
const errorEvent = {
|
||||
message: error.message,
|
||||
filename: '',
|
||||
lineno: 0,
|
||||
colno: 0,
|
||||
error: error
|
||||
} as ErrorEvent;
|
||||
handler(errorEvent);
|
||||
});
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
try {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
|
||||
// Clean up temporary script file
|
||||
this.cleanupScriptFile();
|
||||
} catch (error) {
|
||||
console.error('Failed to terminate Node.js Worker:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary script file
|
||||
*/
|
||||
private cleanupScriptFile(): void {
|
||||
// Only clean up temporarily created files, not user-provided file paths
|
||||
if (this.scriptPath && this.isTemporaryFile) {
|
||||
try {
|
||||
fs.unlinkSync(this.scriptPath);
|
||||
} catch (error) {
|
||||
console.warn('Failed to clean up Worker script file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Copy the Code
|
||||
|
||||
Copy the above code to your project, e.g., `src/platform/NodeAdapter.ts`.
|
||||
|
||||
### 2. Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { NodeAdapter } from './platform/NodeAdapter';
|
||||
|
||||
// Check if in Node.js environment
|
||||
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
||||
const nodeAdapter = new NodeAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(nodeAdapter);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use WorkerEntitySystem
|
||||
|
||||
The Node.js adapter works with WorkerEntitySystem, and the framework automatically handles Worker script creation:
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
import * as os from 'os';
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: os.cpus().length, // Use all CPU cores
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: { gravity: 9.8 }
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6; // x, y, vx, vy, mass, radius
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
return {
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: 1,
|
||||
radius: 10
|
||||
};
|
||||
}
|
||||
|
||||
// This function is automatically serialized and executed in Worker
|
||||
protected workerProcess(entities, deltaTime, config) {
|
||||
return entities.map(entity => {
|
||||
// Apply gravity
|
||||
entity.vy += config.gravity * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
}
|
||||
|
||||
interface PhysicsData {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
radius: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get System Information
|
||||
|
||||
```typescript
|
||||
const manager = PlatformManager.getInstance();
|
||||
if (manager.hasAdapter()) {
|
||||
const adapter = manager.getAdapter();
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
|
||||
console.log('Node.js version:', deviceInfo.process?.version);
|
||||
console.log('CPU core count:', deviceInfo.cpus?.length);
|
||||
console.log('Total memory:', Math.round(deviceInfo.totalMemory! / 1024 / 1024), 'MB');
|
||||
console.log('Available memory:', Math.round(deviceInfo.freeMemory! / 1024 / 1024), 'MB');
|
||||
}
|
||||
```
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
Node.js Worker Threads related official documentation:
|
||||
|
||||
- [Worker Threads Official Docs](https://nodejs.org/api/worker_threads.html)
|
||||
- [SharedArrayBuffer Support](https://nodejs.org/api/globals.html#class-sharedarraybuffer)
|
||||
- [OS Module Docs](https://nodejs.org/api/os.html)
|
||||
- [Process Module Docs](https://nodejs.org/api/process.html)
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Worker Threads Requirements
|
||||
|
||||
- **Node.js Version**: Requires Node.js 10.5.0+ (12+ recommended)
|
||||
- **Module Type**: Supports both CommonJS and ES modules
|
||||
- **Thread Limit**: Theoretically unlimited, but recommended not to exceed 2x CPU core count
|
||||
|
||||
### Performance Optimization Tips
|
||||
|
||||
#### 1. Worker Pool Management
|
||||
|
||||
```typescript
|
||||
class ServerPhysicsSystem extends WorkerEntitySystem {
|
||||
constructor() {
|
||||
const cpuCount = os.cpus().length;
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: Math.min(cpuCount * 2, 16), // Max 16 Workers
|
||||
entitiesPerWorker: 1000, // 1000 entities per Worker
|
||||
useSharedArrayBuffer: true,
|
||||
systemConfig: {
|
||||
gravity: 9.8,
|
||||
timeStep: 1/60
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Memory Management
|
||||
|
||||
```typescript
|
||||
class MemoryMonitor {
|
||||
public static checkMemoryUsage(): void {
|
||||
const used = process.memoryUsage();
|
||||
|
||||
console.log('Memory usage:');
|
||||
console.log(` RSS: ${Math.round(used.rss / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB`);
|
||||
console.log(` Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB`);
|
||||
console.log(` External: ${Math.round(used.external / 1024 / 1024)} MB`);
|
||||
|
||||
// Trigger warning when memory usage is too high
|
||||
if (used.heapUsed > used.heapTotal * 0.9) {
|
||||
console.warn('Memory usage too high, consider optimizing or restarting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check memory usage periodically
|
||||
setInterval(() => {
|
||||
MemoryMonitor.checkMemoryUsage();
|
||||
}, 30000); // Check every 30 seconds
|
||||
```
|
||||
|
||||
#### 3. Server Environment Optimization
|
||||
|
||||
```typescript
|
||||
// Set process title
|
||||
process.title = 'ecs-game-server';
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Promise rejection:', reason);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('Received SIGTERM signal, shutting down server...');
|
||||
// Clean up resources
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check Node.js environment support
|
||||
const adapter = new NodeAdapter();
|
||||
console.log('Node.js version:', adapter.version);
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('SharedArrayBuffer support:', adapter.isSharedArrayBufferSupported());
|
||||
console.log('CPU core count:', adapter.getHardwareConcurrency());
|
||||
|
||||
// Get detailed configuration
|
||||
const config = adapter.getPlatformConfig();
|
||||
console.log('Platform config:', JSON.stringify(config, null, 2));
|
||||
|
||||
// System resource monitoring
|
||||
const deviceInfo = adapter.getDeviceInfo();
|
||||
console.log('System load:', deviceInfo.loadAverage);
|
||||
console.log('Network interfaces:', Object.keys(deviceInfo.networkInterfaces!));
|
||||
```
|
||||
@@ -0,0 +1,485 @@
|
||||
---
|
||||
title: "WeChat Mini Game Adapter"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The WeChat Mini Game platform adapter is designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
|
||||
|
||||
## Feature Support
|
||||
|
||||
| Feature | Support | Notes |
|
||||
|---------|---------|-------|
|
||||
| **Worker** | Supported | Requires precompiled file, configure `workerScriptPath` |
|
||||
| **SharedArrayBuffer** | Not Supported | WeChat Mini Game environment doesn't support this |
|
||||
| **Transferable Objects** | Not Supported | Only serializable objects supported |
|
||||
| **High-Resolution Time** | Supported | Uses `wx.getPerformance()` |
|
||||
| **Device Info** | Supported | Complete WeChat Mini Game device info |
|
||||
|
||||
## WorkerEntitySystem Usage
|
||||
|
||||
### Important: WeChat Mini Game Worker Restrictions
|
||||
|
||||
WeChat Mini Game Workers have the following restrictions:
|
||||
- **Worker scripts must be in the code package**, cannot be dynamically generated
|
||||
- **Must be configured in `game.json`** with `workers` directory
|
||||
- **Maximum of 1 Worker** can be created
|
||||
|
||||
Therefore, when using `WorkerEntitySystem`, there are two approaches:
|
||||
1. **Recommended: Use CLI tool** to automatically generate Worker files
|
||||
2. Manually create Worker files
|
||||
|
||||
### Method 1: Use CLI Tool for Auto-Generation (Recommended)
|
||||
|
||||
We provide the `@esengine/worker-generator` tool that can automatically extract `workerProcess` functions from your TypeScript code and generate WeChat Mini Game compatible Worker files.
|
||||
|
||||
#### Installation
|
||||
|
||||
```bash
|
||||
pnpm add -D @esengine/worker-generator
|
||||
# or
|
||||
npm install --save-dev @esengine/worker-generator
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
# Scan src directory, generate Worker files to workers directory
|
||||
npx esengine-worker-gen --src ./src --out ./workers --wechat
|
||||
|
||||
# View help
|
||||
npx esengine-worker-gen --help
|
||||
```
|
||||
|
||||
#### Parameter Reference
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `-s, --src <dir>` | Source code directory | `./src` |
|
||||
| `-o, --out <dir>` | Output directory | `./workers` |
|
||||
| `-w, --wechat` | Generate WeChat Mini Game compatible code | `false` |
|
||||
| `-m, --mapping` | Generate worker-mapping.json | `true` |
|
||||
| `-t, --tsconfig <path>` | TypeScript config file path | Auto-detect |
|
||||
| `-v, --verbose` | Verbose output | `false` |
|
||||
|
||||
#### Example Output
|
||||
|
||||
```
|
||||
ESEngine Worker Generator
|
||||
|
||||
Source directory: /project/src
|
||||
Output directory: /project/workers
|
||||
WeChat mode: Yes
|
||||
|
||||
Scanning for WorkerEntitySystem classes...
|
||||
|
||||
Found 1 WorkerEntitySystem class(es):
|
||||
- PhysicsSystem (src/systems/PhysicsSystem.ts)
|
||||
|
||||
Generating Worker files...
|
||||
|
||||
Successfully generated 1 Worker file(s):
|
||||
- PhysicsSystem -> workers/physics-system-worker.js
|
||||
|
||||
Usage:
|
||||
1. Copy the generated files to your project's workers/ directory
|
||||
2. Configure game.json (WeChat): { "workers": "workers" }
|
||||
3. In your System constructor, add:
|
||||
workerScriptPath: 'workers/physics-system-worker.js'
|
||||
```
|
||||
|
||||
#### Integration in Build Process
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
|
||||
"build": "pnpm build:workers && your-build-command"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: Manually Create Worker Files
|
||||
|
||||
If you don't want to use the CLI tool, you can manually create Worker files.
|
||||
|
||||
Create `workers/entity-worker.js` in your project:
|
||||
|
||||
```javascript
|
||||
// workers/entity-worker.js
|
||||
// WeChat Mini Game WorkerEntitySystem Generic Worker Template
|
||||
|
||||
let sharedFloatArray = null;
|
||||
|
||||
worker.onMessage(function(e) {
|
||||
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
|
||||
|
||||
try {
|
||||
// Handle SharedArrayBuffer initialization
|
||||
if (type === 'init' && sharedBuffer) {
|
||||
sharedFloatArray = new Float32Array(sharedBuffer);
|
||||
worker.postMessage({ type: 'init', success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle SharedArrayBuffer data
|
||||
if (type === 'shared' && sharedFloatArray) {
|
||||
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
|
||||
worker.postMessage({ id, result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// Traditional processing method
|
||||
if (entities) {
|
||||
const result = workerProcess(entities, deltaTime, systemConfig);
|
||||
|
||||
// Handle Promise return value
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function(finalResult) {
|
||||
worker.postMessage({ id, result: finalResult });
|
||||
}).catch(function(error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
});
|
||||
} else {
|
||||
worker.postMessage({ id, result: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
worker.postMessage({ id, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Entity processing function - Modify this function based on your business logic
|
||||
* @param {Array} entities - Entity data array
|
||||
* @param {number} deltaTime - Frame interval time
|
||||
* @param {Object} systemConfig - System configuration
|
||||
* @returns {Array} Processed entity data
|
||||
*/
|
||||
function workerProcess(entities, deltaTime, systemConfig) {
|
||||
// ====== Write your processing logic here ======
|
||||
// Example: Physics calculation
|
||||
return entities.map(function(entity) {
|
||||
// Apply gravity
|
||||
entity.vy += (systemConfig.gravity || 100) * deltaTime;
|
||||
|
||||
// Update position
|
||||
entity.x += entity.vx * deltaTime;
|
||||
entity.y += entity.vy * deltaTime;
|
||||
|
||||
// Apply friction
|
||||
entity.vx *= (systemConfig.friction || 0.95);
|
||||
entity.vy *= (systemConfig.friction || 0.95);
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedArrayBuffer processing function (optional)
|
||||
*/
|
||||
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
|
||||
if (!sharedFloatArray) return;
|
||||
|
||||
// ====== Implement SharedArrayBuffer processing logic as needed ======
|
||||
// Note: WeChat Mini Game doesn't support SharedArrayBuffer, this function typically won't be called
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Configure game.json
|
||||
|
||||
Add workers configuration in `game.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceOrientation": "portrait",
|
||||
"showStatusBar": false,
|
||||
"workers": "workers"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Use WorkerEntitySystem
|
||||
|
||||
```typescript
|
||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
interface PhysicsData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: true,
|
||||
workerCount: 1, // WeChat Mini Game limits to 1 Worker
|
||||
workerScriptPath: 'workers/entity-worker.js', // Specify precompiled Worker file
|
||||
systemConfig: {
|
||||
gravity: 100,
|
||||
friction: 0.95
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getDefaultEntityDataSize(): number {
|
||||
return 6;
|
||||
}
|
||||
|
||||
protected extractEntityData(entity: Entity): PhysicsData {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
const physics = entity.getComponent(Physics);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
x: transform.x,
|
||||
y: transform.y,
|
||||
vx: velocity.x,
|
||||
vy: velocity.y,
|
||||
mass: physics.mass
|
||||
};
|
||||
}
|
||||
|
||||
// Note: In WeChat Mini Game, this method won't be used
|
||||
// Worker processing logic is in workers/entity-worker.js workerProcess function
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
protected applyResult(entity: Entity, result: PhysicsData): void {
|
||||
const transform = entity.getComponent(Transform);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
transform.x = result.x;
|
||||
transform.y = result.y;
|
||||
velocity.x = result.vx;
|
||||
velocity.y = result.vy;
|
||||
}
|
||||
|
||||
// SharedArrayBuffer related methods (not supported in WeChat Mini Game, can be omitted)
|
||||
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
|
||||
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
|
||||
}
|
||||
```
|
||||
|
||||
### Temporarily Disable Worker (Fallback to Sync Mode)
|
||||
|
||||
If you encounter issues, you can temporarily disable Worker:
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Velocity), {
|
||||
enableWorker: false, // Disable Worker, use main thread synchronous processing
|
||||
// ... other config
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Adapter Implementation
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* WeChat Mini Game platform adapter
|
||||
*/
|
||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
||||
public readonly name = 'wechat-minigame';
|
||||
public readonly version: string;
|
||||
private systemInfo: any;
|
||||
|
||||
constructor() {
|
||||
this.systemInfo = this.getSystemInfo();
|
||||
this.version = this.systemInfo.SDKVersion || 'unknown';
|
||||
}
|
||||
|
||||
public isWorkerSupported(): boolean {
|
||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
||||
}
|
||||
|
||||
public isSharedArrayBufferSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getHardwareConcurrency(): number {
|
||||
return 1; // WeChat Mini Game max 1 Worker
|
||||
}
|
||||
|
||||
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
||||
if (!this.isWorkerSupported()) {
|
||||
throw new Error('WeChat Mini Game environment does not support Worker');
|
||||
}
|
||||
|
||||
// scriptPath must be a file path in the code package
|
||||
const worker = wx.createWorker(scriptPath, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
|
||||
return new WeChatWorker(worker);
|
||||
}
|
||||
|
||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getHighResTimestamp(): number {
|
||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
||||
return wx.getPerformance().now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
public getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: false,
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true, // Important: Mark that dynamic scripts not supported
|
||||
requiresWorkerInit: false,
|
||||
memoryLimit: 512 * 1024 * 1024,
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'Maximum of 1 Worker can be created',
|
||||
'Worker scripts must be in the code package',
|
||||
'workers path must be configured in game.json',
|
||||
'workerScriptPath configuration required'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
sdkVersion: this.systemInfo.SDKVersion
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getSystemInfo(): any {
|
||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
||||
try {
|
||||
return wx.getSystemInfoSync();
|
||||
} catch (error) {
|
||||
console.warn('Failed to get WeChat system info:', error);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WeChat Worker wrapper
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
private worker: any;
|
||||
|
||||
constructor(worker: any) {
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
public get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public postMessage(message: any, transfer?: Transferable[]): void {
|
||||
if (this._state === 'terminated') {
|
||||
throw new Error('Worker has been terminated');
|
||||
}
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
|
||||
public onMessage(handler: (event: { data: any }) => void): void {
|
||||
this.worker.onMessage((res: any) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
public onError(handler: (error: ErrorEvent) => void): void {
|
||||
if (this.worker.onError) {
|
||||
this.worker.onError(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public terminate(): void {
|
||||
if (this._state === 'running') {
|
||||
this.worker.terminate();
|
||||
this._state = 'terminated';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register the Adapter
|
||||
|
||||
```typescript
|
||||
import { PlatformManager } from '@esengine/ecs-framework';
|
||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||
|
||||
// Register adapter at game startup
|
||||
if (typeof wx !== 'undefined') {
|
||||
const adapter = new WeChatMiniGameAdapter();
|
||||
PlatformManager.getInstance().registerAdapter(adapter);
|
||||
}
|
||||
```
|
||||
|
||||
## Official Documentation Reference
|
||||
|
||||
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
|
||||
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
|
||||
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Worker Restrictions
|
||||
|
||||
| Restriction | Description |
|
||||
|-------------|-------------|
|
||||
| Quantity Limit | Maximum of 1 Worker can be created |
|
||||
| Version Requirement | Requires base library 1.9.90 or above |
|
||||
| Script Location | Must be in code package, dynamic generation not supported |
|
||||
| Lifecycle | Must terminate() before creating new Worker |
|
||||
|
||||
### Memory Limits
|
||||
|
||||
- Typically limited to 256MB - 512MB
|
||||
- Need to release unused resources promptly
|
||||
- Recommend listening for memory warnings:
|
||||
|
||||
```typescript
|
||||
wx.onMemoryWarning(() => {
|
||||
console.warn('Received memory warning, starting resource cleanup');
|
||||
// Clean up unnecessary resources
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```typescript
|
||||
// Check Worker configuration
|
||||
const adapter = PlatformManager.getInstance().getAdapter();
|
||||
const config = adapter.getPlatformConfig();
|
||||
|
||||
console.log('Worker support:', adapter.isWorkerSupported());
|
||||
console.log('Max Worker count:', config.maxWorkerCount);
|
||||
console.log('Platform limitations:', config.limitations);
|
||||
```
|
||||
645
docs/src/content/docs/en/guide/plugin-system.md
Normal file
645
docs/src/content/docs/en/guide/plugin-system.md
Normal file
@@ -0,0 +1,645 @@
|
||||
---
|
||||
title: "Plugin System"
|
||||
---
|
||||
|
||||
The plugin system allows you to extend ECS Framework functionality in a modular way. Through plugins, you can encapsulate specific features (such as network synchronization, physics engines, debugging tools, etc.) and reuse them across multiple projects.
|
||||
|
||||
## Overview
|
||||
|
||||
### What is a Plugin
|
||||
|
||||
A plugin is a class that implements the `IPlugin` interface and can be dynamically installed into the framework at runtime. Plugins can:
|
||||
|
||||
- Register custom services to the service container
|
||||
- Add systems to scenes
|
||||
- Register custom components
|
||||
- Extend framework functionality
|
||||
|
||||
### Advantages of Plugins
|
||||
|
||||
- **Modular**: Encapsulate functionality as independent modules, improving code maintainability
|
||||
- **Reusable**: Same plugin can be used across multiple projects
|
||||
- **Decoupled**: Core framework separated from extended functionality
|
||||
- **Hot-swappable**: Dynamically install and uninstall plugins at runtime
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Creating Your First Plugin
|
||||
|
||||
Create a simple debug plugin:
|
||||
|
||||
```typescript
|
||||
import { IPlugin, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class DebugPlugin implements IPlugin {
|
||||
readonly name = 'debug-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
console.log('Debug plugin installed');
|
||||
|
||||
// Can register services, add systems, etc. here
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
console.log('Debug plugin uninstalled');
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Installing a Plugin
|
||||
|
||||
Use `Core.installPlugin()` to install a plugin:
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// Initialize Core
|
||||
Core.create({ debug: true });
|
||||
|
||||
// Install plugin
|
||||
await Core.installPlugin(new DebugPlugin());
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('debug-plugin')) {
|
||||
console.log('Debug plugin is running');
|
||||
}
|
||||
```
|
||||
|
||||
### Uninstalling a Plugin
|
||||
|
||||
```typescript
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('debug-plugin');
|
||||
```
|
||||
|
||||
### Getting Plugin Instance
|
||||
|
||||
```typescript
|
||||
// Get installed plugin
|
||||
const plugin = Core.getPlugin('debug-plugin');
|
||||
if (plugin) {
|
||||
console.log(`Plugin version: ${plugin.version}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Development
|
||||
|
||||
### IPlugin Interface
|
||||
|
||||
All plugins must implement the `IPlugin` interface:
|
||||
|
||||
```typescript
|
||||
export interface IPlugin {
|
||||
// Unique plugin name
|
||||
readonly name: string;
|
||||
|
||||
// Plugin version (semver recommended)
|
||||
readonly version: string;
|
||||
|
||||
// Dependencies on other plugins (optional)
|
||||
readonly dependencies?: readonly string[];
|
||||
|
||||
// Called when plugin is installed
|
||||
install(core: Core, services: ServiceContainer): void | Promise<void>;
|
||||
|
||||
// Called when plugin is uninstalled
|
||||
uninstall(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
#### install Method
|
||||
|
||||
Called when the plugin is installed, used to initialize the plugin:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// 1. Register services
|
||||
services.registerSingleton(MyService);
|
||||
|
||||
// 2. Access current scene
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
// 3. Add systems
|
||||
scene.addSystem(new MySystem());
|
||||
}
|
||||
|
||||
// 4. Other initialization logic
|
||||
console.log('Plugin initialized');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### uninstall Method
|
||||
|
||||
Called when the plugin is uninstalled, used to clean up resources:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private myService?: MyService;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
this.myService = new MyService();
|
||||
services.registerInstance(MyService, this.myService);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clean up service
|
||||
if (this.myService) {
|
||||
this.myService.dispose();
|
||||
this.myService = undefined;
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
// Release other resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Plugins
|
||||
|
||||
Plugin `install` and `uninstall` methods both support async:
|
||||
|
||||
```typescript
|
||||
class AsyncPlugin implements IPlugin {
|
||||
readonly name = 'async-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
// Async resource loading
|
||||
const config = await fetch('/plugin-config.json').then(r => r.json());
|
||||
|
||||
// Initialize service with loaded config
|
||||
const service = new MyService(config);
|
||||
services.registerInstance(MyService, service);
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// Async cleanup
|
||||
await this.saveState();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
// Save plugin state
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
await Core.installPlugin(new AsyncPlugin());
|
||||
```
|
||||
|
||||
### Registering Services
|
||||
|
||||
Plugins can register their own services to the service container:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkService implements IService {
|
||||
connect(url: string) {
|
||||
console.log(`Connecting to ${url}`);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('Network service disposed');
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkService);
|
||||
|
||||
// Resolve and use service
|
||||
const network = services.resolve(NetworkService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service container automatically calls service's dispose method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Systems
|
||||
|
||||
Plugins can add custom systems to scenes:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PhysicsBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Physics simulation logic
|
||||
}
|
||||
}
|
||||
|
||||
class PhysicsPlugin implements IPlugin {
|
||||
readonly name = 'physics-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private physicsSystem?: PhysicsSystem;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const scene = core.scene;
|
||||
if (scene) {
|
||||
this.physicsSystem = new PhysicsSystem();
|
||||
scene.addSystem(this.physicsSystem);
|
||||
}
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Remove system
|
||||
if (this.physicsSystem) {
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.removeSystem(this.physicsSystem);
|
||||
}
|
||||
this.physicsSystem = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Declaring Dependencies
|
||||
|
||||
Plugins can declare dependencies on other plugins:
|
||||
|
||||
```typescript
|
||||
class AdvancedPhysicsPlugin implements IPlugin {
|
||||
readonly name = 'advanced-physics';
|
||||
readonly version = '2.0.0';
|
||||
|
||||
// Declare dependency on base physics plugin
|
||||
readonly dependencies = ['physics-plugin'] as const;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Can safely use services provided by physics-plugin
|
||||
const physicsService = services.resolve(PhysicsService);
|
||||
// ...
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Checking
|
||||
|
||||
The framework automatically checks dependency relationships and throws errors if dependencies are unmet:
|
||||
|
||||
```typescript
|
||||
// Error: physics-plugin not installed
|
||||
try {
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
} catch (error) {
|
||||
console.error(error); // Plugin advanced-physics has unmet dependencies: physics-plugin
|
||||
}
|
||||
|
||||
// Correct: Install dependency first
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
```
|
||||
|
||||
### Uninstall Order
|
||||
|
||||
The framework checks dependency relationships, preventing uninstallation of plugins that other plugins depend on:
|
||||
|
||||
```typescript
|
||||
await Core.installPlugin(new PhysicsPlugin());
|
||||
await Core.installPlugin(new AdvancedPhysicsPlugin());
|
||||
|
||||
// Error: physics-plugin is required by advanced-physics
|
||||
try {
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
} catch (error) {
|
||||
console.error(error); // Cannot uninstall plugin physics-plugin: it is required by advanced-physics
|
||||
}
|
||||
|
||||
// Correct: Uninstall dependent plugin first
|
||||
await Core.uninstallPlugin('advanced-physics');
|
||||
await Core.uninstallPlugin('physics-plugin');
|
||||
```
|
||||
|
||||
## Plugin Management
|
||||
|
||||
### Managing via Core
|
||||
|
||||
The Core class provides convenient plugin management methods:
|
||||
|
||||
```typescript
|
||||
// Install plugin
|
||||
await Core.installPlugin(myPlugin);
|
||||
|
||||
// Uninstall plugin
|
||||
await Core.uninstallPlugin('plugin-name');
|
||||
|
||||
// Check if plugin is installed
|
||||
if (Core.isPluginInstalled('plugin-name')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Get plugin instance
|
||||
const plugin = Core.getPlugin('plugin-name');
|
||||
```
|
||||
|
||||
### Managing via PluginManager
|
||||
|
||||
You can also use the PluginManager service directly:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// Get all plugins
|
||||
const allPlugins = pluginManager.getAllPlugins();
|
||||
console.log(`Total plugins: ${allPlugins.length}`);
|
||||
|
||||
// Get plugin metadata
|
||||
const metadata = pluginManager.getMetadata('my-plugin');
|
||||
if (metadata) {
|
||||
console.log(`State: ${metadata.state}`);
|
||||
console.log(`Installed at: ${new Date(metadata.installedAt!)}`);
|
||||
}
|
||||
|
||||
// Get all plugin metadata
|
||||
const allMetadata = pluginManager.getAllMetadata();
|
||||
for (const meta of allMetadata) {
|
||||
console.log(`${meta.name} v${meta.version} - ${meta.state}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Practical Plugin Examples
|
||||
|
||||
### Network Sync Plugin
|
||||
|
||||
```typescript
|
||||
import { IPlugin, IService, Core, ServiceContainer } from '@esengine/ecs-framework';
|
||||
|
||||
class NetworkSyncService implements IService {
|
||||
private ws?: WebSocket;
|
||||
|
||||
connect(url: string) {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
};
|
||||
}
|
||||
|
||||
private handleMessage(data: any) {
|
||||
// Handle network messages
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkSyncPlugin implements IPlugin {
|
||||
readonly name = 'network-sync';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Register network service
|
||||
services.registerSingleton(NetworkSyncService);
|
||||
|
||||
// Auto connect
|
||||
const network = services.resolve(NetworkSyncService);
|
||||
network.connect('ws://localhost:8080');
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Service will auto dispose
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Analysis Plugin
|
||||
|
||||
```typescript
|
||||
class PerformanceAnalysisPlugin implements IPlugin {
|
||||
readonly name = 'performance-analysis';
|
||||
readonly version = '1.0.0';
|
||||
private frameCount = 0;
|
||||
private totalTime = 0;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const monitor = services.resolve(PerformanceMonitor);
|
||||
monitor.enable();
|
||||
|
||||
// Periodically output performance report
|
||||
const timer = services.resolve(TimerManager);
|
||||
timer.schedule(5.0, true, null, () => {
|
||||
this.printReport(monitor);
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
|
||||
private printReport(monitor: PerformanceMonitor) {
|
||||
console.log('=== Performance Report ===');
|
||||
console.log(`FPS: ${monitor.getFPS()}`);
|
||||
console.log(`Memory: ${monitor.getMemoryUsage()} MB`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- Plugin names use lowercase letters and hyphens: `my-awesome-plugin`
|
||||
- Version numbers follow semantic versioning: `1.0.0`
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-awesome-plugin'; // Good
|
||||
readonly version = '1.0.0'; // Good
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Cleanup
|
||||
|
||||
Always clean up all resources created by the plugin in `uninstall`:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
private timerId?: number;
|
||||
private listener?: () => void;
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
// Add timer
|
||||
this.timerId = setInterval(() => {
|
||||
// ...
|
||||
}, 1000);
|
||||
|
||||
// Add event listener
|
||||
this.listener = () => {};
|
||||
window.addEventListener('resize', this.listener);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Clean up timer
|
||||
if (this.timerId) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = undefined;
|
||||
}
|
||||
|
||||
// Remove event listener
|
||||
if (this.listener) {
|
||||
window.removeEventListener('resize', this.listener);
|
||||
this.listener = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Handle errors properly in plugins to avoid affecting the entire application:
|
||||
|
||||
```typescript
|
||||
class MyPlugin implements IPlugin {
|
||||
readonly name = 'my-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
async install(core: Core, services: ServiceContainer): Promise<void> {
|
||||
try {
|
||||
// Operation that might fail
|
||||
await this.loadConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to load plugin config:', error);
|
||||
throw error; // Re-throw to let framework know installation failed
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup plugin:', error);
|
||||
// Cleanup failure shouldn't block uninstall
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig() {
|
||||
// Load configuration
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Allow users to configure plugin behavior:
|
||||
|
||||
```typescript
|
||||
interface NetworkPluginConfig {
|
||||
serverUrl: string;
|
||||
autoReconnect: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
class NetworkPlugin implements IPlugin {
|
||||
readonly name = 'network-plugin';
|
||||
readonly version = '1.0.0';
|
||||
|
||||
constructor(private config: NetworkPluginConfig) {}
|
||||
|
||||
install(core: Core, services: ServiceContainer): void {
|
||||
const network = new NetworkService(this.config);
|
||||
services.registerInstance(NetworkService, network);
|
||||
}
|
||||
|
||||
uninstall(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const plugin = new NetworkPlugin({
|
||||
serverUrl: 'ws://localhost:8080',
|
||||
autoReconnect: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await Core.installPlugin(plugin);
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Plugin Installation Failed
|
||||
|
||||
**Problem**: Plugin throws error during installation
|
||||
|
||||
**Causes**:
|
||||
- Dependencies not met
|
||||
- Exception in install method
|
||||
- Service registration conflict
|
||||
|
||||
**Solutions**:
|
||||
1. Check if dependencies are installed
|
||||
2. Check error logs
|
||||
3. Ensure service names don't conflict
|
||||
|
||||
### Plugin Still Has Side Effects After Uninstall
|
||||
|
||||
**Problem**: After uninstalling plugin, plugin functionality is still running
|
||||
|
||||
**Cause**: Resources not properly cleaned up in uninstall method
|
||||
|
||||
**Solution**: Ensure cleanup in uninstall:
|
||||
- Timers
|
||||
- Event listeners
|
||||
- WebSocket connections
|
||||
- System references
|
||||
|
||||
### When to Use Plugins
|
||||
|
||||
**Good for plugins**:
|
||||
- Optional features (debug tools, performance analysis)
|
||||
- Third-party integrations (network libraries, physics engines)
|
||||
- Functionality modules reused across projects
|
||||
|
||||
**Not suitable for plugins**:
|
||||
- Core game logic
|
||||
- Simple utility classes
|
||||
- Project-specific features
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Service Container](./service-container/) - Using service container in plugins
|
||||
- [System Architecture](./system/) - Adding systems in plugins
|
||||
- [Quick Start](./getting-started/) - Core initialization and basic usage
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
title: "scene-manager"
|
||||
---
|
||||
|
||||
# SceneManager
|
||||
|
||||
SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading.
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
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.
|
||||
165
docs/src/content/docs/en/guide/serialization/decorators.md
Normal file
165
docs/src/content/docs/en/guide/serialization/decorators.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Decorators & Inheritance"
|
||||
description: "Advanced serialization decorator usage and component inheritance"
|
||||
---
|
||||
|
||||
## Advanced Decorators
|
||||
|
||||
### Field Serialization Options
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Advanced')
|
||||
@Serializable({ version: 1 })
|
||||
class AdvancedComponent extends Component {
|
||||
// Use alias
|
||||
@Serialize({ alias: 'playerName' })
|
||||
public name: string = '';
|
||||
|
||||
// Custom serializer
|
||||
@Serialize({
|
||||
serializer: (value: Date) => value.toISOString(),
|
||||
deserializer: (value: string) => new Date(value)
|
||||
})
|
||||
public createdAt: Date = new Date();
|
||||
|
||||
// Ignore serialization
|
||||
@IgnoreSerialization()
|
||||
public cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Type Serialization
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Collections')
|
||||
@Serializable({ version: 1 })
|
||||
class CollectionsComponent extends Component {
|
||||
// Map serialization
|
||||
@SerializeAsMap()
|
||||
public inventory: Map<string, number> = new Map();
|
||||
|
||||
// Set serialization
|
||||
@SerializeAsSet()
|
||||
public acquiredSkills: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inventory.set('gold', 100);
|
||||
this.inventory.set('silver', 50);
|
||||
this.acquiredSkills.add('attack');
|
||||
this.acquiredSkills.add('defense');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component Inheritance and Serialization
|
||||
|
||||
The framework fully supports component class inheritance. Subclasses automatically inherit parent class serialization fields while adding their own.
|
||||
|
||||
### Basic Inheritance
|
||||
|
||||
```typescript
|
||||
// Base component
|
||||
@ECSComponent('Collider2DBase')
|
||||
@Serializable({ version: 1, typeId: 'Collider2DBase' })
|
||||
abstract class Collider2DBase extends Component {
|
||||
@Serialize()
|
||||
public friction: number = 0.5;
|
||||
|
||||
@Serialize()
|
||||
public restitution: number = 0.0;
|
||||
|
||||
@Serialize()
|
||||
public isTrigger: boolean = false;
|
||||
}
|
||||
|
||||
// Subclass component - automatically inherits parent's serialization fields
|
||||
@ECSComponent('BoxCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public width: number = 1.0;
|
||||
|
||||
@Serialize()
|
||||
public height: number = 1.0;
|
||||
}
|
||||
|
||||
// Another subclass component
|
||||
@ECSComponent('CircleCollider2D')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase {
|
||||
@Serialize()
|
||||
public radius: number = 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
### Inheritance Rules
|
||||
|
||||
1. **Field Inheritance**: Subclasses automatically inherit all `@Serialize()` marked fields from parent
|
||||
2. **Independent Metadata**: Each subclass maintains independent serialization metadata; modifying subclass doesn't affect parent or other subclasses
|
||||
3. **typeId Distinction**: Use `typeId` option to specify unique identifier for each class, ensuring correct component type recognition during deserialization
|
||||
|
||||
### Importance of Using typeId
|
||||
|
||||
When using component inheritance, it's **strongly recommended** to set a unique `typeId` for each class:
|
||||
|
||||
```typescript
|
||||
// ✅ Recommended: Explicitly specify typeId
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
|
||||
class CircleCollider2DComponent extends Collider2DBase { }
|
||||
|
||||
// ⚠️ Not recommended: Relying on class name as typeId
|
||||
// Class names may change after code minification, causing deserialization failure
|
||||
@Serializable({ version: 1 })
|
||||
class BoxCollider2DComponent extends Collider2DBase { }
|
||||
```
|
||||
|
||||
### Subclass Overriding Parent Fields
|
||||
|
||||
Subclasses can redeclare parent fields to modify their serialization options:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('SpecialCollider')
|
||||
@Serializable({ version: 1, typeId: 'SpecialCollider' })
|
||||
class SpecialColliderComponent extends Collider2DBase {
|
||||
// Override parent field with different alias
|
||||
@Serialize({ alias: 'fric' })
|
||||
public override friction: number = 0.8;
|
||||
|
||||
@Serialize()
|
||||
public specialProperty: string = '';
|
||||
}
|
||||
```
|
||||
|
||||
### Ignoring Inherited Fields
|
||||
|
||||
Use `@IgnoreSerialization()` to ignore fields inherited from parent in subclass:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TriggerOnly')
|
||||
@Serializable({ version: 1, typeId: 'TriggerOnly' })
|
||||
class TriggerOnlyCollider extends Collider2DBase {
|
||||
// Ignore parent's friction and restitution fields
|
||||
// Because Trigger doesn't need physics material properties
|
||||
@IgnoreSerialization()
|
||||
public override friction: number = 0;
|
||||
|
||||
@IgnoreSerialization()
|
||||
public override restitution: number = 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Decorator Reference
|
||||
|
||||
| Decorator | Description |
|
||||
|-----------|-------------|
|
||||
| `@Serializable({ version, typeId })` | Mark component as serializable |
|
||||
| `@Serialize()` | Mark field as serializable |
|
||||
| `@Serialize({ alias })` | Serialize field with alias |
|
||||
| `@Serialize({ serializer, deserializer })` | Custom serialization logic |
|
||||
| `@SerializeAsMap()` | Serialize Map type |
|
||||
| `@SerializeAsSet()` | Serialize Set type |
|
||||
| `@IgnoreSerialization()` | Ignore field serialization |
|
||||
228
docs/src/content/docs/en/guide/serialization/incremental.md
Normal file
228
docs/src/content/docs/en/guide/serialization/incremental.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "Incremental Serialization"
|
||||
description: "Only serialize changes in the scene"
|
||||
---
|
||||
|
||||
Incremental serialization only saves the changed parts of a scene, suitable for network synchronization, undo/redo, time rewinding, and other scenarios requiring frequent state saving.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Create Base Snapshot
|
||||
|
||||
```typescript
|
||||
// Create base snapshot before starting to record changes
|
||||
scene.createIncrementalSnapshot();
|
||||
```
|
||||
|
||||
### 2. Modify Scene
|
||||
|
||||
```typescript
|
||||
// Add entity
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new PositionComponent(100, 200));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
|
||||
// Modify component
|
||||
const player = scene.findEntity('Player');
|
||||
const pos = player.getComponent(PositionComponent);
|
||||
pos.x = 300;
|
||||
pos.y = 400;
|
||||
|
||||
// Remove component
|
||||
player.removeComponentByType(BuffComponent);
|
||||
|
||||
// Delete entity
|
||||
const oldEntity = scene.findEntity('ToDelete');
|
||||
oldEntity.destroy();
|
||||
|
||||
// Modify scene data
|
||||
scene.sceneData.set('score', 1000);
|
||||
```
|
||||
|
||||
### 3. Get Incremental Changes
|
||||
|
||||
```typescript
|
||||
// Get all changes relative to base snapshot
|
||||
const incremental = scene.serializeIncremental();
|
||||
|
||||
// View change statistics
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
console.log('Total changes:', stats.totalChanges);
|
||||
console.log('Added entities:', stats.addedEntities);
|
||||
console.log('Removed entities:', stats.removedEntities);
|
||||
console.log('Added components:', stats.addedComponents);
|
||||
console.log('Updated components:', stats.updatedComponents);
|
||||
```
|
||||
|
||||
### 4. Serialize Incremental Data
|
||||
|
||||
```typescript
|
||||
// JSON format (default)
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json'
|
||||
});
|
||||
|
||||
// Binary format (smaller size, higher performance)
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// Pretty print JSON output (for debugging)
|
||||
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'json',
|
||||
pretty: true
|
||||
});
|
||||
|
||||
// Send or save
|
||||
socket.send(binaryData); // Use binary for network transmission
|
||||
localStorage.setItem('changes', jsonData); // JSON for local storage
|
||||
```
|
||||
|
||||
### 5. Apply Incremental Changes
|
||||
|
||||
```typescript
|
||||
// Apply changes to another scene
|
||||
const otherScene = new Scene();
|
||||
|
||||
// Directly apply incremental object
|
||||
otherScene.applyIncremental(incremental);
|
||||
|
||||
// Apply from JSON string
|
||||
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
|
||||
otherScene.applyIncremental(jsonData);
|
||||
|
||||
// Apply from binary Uint8Array
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
|
||||
otherScene.applyIncremental(binaryData);
|
||||
```
|
||||
|
||||
## Incremental Snapshot Management
|
||||
|
||||
### Update Snapshot Base
|
||||
|
||||
After applying incremental changes, you can update the snapshot base:
|
||||
|
||||
```typescript
|
||||
// Create initial snapshot
|
||||
scene.createIncrementalSnapshot();
|
||||
|
||||
// First modification
|
||||
entity.addComponent(new VelocityComponent(5, 0));
|
||||
const incremental1 = scene.serializeIncremental();
|
||||
|
||||
// Update base (set current state as new base)
|
||||
scene.updateIncrementalSnapshot();
|
||||
|
||||
// Second modification (incremental will be based on updated base)
|
||||
entity.getComponent(VelocityComponent).dx = 10;
|
||||
const incremental2 = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
### Clear Snapshot
|
||||
|
||||
```typescript
|
||||
// Release memory used by snapshot
|
||||
scene.clearIncrementalSnapshot();
|
||||
|
||||
// Check if snapshot exists
|
||||
if (scene.hasIncrementalSnapshot()) {
|
||||
console.log('Incremental snapshot exists');
|
||||
}
|
||||
```
|
||||
|
||||
## Incremental Serialization Options
|
||||
|
||||
```typescript
|
||||
interface IncrementalSerializationOptions {
|
||||
// Whether to perform deep comparison of component data
|
||||
// Default true, set to false to improve performance but may miss internal field changes
|
||||
deepComponentComparison?: boolean;
|
||||
|
||||
// Whether to track scene data changes
|
||||
// Default true
|
||||
trackSceneData?: boolean;
|
||||
|
||||
// Whether to compress snapshot (using JSON serialization)
|
||||
// Default false
|
||||
compressSnapshot?: boolean;
|
||||
|
||||
// Serialization format
|
||||
// 'json': JSON format (readable, convenient for debugging)
|
||||
// 'binary': MessagePack binary format (smaller, higher performance)
|
||||
// Default 'json'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// Whether to pretty print JSON output (only effective when format='json')
|
||||
// Default false
|
||||
pretty?: boolean;
|
||||
}
|
||||
|
||||
// Using options
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: true,
|
||||
trackSceneData: true
|
||||
});
|
||||
```
|
||||
|
||||
## Incremental Data Structure
|
||||
|
||||
Incremental snapshots contain the following change types:
|
||||
|
||||
```typescript
|
||||
interface IncrementalSnapshot {
|
||||
version: number; // Snapshot version number
|
||||
timestamp: number; // Timestamp
|
||||
sceneName: string; // Scene name
|
||||
baseVersion: number; // Base version number
|
||||
entityChanges: EntityChange[]; // Entity changes
|
||||
componentChanges: ComponentChange[]; // Component changes
|
||||
sceneDataChanges: SceneDataChange[]; // Scene data changes
|
||||
}
|
||||
|
||||
// Change operation types
|
||||
enum ChangeOperation {
|
||||
EntityAdded = 'entity_added',
|
||||
EntityRemoved = 'entity_removed',
|
||||
EntityUpdated = 'entity_updated',
|
||||
ComponentAdded = 'component_added',
|
||||
ComponentRemoved = 'component_removed',
|
||||
ComponentUpdated = 'component_updated',
|
||||
SceneDataUpdated = 'scene_data_updated'
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### For High-Frequency Sync
|
||||
|
||||
```typescript
|
||||
// Disable deep comparison to improve performance
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // Only detect component addition/removal
|
||||
});
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Batch modify then serialize
|
||||
scene.entities.buffer.forEach(entity => {
|
||||
// Batch modifications
|
||||
});
|
||||
|
||||
// Serialize all changes at once
|
||||
const incremental = scene.serializeIncremental();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `scene.createIncrementalSnapshot(options?)` | Create base snapshot |
|
||||
| `scene.serializeIncremental()` | Get incremental changes |
|
||||
| `scene.applyIncremental(data)` | Apply incremental changes |
|
||||
| `scene.updateIncrementalSnapshot()` | Update snapshot base |
|
||||
| `scene.clearIncrementalSnapshot()` | Clear snapshot |
|
||||
| `scene.hasIncrementalSnapshot()` | Check if snapshot exists |
|
||||
| `IncrementalSerializer.getIncrementalStats(snapshot)` | Get change statistics |
|
||||
| `IncrementalSerializer.serializeIncremental(snapshot, options)` | Serialize incremental data |
|
||||
170
docs/src/content/docs/en/guide/serialization/index.md
Normal file
170
docs/src/content/docs/en/guide/serialization/index.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "Serialization System"
|
||||
description: "ECS serialization system overview and full serialization"
|
||||
---
|
||||
|
||||
The serialization system provides a complete solution for persisting scene, entity, and component data. It supports both full serialization and incremental serialization modes, suitable for game saves, network synchronization, scene editors, time rewinding, and more.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
The serialization system has two layers:
|
||||
|
||||
- **Full Serialization**: Serializes the complete scene state, including all entities, components, and scene data
|
||||
- **Incremental Serialization**: Only serializes changes relative to a base snapshot, greatly reducing data size
|
||||
|
||||
### Supported Data Formats
|
||||
|
||||
- **JSON Format**: Human-readable, convenient for debugging and editing
|
||||
- **Binary Format**: Uses MessagePack, smaller size and better performance
|
||||
|
||||
> **v2.2.2 Important Change**
|
||||
>
|
||||
> Starting from v2.2.2, binary serialization returns `Uint8Array` instead of Node.js `Buffer` to ensure browser compatibility:
|
||||
> - `serialize({ format: 'binary' })` returns `string | Uint8Array` (was `string | Buffer`)
|
||||
> - `deserialize(data)` accepts `string | Uint8Array` (was `string | Buffer`)
|
||||
> - `applyIncremental(data)` accepts `IncrementalSnapshot | string | Uint8Array` (was including `Buffer`)
|
||||
>
|
||||
> **Migration Impact**:
|
||||
> - **Runtime Compatible**: Node.js `Buffer` inherits from `Uint8Array`, existing code works directly
|
||||
> - **Type Checking**: If your TypeScript code explicitly uses `Buffer` type, change to `Uint8Array`
|
||||
> - **Browser Support**: `Uint8Array` is a standard JavaScript type supported by all modern browsers
|
||||
|
||||
## Full Serialization
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### 1. Mark Serializable Components
|
||||
|
||||
Use `@Serializable` and `@Serialize` decorators to mark components and fields for serialization:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
@Serialize()
|
||||
public experience: number = 0;
|
||||
|
||||
@Serialize()
|
||||
public position: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
// Fields without @Serialize() won't be serialized
|
||||
private tempData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Serialize Scene
|
||||
|
||||
```typescript
|
||||
// JSON format serialization
|
||||
const jsonData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true // Pretty print output
|
||||
});
|
||||
|
||||
// Save to local storage
|
||||
localStorage.setItem('gameSave', jsonData);
|
||||
|
||||
// Binary format serialization (smaller size)
|
||||
const binaryData = scene.serialize({
|
||||
format: 'binary'
|
||||
});
|
||||
|
||||
// Save to file (Node.js environment)
|
||||
// Note: binaryData is Uint8Array type, Node.js fs can write it directly
|
||||
fs.writeFileSync('save.bin', binaryData);
|
||||
```
|
||||
|
||||
#### 3. Deserialize Scene
|
||||
|
||||
```typescript
|
||||
// Restore from JSON
|
||||
const saveData = localStorage.getItem('gameSave');
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace' // Replace current scene content
|
||||
});
|
||||
}
|
||||
|
||||
// Restore from Binary
|
||||
const binaryData = fs.readFileSync('save.bin');
|
||||
scene.deserialize(binaryData, {
|
||||
strategy: 'merge' // Merge into existing scene
|
||||
});
|
||||
```
|
||||
|
||||
### Serialization Options
|
||||
|
||||
#### SerializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneSerializationOptions {
|
||||
// Component types to serialize (optional)
|
||||
components?: ComponentType[];
|
||||
|
||||
// Serialization format: 'json' or 'binary'
|
||||
format?: 'json' | 'binary';
|
||||
|
||||
// Pretty print JSON output
|
||||
pretty?: boolean;
|
||||
|
||||
// Include metadata
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// Only serialize specific component types
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
components: [PlayerComponent, InventoryComponent],
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
#### DeserializationOptions
|
||||
|
||||
```typescript
|
||||
interface SceneDeserializationOptions {
|
||||
// Deserialization strategy
|
||||
strategy?: 'merge' | 'replace';
|
||||
|
||||
// Component type registry (optional, uses global registry by default)
|
||||
componentRegistry?: Map<string, ComponentType>;
|
||||
}
|
||||
```
|
||||
|
||||
### Scene Custom Data
|
||||
|
||||
Besides entities and components, you can also serialize scene-level configuration data:
|
||||
|
||||
```typescript
|
||||
// Set scene data
|
||||
scene.sceneData.set('weather', 'rainy');
|
||||
scene.sceneData.set('difficulty', 'hard');
|
||||
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
|
||||
|
||||
// Scene data is automatically included when serializing
|
||||
const saveData = scene.serialize({ format: 'json' });
|
||||
|
||||
// Scene data is restored after deserialization
|
||||
scene.deserialize(saveData);
|
||||
console.log(scene.sceneData.get('weather')); // 'rainy'
|
||||
```
|
||||
|
||||
## More Topics
|
||||
|
||||
- [Decorators & Inheritance](/en/guide/serialization/decorators) - Advanced decorator usage and component inheritance
|
||||
- [Incremental Serialization](/en/guide/serialization/incremental) - Only serialize changes
|
||||
- [Version Migration](/en/guide/serialization/migration) - Handle data structure changes
|
||||
- [Use Cases](/en/guide/serialization/use-cases) - Save system, network sync, undo/redo examples
|
||||
151
docs/src/content/docs/en/guide/serialization/migration.md
Normal file
151
docs/src/content/docs/en/guide/serialization/migration.md
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: "Version Migration"
|
||||
description: "Handle component and scene data structure changes"
|
||||
---
|
||||
|
||||
When component structure changes, the version migration system can automatically upgrade old version save data.
|
||||
|
||||
## Register Migration Function
|
||||
|
||||
```typescript
|
||||
import { VersionMigrationManager } from '@esengine/ecs-framework';
|
||||
|
||||
// Assume PlayerComponent v1 has hp field
|
||||
// v2 changes to health and maxHealth fields
|
||||
|
||||
// Register migration from version 1 to version 2
|
||||
VersionMigrationManager.registerComponentMigration(
|
||||
'Player',
|
||||
1, // From version
|
||||
2, // To version
|
||||
(data) => {
|
||||
// Migration logic
|
||||
const newData = {
|
||||
...data,
|
||||
health: data.hp,
|
||||
maxHealth: data.hp,
|
||||
};
|
||||
delete newData.hp;
|
||||
return newData;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Using Migration Builder
|
||||
|
||||
```typescript
|
||||
import { MigrationBuilder } from '@esengine/ecs-framework';
|
||||
|
||||
new MigrationBuilder()
|
||||
.forComponent('Player')
|
||||
.fromVersionToVersion(2, 3)
|
||||
.migrate((data) => {
|
||||
// Migrate from version 2 to version 3
|
||||
data.experience = data.exp || 0;
|
||||
delete data.exp;
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
## Scene-Level Migration
|
||||
|
||||
```typescript
|
||||
// Register scene-level migration
|
||||
VersionMigrationManager.registerSceneMigration(
|
||||
1, // From version
|
||||
2, // To version
|
||||
(scene) => {
|
||||
// Migrate scene structure
|
||||
scene.metadata = {
|
||||
...scene.metadata,
|
||||
migratedFrom: 1
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Check Migration Path
|
||||
|
||||
```typescript
|
||||
// Check if migration is possible
|
||||
const canMigrate = VersionMigrationManager.canMigrateComponent(
|
||||
'Player',
|
||||
1, // From version
|
||||
3 // To version
|
||||
);
|
||||
|
||||
if (canMigrate) {
|
||||
// Safe to migrate
|
||||
scene.deserialize(oldSaveData);
|
||||
}
|
||||
|
||||
// Get migration path
|
||||
const path = VersionMigrationManager.getComponentMigrationPath('Player');
|
||||
console.log('Available migration versions:', path); // [1, 2, 3]
|
||||
```
|
||||
|
||||
## Migration Chain Example
|
||||
|
||||
When migrating across multiple versions, the system automatically chains migrations:
|
||||
|
||||
```typescript
|
||||
// Register v1 -> v2
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, (data) => {
|
||||
data.health = data.hp;
|
||||
delete data.hp;
|
||||
return data;
|
||||
});
|
||||
|
||||
// Register v2 -> v3
|
||||
VersionMigrationManager.registerComponentMigration('Player', 2, 3, (data) => {
|
||||
data.stats = { health: data.health, mana: 100 };
|
||||
delete data.health;
|
||||
return data;
|
||||
});
|
||||
|
||||
// When loading v1 data, it will automatically execute v1 -> v2 -> v3
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Maintain Backward Compatibility
|
||||
|
||||
```typescript
|
||||
// Document data structure changes for each version
|
||||
// v1: { hp: number }
|
||||
// v2: { health: number, maxHealth: number }
|
||||
// v3: { stats: { health: number, mana: number } }
|
||||
```
|
||||
|
||||
### 2. Test Migration Paths
|
||||
|
||||
```typescript
|
||||
// Test all possible migration paths
|
||||
const testData = { hp: 100 };
|
||||
const migrated = VersionMigrationManager.migrateComponent('Player', testData, 1, 3);
|
||||
expect(migrated.stats.health).toBe(100);
|
||||
```
|
||||
|
||||
### 3. Keep Backup of Original Data
|
||||
|
||||
```typescript
|
||||
// Backup before migration
|
||||
const backup = JSON.parse(JSON.stringify(saveData));
|
||||
try {
|
||||
scene.deserialize(saveData);
|
||||
} catch (e) {
|
||||
// Restore on migration failure
|
||||
console.error('Migration failed:', e);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `registerComponentMigration(type, from, to, fn)` | Register component migration function |
|
||||
| `registerSceneMigration(from, to, fn)` | Register scene migration function |
|
||||
| `canMigrateComponent(type, from, to)` | Check if migration is possible |
|
||||
| `getComponentMigrationPath(type)` | Get component's migration version path |
|
||||
| `migrateComponent(type, data, from, to)` | Execute component data migration |
|
||||
306
docs/src/content/docs/en/guide/serialization/use-cases.md
Normal file
306
docs/src/content/docs/en/guide/serialization/use-cases.md
Normal file
@@ -0,0 +1,306 @@
|
||||
---
|
||||
title: "Use Cases"
|
||||
description: "Practical examples of the serialization system"
|
||||
---
|
||||
|
||||
## Game Save System
|
||||
|
||||
```typescript
|
||||
class SaveSystem {
|
||||
private static SAVE_KEY = 'game_save';
|
||||
|
||||
// Save game
|
||||
public static saveGame(scene: Scene): void {
|
||||
const saveData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: false
|
||||
});
|
||||
|
||||
localStorage.setItem(this.SAVE_KEY, saveData);
|
||||
console.log('Game saved');
|
||||
}
|
||||
|
||||
// Load game
|
||||
public static loadGame(scene: Scene): boolean {
|
||||
const saveData = localStorage.getItem(this.SAVE_KEY);
|
||||
if (saveData) {
|
||||
scene.deserialize(saveData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
console.log('Game loaded');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if save exists
|
||||
public static hasSave(): boolean {
|
||||
return localStorage.getItem(this.SAVE_KEY) !== null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network Synchronization
|
||||
|
||||
```typescript
|
||||
class NetworkSync {
|
||||
private baseSnapshot?: any;
|
||||
private syncInterval: number = 100; // Sync every 100ms
|
||||
|
||||
constructor(private scene: Scene, private socket: WebSocket) {
|
||||
this.setupSync();
|
||||
}
|
||||
|
||||
private setupSync(): void {
|
||||
// Create base snapshot
|
||||
this.scene.createIncrementalSnapshot();
|
||||
|
||||
// Periodically send incremental updates
|
||||
setInterval(() => {
|
||||
this.sendIncremental();
|
||||
}, this.syncInterval);
|
||||
|
||||
// Receive remote incremental updates
|
||||
this.socket.onmessage = (event) => {
|
||||
this.receiveIncremental(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
private sendIncremental(): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
const stats = IncrementalSerializer.getIncrementalStats(incremental);
|
||||
|
||||
// Only send when there are changes
|
||||
if (stats.totalChanges > 0) {
|
||||
// Use binary format to reduce network transmission
|
||||
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
|
||||
format: 'binary'
|
||||
});
|
||||
this.socket.send(binaryData);
|
||||
|
||||
// Update base
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private receiveIncremental(data: ArrayBuffer): void {
|
||||
// Directly apply binary data (ArrayBuffer to Uint8Array)
|
||||
const uint8Array = new Uint8Array(data);
|
||||
this.scene.applyIncremental(uint8Array);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Undo/Redo System
|
||||
|
||||
```typescript
|
||||
class UndoRedoSystem {
|
||||
private history: IncrementalSnapshot[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxHistory: number = 50;
|
||||
|
||||
constructor(private scene: Scene) {
|
||||
// Create initial snapshot
|
||||
this.scene.createIncrementalSnapshot();
|
||||
this.saveState('Initial');
|
||||
}
|
||||
|
||||
// Save current state
|
||||
public saveState(label: string): void {
|
||||
const incremental = this.scene.serializeIncremental();
|
||||
|
||||
// Remove history after current position
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
|
||||
// Add new state
|
||||
this.history.push(incremental);
|
||||
this.currentIndex++;
|
||||
|
||||
// Limit history count
|
||||
if (this.history.length > this.maxHistory) {
|
||||
this.history.shift();
|
||||
this.currentIndex--;
|
||||
}
|
||||
|
||||
// Update snapshot base
|
||||
this.scene.updateIncrementalSnapshot();
|
||||
}
|
||||
|
||||
// Undo
|
||||
public undo(): boolean {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Redo
|
||||
public redo(): boolean {
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.currentIndex++;
|
||||
const incremental = this.history[this.currentIndex];
|
||||
this.scene.applyIncremental(incremental);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Level Editor
|
||||
|
||||
```typescript
|
||||
class LevelEditor {
|
||||
// Export level
|
||||
public exportLevel(scene: Scene, filename: string): void {
|
||||
const levelData = scene.serialize({
|
||||
format: 'json',
|
||||
pretty: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// Browser environment
|
||||
const blob = new Blob([levelData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Import level
|
||||
public importLevel(scene: Scene, fileContent: string): void {
|
||||
scene.deserialize(fileContent, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate level data
|
||||
public validateLevel(saveData: string): boolean {
|
||||
const validation = SceneSerializer.validate(saveData);
|
||||
if (!validation.valid) {
|
||||
console.error('Level data invalid:', validation.errors);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get level info (without full deserialization)
|
||||
public getLevelInfo(saveData: string): any {
|
||||
const info = SceneSerializer.getInfo(saveData);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### 1. Choose the Right Format
|
||||
|
||||
- **Development**: Use JSON format for easy debugging and inspection
|
||||
- **Production**: Use Binary format to reduce 30-50% data size
|
||||
|
||||
### 2. Serialize On-Demand
|
||||
|
||||
```typescript
|
||||
// Only serialize components that need persistence
|
||||
const saveData = scene.serialize({
|
||||
format: 'binary',
|
||||
components: [PlayerComponent, InventoryComponent, QuestComponent]
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Optimize Incremental Serialization
|
||||
|
||||
```typescript
|
||||
// For high-frequency sync, disable deep comparison for better performance
|
||||
scene.createIncrementalSnapshot({
|
||||
deepComponentComparison: false // Only detect component addition/removal
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Explicitly Mark Serialized Fields
|
||||
|
||||
```typescript
|
||||
// Clearly mark fields that need serialization
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
public name: string = '';
|
||||
|
||||
@Serialize()
|
||||
public level: number = 1;
|
||||
|
||||
// Runtime data not serialized
|
||||
private _cachedSprite: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Version Control
|
||||
|
||||
```typescript
|
||||
// Specify version for components
|
||||
@Serializable({ version: 2 })
|
||||
class PlayerComponent extends Component {
|
||||
// Version 2 fields
|
||||
}
|
||||
|
||||
// Register migration function to ensure compatibility
|
||||
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
|
||||
```
|
||||
|
||||
### 3. Avoid Circular References
|
||||
|
||||
```typescript
|
||||
// Don't directly reference other entities in components
|
||||
@ECSComponent('Follower')
|
||||
@Serializable({ version: 1 })
|
||||
class FollowerComponent extends Component {
|
||||
// Store entity ID instead of entity reference
|
||||
@Serialize()
|
||||
public targetId: number = 0;
|
||||
|
||||
// Find target entity through scene
|
||||
public getTarget(scene: Scene): Entity | null {
|
||||
return scene.entities.findEntityById(this.targetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Compress Large Data
|
||||
|
||||
```typescript
|
||||
// For large data structures, use custom serialization
|
||||
@ECSComponent('LargeData')
|
||||
@Serializable({ version: 1 })
|
||||
class LargeDataComponent extends Component {
|
||||
@Serialize({
|
||||
serializer: (data: LargeObject) => compressData(data),
|
||||
deserializer: (data: CompressedData) => decompressData(data)
|
||||
})
|
||||
public data: LargeObject;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Scenario | JSON Format | Binary Format | Savings |
|
||||
|----------|-------------|---------------|---------|
|
||||
| Small save (100 entities) | 50KB | 35KB | 30% |
|
||||
| Medium save (1000 entities) | 500KB | 300KB | 40% |
|
||||
| Large save (10000 entities) | 5MB | 2.5MB | 50% |
|
||||
208
docs/src/content/docs/en/guide/service-container/advanced.md
Normal file
208
docs/src/content/docs/en/guide/service-container/advanced.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: "Advanced Usage"
|
||||
description: "Symbol patterns, best practices, common issues"
|
||||
---
|
||||
|
||||
## Interface and Symbol Identifier Pattern
|
||||
|
||||
For large projects or games requiring cross-platform adaptation, the "Interface + Symbol.for identifier" pattern is recommended.
|
||||
|
||||
### Why Use Symbol.for
|
||||
|
||||
- **Cross-package Sharing**: `Symbol.for('key')` creates/retrieves Symbol from global registry
|
||||
- **Interface Decoupling**: Consumers only depend on interface definitions, not concrete implementations
|
||||
- **Replaceable Implementations**: Can inject different implementations at runtime (test mocks, platform adapters)
|
||||
|
||||
### Define Interface and Identifier
|
||||
|
||||
```typescript
|
||||
// IAudioService.ts
|
||||
export interface IAudioService {
|
||||
dispose(): void;
|
||||
playSound(id: string): void;
|
||||
playMusic(id: string, loop?: boolean): void;
|
||||
stopMusic(): void;
|
||||
setVolume(volume: number): void;
|
||||
preload(id: string, url: string): Promise<void>;
|
||||
}
|
||||
|
||||
// Use Symbol.for to ensure cross-package sharing
|
||||
export const IAudioService = Symbol.for('IAudioService');
|
||||
```
|
||||
|
||||
### Implement Interface
|
||||
|
||||
```typescript
|
||||
// WebAudioService.ts - Web platform
|
||||
export class WebAudioService implements IAudioService {
|
||||
private audioContext: AudioContext;
|
||||
|
||||
constructor() {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
playSound(id: string): void {
|
||||
// Web Audio API implementation
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.audioContext.close();
|
||||
}
|
||||
}
|
||||
|
||||
// WechatAudioService.ts - WeChat Mini Game platform
|
||||
export class WechatAudioService implements IAudioService {
|
||||
playSound(id: string): void {
|
||||
// WeChat Mini Game API implementation
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// Cleanup
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Register and Use
|
||||
|
||||
```typescript
|
||||
// Register different implementations based on platform
|
||||
if (typeof wx !== 'undefined') {
|
||||
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
||||
} else {
|
||||
Core.services.registerInstance(IAudioService, new WebAudioService());
|
||||
}
|
||||
|
||||
// Business code - doesn't care about concrete implementation
|
||||
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
||||
audio.playSound('explosion');
|
||||
```
|
||||
|
||||
### Symbol vs Symbol.for
|
||||
|
||||
```typescript
|
||||
// Symbol() - Creates unique Symbol each time
|
||||
const sym1 = Symbol('test');
|
||||
const sym2 = Symbol('test');
|
||||
console.log(sym1 === sym2); // false
|
||||
|
||||
// Symbol.for() - Shares in global registry
|
||||
const sym3 = Symbol.for('test');
|
||||
const sym4 = Symbol.for('test');
|
||||
console.log(sym3 === sym4); // true
|
||||
```
|
||||
|
||||
## Circular Dependency Detection
|
||||
|
||||
The service container automatically detects circular dependencies:
|
||||
|
||||
```typescript
|
||||
// A depends on B, B depends on A
|
||||
@Injectable()
|
||||
class ServiceA implements IService {
|
||||
@InjectProperty(ServiceB)
|
||||
private b!: ServiceB;
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ServiceB implements IService {
|
||||
@InjectProperty(ServiceA)
|
||||
private a!: ServiceA;
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// Throws error on resolution
|
||||
// Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
||||
```
|
||||
|
||||
## Service Management
|
||||
|
||||
```typescript
|
||||
// Get all registered service types
|
||||
const types = Core.services.getRegisteredServices();
|
||||
|
||||
// Get all instantiated service instances
|
||||
const instances = Core.services.getAll();
|
||||
|
||||
// Unregister single service
|
||||
Core.services.unregister(MyService);
|
||||
|
||||
// Clear all services (calls dispose on each)
|
||||
Core.services.clear();
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Service Naming
|
||||
|
||||
Service class names should end with `Service` or `Manager`:
|
||||
|
||||
```typescript
|
||||
class PlayerService implements IService {}
|
||||
class AudioManager implements IService {}
|
||||
class NetworkService implements IService {}
|
||||
```
|
||||
|
||||
### Resource Cleanup
|
||||
|
||||
Always clean up resources in the `dispose()` method:
|
||||
|
||||
```typescript
|
||||
class ResourceService implements IService {
|
||||
private resources: Map<string, Resource> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
for (const resource of this.resources.values()) {
|
||||
resource.release();
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid Overuse
|
||||
|
||||
Don't register every class as a service. Services should be:
|
||||
|
||||
- Global singletons or need shared state
|
||||
- Used in multiple places
|
||||
- Need lifecycle management
|
||||
- Require dependency injection
|
||||
|
||||
### Dependency Direction
|
||||
|
||||
Maintain clear dependency direction, avoid circular dependencies:
|
||||
|
||||
```
|
||||
High-level services -> Mid-level services -> Low-level services
|
||||
GameLogic -> DataService -> ConfigService
|
||||
```
|
||||
|
||||
### When to Use Singleton vs Transient
|
||||
|
||||
- **Singleton**: Manager classes, config, cache, state management
|
||||
- **Transient**: Command objects, request handlers, temporary tasks
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Service Not Registered Error
|
||||
|
||||
**Problem**: `Error: Service MyService is not registered`
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// Ensure service is registered
|
||||
Core.services.registerSingleton(MyService);
|
||||
|
||||
// Or use tryResolve
|
||||
const service = Core.services.tryResolve(MyService);
|
||||
if (!service) {
|
||||
console.log('Service not found');
|
||||
}
|
||||
```
|
||||
|
||||
### Circular Dependency Error
|
||||
|
||||
**Problem**: `Circular dependency detected`
|
||||
|
||||
**Solution**: Redesign service dependencies, introduce intermediate services or use event system for decoupling.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: "Built-in Services"
|
||||
description: "Services automatically registered by Core"
|
||||
---
|
||||
|
||||
Core automatically registers the following built-in services during initialization:
|
||||
|
||||
## TimerManager
|
||||
|
||||
Timer manager responsible for managing all game timers:
|
||||
|
||||
```typescript
|
||||
const timerManager = Core.services.resolve(TimerManager);
|
||||
|
||||
// Create a timer
|
||||
timerManager.schedule(1.0, false, null, (timer) => {
|
||||
console.log('Executed after 1 second');
|
||||
});
|
||||
```
|
||||
|
||||
## PerformanceMonitor
|
||||
|
||||
Performance monitor for tracking game performance:
|
||||
|
||||
```typescript
|
||||
const monitor = Core.services.resolve(PerformanceMonitor);
|
||||
|
||||
// Enable performance monitoring
|
||||
monitor.enable();
|
||||
|
||||
// Get performance data
|
||||
const fps = monitor.getFPS();
|
||||
```
|
||||
|
||||
## SceneManager
|
||||
|
||||
Scene manager for single-scene application lifecycle:
|
||||
|
||||
```typescript
|
||||
const sceneManager = Core.services.resolve(SceneManager);
|
||||
|
||||
// Set current scene
|
||||
sceneManager.setScene(new GameScene());
|
||||
|
||||
// Get current scene
|
||||
const currentScene = sceneManager.currentScene;
|
||||
|
||||
// Delayed scene switch
|
||||
sceneManager.loadScene(new MenuScene());
|
||||
```
|
||||
|
||||
## WorldManager
|
||||
|
||||
World manager for managing multiple independent World instances (advanced use case):
|
||||
|
||||
```typescript
|
||||
const worldManager = Core.services.resolve(WorldManager);
|
||||
|
||||
// Create independent game worlds
|
||||
const gameWorld = worldManager.createWorld('game_room_001', {
|
||||
name: 'GameRoom',
|
||||
maxScenes: 5
|
||||
});
|
||||
|
||||
// Create scene in World
|
||||
const scene = gameWorld.createScene('battle', new BattleScene());
|
||||
gameWorld.setSceneActive('battle', true);
|
||||
|
||||
// Update all Worlds
|
||||
worldManager.updateAll();
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- **SceneManager**: Suitable for 95% of games (single-player, simple multiplayer)
|
||||
- **WorldManager**: Suitable for MMO servers, game room systems requiring complete isolation
|
||||
|
||||
## PoolManager
|
||||
|
||||
Object pool manager:
|
||||
|
||||
```typescript
|
||||
const poolManager = Core.services.resolve(PoolManager);
|
||||
|
||||
// Create object pool
|
||||
const bulletPool = poolManager.createPool('bullets', () => new Bullet(), 100);
|
||||
```
|
||||
|
||||
## PluginManager
|
||||
|
||||
Plugin manager for installing and uninstalling plugins:
|
||||
|
||||
```typescript
|
||||
const pluginManager = Core.services.resolve(PluginManager);
|
||||
|
||||
// Get all installed plugins
|
||||
const plugins = pluginManager.getAllPlugins();
|
||||
```
|
||||
@@ -0,0 +1,190 @@
|
||||
---
|
||||
title: "Dependency Injection"
|
||||
description: "Decorators and automatic dependency injection"
|
||||
---
|
||||
|
||||
ECS Framework provides decorators to simplify dependency injection.
|
||||
|
||||
## @Injectable Decorator
|
||||
|
||||
Marks a class as injectable:
|
||||
|
||||
```typescript
|
||||
import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class GameService implements IService {
|
||||
constructor() {
|
||||
console.log('GameService created');
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
console.log('GameService disposed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @InjectProperty Decorator
|
||||
|
||||
Injects dependencies via property decorator. Injection occurs after constructor execution, before `onInitialize()` is called:
|
||||
|
||||
```typescript
|
||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
class PlayerService implements IService {
|
||||
@InjectProperty(DataService)
|
||||
private data!: DataService;
|
||||
|
||||
@InjectProperty(GameService)
|
||||
private game!: GameService;
|
||||
|
||||
dispose(): void {
|
||||
// Cleanup resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in EntitySystem
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class CombatSystem extends EntitySystem {
|
||||
@InjectProperty(TimeService)
|
||||
private timeService!: TimeService;
|
||||
|
||||
@InjectProperty(AudioService)
|
||||
private audio!: AudioService;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Health, Attack));
|
||||
}
|
||||
|
||||
onInitialize(): void {
|
||||
// Properties are injected at this point
|
||||
console.log('Delta time:', this.timeService.getDeltaTime());
|
||||
}
|
||||
|
||||
processEntity(entity: Entity): void {
|
||||
this.audio.playSound('attack');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: Use `!` assertion when declaring properties (e.g., `private data!: DataService`) to indicate the property will be injected before use.
|
||||
|
||||
## Registering Injectable Services
|
||||
|
||||
Use `registerInjectable` to automatically handle dependency injection:
|
||||
|
||||
```typescript
|
||||
import { registerInjectable } from '@esengine/ecs-framework';
|
||||
|
||||
// Register service (automatically resolves @InjectProperty dependencies)
|
||||
registerInjectable(Core.services, PlayerService);
|
||||
|
||||
// Property dependencies are automatically injected on resolution
|
||||
const player = Core.services.resolve(PlayerService);
|
||||
```
|
||||
|
||||
## @Updatable Decorator
|
||||
|
||||
Marks a service as updatable, making it automatically called each frame:
|
||||
|
||||
```typescript
|
||||
import { Injectable, Updatable, IService, IUpdatable } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@Updatable() // Default priority is 0
|
||||
class PhysicsService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// Update physics simulation each frame
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// Specify update priority (lower values execute first)
|
||||
@Injectable()
|
||||
@Updatable(10)
|
||||
class RenderService implements IService, IUpdatable {
|
||||
update(deltaTime?: number): void {
|
||||
// Render each frame
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
```
|
||||
|
||||
Services with `@Updatable` decorator are automatically called by Core:
|
||||
|
||||
```typescript
|
||||
function gameLoop(deltaTime: number) {
|
||||
Core.update(deltaTime); // Automatically updates all updatable services
|
||||
}
|
||||
```
|
||||
|
||||
## Service Dependencies
|
||||
|
||||
Recommended to use `@InjectProperty` for service dependencies:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class ConfigService implements IService {
|
||||
private config: any = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.config[key];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
@InjectProperty(ConfigService)
|
||||
private config!: ConfigService;
|
||||
|
||||
onInitialize(): void {
|
||||
// Use injected service in onInitialize
|
||||
const apiUrl = this.config.get('apiUrl');
|
||||
}
|
||||
|
||||
dispose(): void {}
|
||||
}
|
||||
|
||||
// Register services (in dependency order)
|
||||
registerInjectable(Core.services, ConfigService);
|
||||
registerInjectable(Core.services, NetworkService);
|
||||
```
|
||||
|
||||
## @Inject Constructor Decorator
|
||||
|
||||
:::caution[Compatibility Warning]
|
||||
The `@Inject` constructor parameter decorator may **not be supported** in **Cocos Creator** and **LayaAir**, as these engines' build tools may have issues processing constructor parameter decorators.
|
||||
|
||||
**Recommended**: Always use `@InjectProperty` property decorator, which works correctly on all platforms.
|
||||
:::
|
||||
|
||||
```typescript
|
||||
// ⚠️ Not recommended: Constructor injection (may not work in Cocos/Laya)
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
constructor(
|
||||
@Inject(ConfigService) private config: ConfigService
|
||||
) {}
|
||||
}
|
||||
|
||||
// ✅ Recommended: Property injection (compatible with all platforms)
|
||||
@Injectable()
|
||||
class NetworkService implements IService {
|
||||
@InjectProperty(ConfigService)
|
||||
private config!: ConfigService;
|
||||
|
||||
onInitialize(): void {
|
||||
// Use injected service here
|
||||
}
|
||||
}
|
||||
```
|
||||
123
docs/src/content/docs/en/guide/service-container/index.md
Normal file
123
docs/src/content/docs/en/guide/service-container/index.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Service Container Overview"
|
||||
description: "ECS Framework Dependency Injection Container"
|
||||
---
|
||||
|
||||
The ServiceContainer is the dependency injection container of ECS Framework, responsible for managing the registration, resolution, and lifecycle of all services.
|
||||
|
||||
## What is a Service Container
|
||||
|
||||
The service container is a lightweight dependency injection (DI) container that provides:
|
||||
|
||||
- **Service Registration**: Register service types into the container
|
||||
- **Service Resolution**: Retrieve service instances from the container
|
||||
- **Lifecycle Management**: Automatically manage service instance creation and destruction
|
||||
- **Dependency Injection**: Automatically resolve dependencies between services
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Service
|
||||
|
||||
A service is a class that implements the `IService` interface and must provide a `dispose()` method for resource cleanup:
|
||||
|
||||
```typescript
|
||||
import { IService } from '@esengine/ecs-framework';
|
||||
|
||||
class MyService implements IService {
|
||||
constructor() {
|
||||
// Initialization logic
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// Cleanup resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Identifier
|
||||
|
||||
Service identifiers are used to uniquely identify a service in the container. Two types are supported:
|
||||
|
||||
- **Class Constructor**: Use the service class directly as identifier
|
||||
- **Symbol**: Use Symbol as identifier (recommended for interface abstractions)
|
||||
|
||||
```typescript
|
||||
// Method 1: Using class as identifier
|
||||
Core.services.registerSingleton(DataService);
|
||||
const data = Core.services.resolve(DataService);
|
||||
|
||||
// Method 2: Using Symbol as identifier
|
||||
const IFileSystem = Symbol.for('IFileSystem');
|
||||
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
||||
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
||||
```
|
||||
|
||||
### Lifecycle
|
||||
|
||||
- **Singleton**: Only one instance throughout the application lifecycle
|
||||
- **Transient**: Creates a new instance on each resolution
|
||||
|
||||
## Container Hierarchy
|
||||
|
||||
ECS Framework provides three levels of service containers:
|
||||
|
||||
```
|
||||
Core.services (Application global)
|
||||
└─ World.services (World level)
|
||||
└─ Scene.services (Scene level)
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Core level
|
||||
const container = Core.services;
|
||||
|
||||
// World level
|
||||
const worldContainer = world.services;
|
||||
|
||||
// Scene level
|
||||
const sceneContainer = scene.services;
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Registering Services
|
||||
|
||||
```typescript
|
||||
// Singleton service
|
||||
Core.services.registerSingleton(DataService);
|
||||
|
||||
// Transient service
|
||||
Core.services.registerTransient(CommandService);
|
||||
|
||||
// Register instance
|
||||
Core.services.registerInstance(ConfigService, config);
|
||||
|
||||
// Factory function
|
||||
Core.services.registerSingleton(LoggerService, (container) => {
|
||||
const logger = new LoggerService();
|
||||
logger.setLevel('debug');
|
||||
return logger;
|
||||
});
|
||||
```
|
||||
|
||||
### Resolving Services
|
||||
|
||||
```typescript
|
||||
// Resolve service (throws if not registered)
|
||||
const dataService = Core.services.resolve(DataService);
|
||||
|
||||
// Try resolve (returns null if not registered)
|
||||
const optional = Core.services.tryResolve(OptionalService);
|
||||
|
||||
// Check if registered
|
||||
if (Core.services.isRegistered(DataService)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Built-in Services](./built-in-services/) - Framework provided services
|
||||
- [Dependency Injection](./dependency-injection/) - Decorators and auto-injection
|
||||
- [PluginServiceRegistry](./plugin-service-registry/) - Plugin service registry
|
||||
- [Advanced Usage](./advanced/) - Symbol patterns, best practices
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "PluginServiceRegistry"
|
||||
description: "Type-safe plugin service registry"
|
||||
---
|
||||
|
||||
`PluginServiceRegistry` is a type-safe service registry based on `ServiceToken`, designed for sharing services between plugins.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Type Safety** - Uses ServiceToken to carry type information
|
||||
2. **Explicit Dependencies** - Express dependencies clearly by importing tokens
|
||||
3. **Optional Dependencies** - `get` returns undefined, `require` throws
|
||||
4. **Single Responsibility** - Only handles registration and lookup, not lifecycle
|
||||
5. **Who Defines the Interface, Exports the Token** - Each module defines its own interfaces and tokens
|
||||
|
||||
## ServiceToken
|
||||
|
||||
Service tokens are used for type-safe service registration and retrieval:
|
||||
|
||||
```typescript
|
||||
import { createServiceToken, ServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
// Define interface
|
||||
interface IAssetManager {
|
||||
load(path: string): Promise<any>;
|
||||
unload(path: string): void;
|
||||
}
|
||||
|
||||
// Create service token
|
||||
const AssetManagerToken = createServiceToken<IAssetManager>('assetManager');
|
||||
```
|
||||
|
||||
### Why Use ServiceToken
|
||||
|
||||
- **Cross-package Type Safety**: TypeScript preserves generic type information across packages
|
||||
- **Globally Unique**: Uses `Symbol.for()` to ensure same-named tokens reference the same Symbol
|
||||
- **Explicit Dependencies**: Clearly express inter-module dependencies by importing tokens
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Registering Services
|
||||
|
||||
```typescript
|
||||
import { PluginServiceRegistry, createServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
// Create registry
|
||||
const registry = new PluginServiceRegistry();
|
||||
|
||||
// Define token
|
||||
interface ILogger {
|
||||
log(message: string): void;
|
||||
}
|
||||
const LoggerToken = createServiceToken<ILogger>('logger');
|
||||
|
||||
// Register service
|
||||
const logger: ILogger = {
|
||||
log: (msg) => console.log(msg)
|
||||
};
|
||||
registry.register(LoggerToken, logger);
|
||||
```
|
||||
|
||||
### Getting Services
|
||||
|
||||
```typescript
|
||||
// Optional get (returns undefined if not exists)
|
||||
const logger = registry.get(LoggerToken);
|
||||
if (logger) {
|
||||
logger.log('Hello');
|
||||
}
|
||||
|
||||
// Required get (throws if not exists)
|
||||
try {
|
||||
const logger = registry.require(LoggerToken);
|
||||
logger.log('Hello');
|
||||
} catch (e) {
|
||||
console.error('Logger not registered');
|
||||
}
|
||||
```
|
||||
|
||||
### Check and Unregister
|
||||
|
||||
```typescript
|
||||
// Check if registered
|
||||
if (registry.has(LoggerToken)) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Unregister service
|
||||
registry.unregister(LoggerToken);
|
||||
|
||||
// Clear all services
|
||||
registry.clear();
|
||||
```
|
||||
|
||||
## Usage in Plugins
|
||||
|
||||
### Define Module Service Tokens
|
||||
|
||||
Each module should define its interfaces and tokens in `tokens.ts`:
|
||||
|
||||
```typescript
|
||||
// packages/asset-system/src/tokens.ts
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
export interface IAssetManager {
|
||||
load<T>(path: string): Promise<T>;
|
||||
unload(path: string): void;
|
||||
getCache(path: string): any | undefined;
|
||||
}
|
||||
|
||||
export const AssetManagerToken = createServiceToken<IAssetManager>('assetManager');
|
||||
```
|
||||
|
||||
### Register Service in Plugin
|
||||
|
||||
```typescript
|
||||
// packages/asset-system/src/AssetSystemPlugin.ts
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { AssetManagerToken, IAssetManager } from './tokens';
|
||||
import { AssetManager } from './AssetManager';
|
||||
|
||||
export function installAssetSystem() {
|
||||
const assetManager = new AssetManager();
|
||||
|
||||
// Register to Core's plugin service registry
|
||||
Core.pluginServices.register(AssetManagerToken, assetManager);
|
||||
}
|
||||
```
|
||||
|
||||
### Use in Other Plugins
|
||||
|
||||
```typescript
|
||||
// packages/sprite/src/SpriteSystem.ts
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { AssetManagerToken, IAssetManager } from '@esengine/asset-system';
|
||||
|
||||
class SpriteSystem extends EntitySystem {
|
||||
private assetManager!: IAssetManager;
|
||||
|
||||
onInitialize(): void {
|
||||
// Get from plugin service registry
|
||||
this.assetManager = Core.pluginServices.require(AssetManagerToken);
|
||||
}
|
||||
|
||||
async loadSprite(path: string) {
|
||||
const texture = await this.assetManager.load<Texture>(path);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Difference from ServiceContainer
|
||||
|
||||
| Feature | ServiceContainer | PluginServiceRegistry |
|
||||
|---------|------------------|----------------------|
|
||||
| Purpose | General DI | Cross-plugin service sharing |
|
||||
| Identifier | Class or Symbol | ServiceToken |
|
||||
| Lifecycle | Singleton/Transient | None (caller managed) |
|
||||
| Decorator Support | @Injectable, @InjectProperty | None |
|
||||
| Type Safety | Requires generic assertion | Token carries type |
|
||||
|
||||
## API Reference
|
||||
|
||||
### createServiceToken
|
||||
|
||||
```typescript
|
||||
function createServiceToken<T>(name: string): ServiceToken<T>
|
||||
```
|
||||
|
||||
Creates a service token. Uses `Symbol.for()` to ensure cross-package sharing.
|
||||
|
||||
### PluginServiceRegistry
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `register<T>(token, service)` | Register a service |
|
||||
| `get<T>(token): T \| undefined` | Get service (optional) |
|
||||
| `require<T>(token): T` | Get service (required, throws if missing) |
|
||||
| `has<T>(token): boolean` | Check if registered |
|
||||
| `unregister<T>(token): boolean` | Unregister service |
|
||||
| `clear()` | Clear all services |
|
||||
| `dispose()` | Dispose resources |
|
||||
368
docs/src/content/docs/en/guide/system/best-practices.md
Normal file
368
docs/src/content/docs/en/guide/system/best-practices.md
Normal file
@@ -0,0 +1,368 @@
|
||||
---
|
||||
title: "Best Practices"
|
||||
description: "System design best practices and complex examples"
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Single Responsibility for Systems
|
||||
|
||||
```typescript
|
||||
// ✅ Good system design - single responsibility
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Rendering')
|
||||
class RenderingSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Avoid - too many responsibilities
|
||||
@ECSSystem('GameSystem')
|
||||
class GameSystem extends EntitySystem {
|
||||
// One system handling movement, rendering, sound effects, and more
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use @ECSSystem Decorator
|
||||
|
||||
`@ECSSystem` is a required decorator for system classes, providing type identification and metadata management.
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable system names that remain correct after code obfuscation |
|
||||
| **Debug Support** | Shows readable system names in performance monitoring, logs, and debug tools |
|
||||
| **System Management** | Find and manage systems by name |
|
||||
| **Serialization Support** | Records system configuration during scene serialization |
|
||||
|
||||
```typescript
|
||||
// ✅ Correct usage
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// System implementation
|
||||
}
|
||||
|
||||
// ✅ Recommended: Use descriptive names
|
||||
@ECSSystem('PlayerMovement')
|
||||
class PlayerMovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Player, Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Wrong - no decorator
|
||||
class BadSystem extends EntitySystem {
|
||||
// Systems defined this way may have issues in production
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proper Update Order
|
||||
|
||||
```typescript
|
||||
// Set system update order by logical sequence
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = -100; // Process input first
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Logic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 0; // Process game logic
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 100; // Render last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Avoid Direct References Between Systems
|
||||
|
||||
```typescript
|
||||
// ❌ Avoid: Direct system references
|
||||
@ECSSystem('Bad')
|
||||
class BadSystem extends EntitySystem {
|
||||
private otherSystem: SomeOtherSystem; // Avoid direct references to other systems
|
||||
}
|
||||
|
||||
// ✅ Recommended: Communicate through event system
|
||||
@ECSSystem('Good')
|
||||
class GoodSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Communicate with other systems through event system
|
||||
this.scene?.eventSystem.emitSync('data_updated', { entities });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Clean Up Resources Promptly
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Resource')
|
||||
class ResourceSystem extends EntitySystem {
|
||||
private resources: Map<string, any> = new Map();
|
||||
|
||||
protected onDestroy(): void {
|
||||
// Clean up resources
|
||||
for (const [key, resource] of this.resources) {
|
||||
if (resource.dispose) {
|
||||
resource.dispose();
|
||||
}
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex System Examples
|
||||
|
||||
### Collision Detection System
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Collision')
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Collider));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple n² collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: Entity, entityB: Entity): void {
|
||||
const transformA = entityA.getComponent(Transform);
|
||||
const transformB = entityB.getComponent(Transform);
|
||||
const colliderA = entityA.getComponent(Collider);
|
||||
const colliderB = entityB.getComponent(Collider);
|
||||
|
||||
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
|
||||
// Send collision event
|
||||
this.scene?.eventSystem.emitSync('collision', {
|
||||
entityA,
|
||||
entityB
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isColliding(
|
||||
transformA: Transform, colliderA: Collider,
|
||||
transformB: Transform, colliderB: Collider
|
||||
): boolean {
|
||||
// Collision detection logic
|
||||
const dx = transformA.x - transformB.x;
|
||||
const dy = transformA.y - transformB.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
return distance < colliderA.radius + colliderB.radius;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Machine System
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSSystem('StateMachine')
|
||||
class StateMachineSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(StateMachine));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const stateMachine = entity.getComponent(StateMachine);
|
||||
if (stateMachine) {
|
||||
stateMachine.updateTimer(Time.deltaTime);
|
||||
this.updateState(entity, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(entity: Entity, stateMachine: StateMachine): void {
|
||||
switch (stateMachine.currentState) {
|
||||
case EntityState.Idle:
|
||||
this.handleIdleState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Moving:
|
||||
this.handleMovingState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Attacking:
|
||||
this.handleAttackingState(entity, stateMachine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleState(entity: Entity, sm: StateMachine): void {
|
||||
// Check for movement input
|
||||
const input = entity.getComponent(InputComponent);
|
||||
if (input && input.hasMovementInput()) {
|
||||
sm.changeState(EntityState.Moving);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMovingState(entity: Entity, sm: StateMachine): void {
|
||||
// Check if movement stopped
|
||||
const input = entity.getComponent(InputComponent);
|
||||
if (!input || !input.hasMovementInput()) {
|
||||
sm.changeState(EntityState.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
private handleAttackingState(entity: Entity, sm: StateMachine): void {
|
||||
// Return to idle after attack animation
|
||||
if (sm.stateTimer > 0.5) {
|
||||
sm.changeState(EntityState.Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI Behavior System
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AIBehavior')
|
||||
class AIBehaviorSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(AIComponent, Transform).none(Dead));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const ai = entity.getComponent(AIComponent)!;
|
||||
const transform = entity.getComponent(Transform)!;
|
||||
|
||||
switch (ai.state) {
|
||||
case AIState.Patrol:
|
||||
this.patrol(entity, ai, transform);
|
||||
break;
|
||||
case AIState.Chase:
|
||||
this.chase(entity, ai, transform);
|
||||
break;
|
||||
case AIState.Attack:
|
||||
this.attack(entity, ai);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private patrol(entity: Entity, ai: AIComponent, transform: Transform): void {
|
||||
// Move to patrol point
|
||||
const target = ai.patrolPoints[ai.currentPatrolIndex];
|
||||
const dx = target.x - transform.x;
|
||||
const dy = target.y - transform.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 5) {
|
||||
// Reached patrol point, move to next
|
||||
ai.currentPatrolIndex = (ai.currentPatrolIndex + 1) % ai.patrolPoints.length;
|
||||
} else {
|
||||
// Move towards patrol point
|
||||
const speed = ai.moveSpeed * Time.deltaTime;
|
||||
transform.x += (dx / distance) * speed;
|
||||
transform.y += (dy / distance) * speed;
|
||||
}
|
||||
|
||||
// Detect player
|
||||
if (this.detectPlayer(entity, ai)) {
|
||||
ai.state = AIState.Chase;
|
||||
}
|
||||
}
|
||||
|
||||
private chase(entity: Entity, ai: AIComponent, transform: Transform): void {
|
||||
const player = this.findPlayer();
|
||||
if (!player) {
|
||||
ai.state = AIState.Patrol;
|
||||
return;
|
||||
}
|
||||
|
||||
const playerTransform = player.getComponent(Transform)!;
|
||||
const dx = playerTransform.x - transform.x;
|
||||
const dy = playerTransform.y - transform.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < ai.attackRange) {
|
||||
ai.state = AIState.Attack;
|
||||
} else if (distance > ai.chaseRange) {
|
||||
ai.state = AIState.Patrol;
|
||||
} else {
|
||||
// Chase player
|
||||
const speed = ai.chaseSpeed * Time.deltaTime;
|
||||
transform.x += (dx / distance) * speed;
|
||||
transform.y += (dy / distance) * speed;
|
||||
}
|
||||
}
|
||||
|
||||
private attack(entity: Entity, ai: AIComponent): void {
|
||||
ai.attackCooldown -= Time.deltaTime;
|
||||
if (ai.attackCooldown <= 0) {
|
||||
// Execute attack
|
||||
this.scene?.eventSystem.emitSync('ai_attack', { attacker: entity });
|
||||
ai.attackCooldown = ai.attackInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private detectPlayer(entity: Entity, ai: AIComponent): boolean {
|
||||
// Player detection logic
|
||||
return false;
|
||||
}
|
||||
|
||||
private findPlayer(): Entity | null {
|
||||
const result = this.scene?.querySystem.queryByTag(Tags.PLAYER);
|
||||
return result?.entities[0] ?? null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How big should a system be?
|
||||
|
||||
**A**: Follow the single responsibility principle. If a system handles multiple unrelated logic, split it into multiple systems.
|
||||
|
||||
### Q: How to share data between systems?
|
||||
|
||||
**A**:
|
||||
1. Share data through components (recommended)
|
||||
2. Communicate through event system
|
||||
3. Inject shared services through service container
|
||||
|
||||
### Q: When to use CommandBuffer?
|
||||
|
||||
**A**: When you need to destroy entities during iteration, use CommandBuffer. Adding/removing components can be done directly.
|
||||
|
||||
### Q: How to optimize processing of many entities?
|
||||
|
||||
**A**:
|
||||
1. Use change detection, only process changed entities
|
||||
2. Use WorkerEntitySystem for parallel processing
|
||||
3. Optimize Matcher conditions, reduce matched entities
|
||||
4. Consider using Spatial Partitioning
|
||||
|
||||
---
|
||||
|
||||
Systems are the logic processing core of ECS architecture. Properly designing and using systems makes your game code more modular, efficient, and maintainable.
|
||||
252
docs/src/content/docs/en/guide/system/change-detection.md
Normal file
252
docs/src/content/docs/en/guide/system/change-detection.md
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
title: "Change Detection"
|
||||
description: "Frame-level change detection to optimize system performance"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
The framework provides epoch-based frame-level change detection, allowing systems to process only entities that have changed, significantly improving performance.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Epoch**: Global frame counter, incremented each frame
|
||||
- **lastWriteEpoch**: The epoch when a component was last modified
|
||||
- **Change Detection**: Determine if component changed after a specific point by comparing epochs
|
||||
|
||||
## Marking Components as Modified
|
||||
|
||||
After modifying component data, you need to mark the component as changed. There are two approaches:
|
||||
|
||||
### Approach 1: Via Entity Helper Method (Recommended)
|
||||
|
||||
```typescript
|
||||
// Mark component dirty via entity.markDirty() after modification
|
||||
const pos = entity.getComponent(Position)!;
|
||||
pos.x = 100;
|
||||
pos.y = 200;
|
||||
entity.markDirty(pos);
|
||||
|
||||
// Can mark multiple components at once
|
||||
const vel = entity.getComponent(Velocity)!;
|
||||
vel.vx = 10;
|
||||
entity.markDirty(pos, vel);
|
||||
```
|
||||
|
||||
### Approach 2: Encapsulate in Component
|
||||
|
||||
```typescript
|
||||
class VelocityComponent extends Component {
|
||||
private _vx: number = 0;
|
||||
private _vy: number = 0;
|
||||
|
||||
// Provide modification method that accepts epoch parameter
|
||||
public setVelocity(vx: number, vy: number, epoch: number): void {
|
||||
this._vx = vx;
|
||||
this._vy = vy;
|
||||
this.markDirty(epoch);
|
||||
}
|
||||
|
||||
public get vx(): number { return this._vx; }
|
||||
public get vy(): number { return this._vy; }
|
||||
}
|
||||
|
||||
// Usage in system
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
vel.setVelocity(10, 20, this.currentEpoch);
|
||||
```
|
||||
|
||||
## Using Change Detection in Systems
|
||||
|
||||
EntitySystem provides several change detection helper methods:
|
||||
|
||||
### forEachChanged - Iterate Changed Entities
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Use forEachChanged to process only changed entities
|
||||
// Automatically saves epoch checkpoint
|
||||
this.forEachChanged(entities, [Velocity], (entity) => {
|
||||
const pos = this.requireComponent(entity, Position);
|
||||
const vel = this.requireComponent(entity, Velocity);
|
||||
|
||||
// Only update position when Velocity changes
|
||||
pos.x += vel.vx * Time.deltaTime;
|
||||
pos.y += vel.vy * Time.deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### filterChanged - Get List of Changed Entities
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Transform')
|
||||
class TransformSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Use filterChanged to get list of changed entities
|
||||
const changedEntities = this.filterChanged(entities, [RigidBody]);
|
||||
|
||||
for (const entity of changedEntities) {
|
||||
// Process entities with changed physics state
|
||||
this.updatePhysics(entity);
|
||||
}
|
||||
|
||||
// Manually save epoch checkpoint
|
||||
this.saveEpoch();
|
||||
}
|
||||
|
||||
protected updatePhysics(entity: Entity): void {
|
||||
// Physics update logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### hasChanged - Check Single Entity
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
// Check if single entity's specified components have changed
|
||||
if (this.hasChanged(entity, [Transform])) {
|
||||
this.updateRenderData(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Change Detection API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `forEachChanged(entities, [Types], callback)` | Iterate entities with changed specified components, auto-saves checkpoint |
|
||||
| `filterChanged(entities, [Types])` | Return array of entities with changed specified components |
|
||||
| `hasChanged(entity, [Types])` | Check if single entity's specified components have changed |
|
||||
| `saveEpoch()` | Manually save current epoch as checkpoint |
|
||||
| `lastProcessEpoch` | Get last saved epoch checkpoint |
|
||||
| `currentEpoch` | Get current scene epoch |
|
||||
|
||||
## Use Cases
|
||||
|
||||
Change detection is particularly suitable for:
|
||||
|
||||
### 1. Dirty Flag Optimization
|
||||
|
||||
Only update rendering when data changes:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('RenderUpdate')
|
||||
class RenderUpdateSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Sprite));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only update changed sprites
|
||||
this.forEachChanged(entities, [Transform, Sprite], (entity) => {
|
||||
const transform = this.requireComponent(entity, Transform);
|
||||
const sprite = this.requireComponent(entity, Sprite);
|
||||
|
||||
this.updateSpriteMatrix(sprite, transform);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Network Sync
|
||||
|
||||
Only send changed component data:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('NetworkSync')
|
||||
class NetworkSyncSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(NetworkComponent, Transform));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only sync changed entities, greatly reducing network traffic
|
||||
this.forEachChanged(entities, [Transform], (entity) => {
|
||||
const transform = this.requireComponent(entity, Transform);
|
||||
const network = this.requireComponent(entity, NetworkComponent);
|
||||
|
||||
this.sendTransformUpdate(network.id, transform);
|
||||
});
|
||||
}
|
||||
|
||||
private sendTransformUpdate(id: string, transform: Transform): void {
|
||||
// Send network update
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Physics Sync
|
||||
|
||||
Only sync entities with changed position/velocity:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('PhysicsSync')
|
||||
class PhysicsSyncSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Sync changed entities from physics engine
|
||||
this.forEachChanged(entities, [RigidBody], (entity) => {
|
||||
const transform = entity.getComponent(Transform)!;
|
||||
const rigidBody = entity.getComponent(RigidBody)!;
|
||||
|
||||
// Update Transform
|
||||
transform.position = rigidBody.getPosition();
|
||||
transform.rotation = rigidBody.getRotation();
|
||||
|
||||
// Mark Transform as changed
|
||||
entity.markDirty(transform);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Cache Invalidation
|
||||
|
||||
Only recalculate when dependent data changes:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('PathCache')
|
||||
class PathCacheSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(PathFinder, Transform));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Only recalculate path when position changes
|
||||
this.forEachChanged(entities, [Transform], (entity) => {
|
||||
const pathFinder = entity.getComponent(PathFinder)!;
|
||||
pathFinder.invalidateCache();
|
||||
pathFinder.recalculatePath();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Scenario | Without Change Detection | With Change Detection | Improvement |
|
||||
|----------|--------------------------|----------------------|-------------|
|
||||
| 1000 entities, 10% changed | 1000 processes | 100 processes | 10x |
|
||||
| 1000 entities, 1% changed | 1000 processes | 10 processes | 100x |
|
||||
| Network sync | Full send | Incremental send | 90%+ bandwidth saved |
|
||||
|
||||
:::tip
|
||||
Change detection works best for "many entities, few changes" scenarios. If most entities change every frame, the overhead of change detection may not be worthwhile.
|
||||
:::
|
||||
172
docs/src/content/docs/en/guide/system/command-buffer.md
Normal file
172
docs/src/content/docs/en/guide/system/command-buffer.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: "Command Buffer"
|
||||
description: "Use CommandBuffer for deferred entity operations"
|
||||
---
|
||||
|
||||
> **v2.3.0+**
|
||||
|
||||
CommandBuffer provides a mechanism for deferred execution of entity operations. When you need to destroy entities or perform other operations that might affect iteration during processing, CommandBuffer allows you to defer these operations to the end of the frame.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Every EntitySystem has a built-in `commands` property:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// Use command buffer to defer component removal
|
||||
this.commands.removeComponent(entity, DamageReceiver);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// Defer adding death marker
|
||||
this.commands.addComponent(entity, new Dead());
|
||||
// Defer entity destruction
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Commands
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `addComponent(entity, component)` | Defer adding component |
|
||||
| `removeComponent(entity, ComponentType)` | Defer removing component |
|
||||
| `destroyEntity(entity)` | Defer destroying entity |
|
||||
| `setEntityActive(entity, active)` | Defer setting entity active state |
|
||||
|
||||
## Execution Timing
|
||||
|
||||
Commands in the buffer are automatically executed after the `lateUpdate` phase of each frame. Execution order matches the order commands were queued.
|
||||
|
||||
```
|
||||
Scene Update Flow:
|
||||
1. onBegin()
|
||||
2. process()
|
||||
3. lateProcess()
|
||||
4. onEnd()
|
||||
5. flushCommandBuffers() <-- Commands execute here
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
CommandBuffer is suitable for:
|
||||
|
||||
### 1. Destroying Entities During Iteration
|
||||
|
||||
Avoid modifying collection being traversed:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyDeath')
|
||||
class EnemyDeathSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
// Play death animation, spawn loot, etc.
|
||||
this.spawnLoot(entity);
|
||||
|
||||
// Defer destruction, doesn't affect current iteration
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private spawnLoot(entity: Entity): void {
|
||||
// Loot spawning logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Batch Deferred Operations
|
||||
|
||||
Merge multiple operations to execute at end of frame:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Cleanup')
|
||||
class CleanupSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(MarkedForDeletion));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
// Batch collect entities to delete
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
// All destruction operations execute at frame end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Cross-System Coordination
|
||||
|
||||
One system marks, another system responds:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
if (this.shouldDie(entity)) {
|
||||
// Mark as dead
|
||||
this.commands.addComponent(entity, new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('DeathHandler')
|
||||
class DeathHandlerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Dead));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
// Handle death logic
|
||||
this.playDeathAnimation(entity);
|
||||
this.commands.destroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Commands skip already destroyed entities (safety check)
|
||||
- Single command failure doesn't affect other commands
|
||||
- Commands execute in queue order
|
||||
- Command queue clears after each `flush()`
|
||||
|
||||
## Direct Modification vs Command Buffer
|
||||
|
||||
| Operation | Direct Modification | Command Buffer |
|
||||
|-----------|---------------------|----------------|
|
||||
| Add component | ✅ Safe | ✅ Safe |
|
||||
| Remove component | ✅ Safe | ✅ Safe |
|
||||
| Destroy entity | ⚠️ May cause issues | ✅ Recommended |
|
||||
| Execution timing | Immediate | End of frame |
|
||||
|
||||
:::tip
|
||||
While adding/removing components in `process` is safe (due to snapshot mechanism), if you need to destroy entities, using command buffer is recommended.
|
||||
:::
|
||||
170
docs/src/content/docs/en/guide/system/index.md
Normal file
170
docs/src/content/docs/en/guide/system/index.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "System Architecture"
|
||||
description: "ECS System Overview and Basic Concepts"
|
||||
---
|
||||
|
||||
In ECS architecture, Systems are where business logic is processed. Systems are responsible for performing operations on entities that have specific component combinations, serving as the logic processing units of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Systems are concrete classes that inherit from the `EntitySystem` abstract base class, used for:
|
||||
- Defining entity processing logic (such as movement, collision detection, rendering, etc.)
|
||||
- Filtering entities based on component combinations
|
||||
- Providing lifecycle management and performance monitoring
|
||||
- Managing entity add/remove events
|
||||
|
||||
## Quick Example
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Use Matcher to define entity conditions to process
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Properties and Methods
|
||||
|
||||
### Important Properties
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
showSystemInfo(): void {
|
||||
console.log(`System name: ${this.systemName}`); // System name
|
||||
console.log(`Update order: ${this.updateOrder}`); // Update order
|
||||
console.log(`Is enabled: ${this.enabled}`); // Enabled state
|
||||
console.log(`Entity count: ${this.entities.length}`); // Number of matched entities
|
||||
console.log(`Scene: ${this.scene?.name}`); // Parent scene
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Access
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Method 1: Use entity list from parameter
|
||||
for (const entity of entities) {
|
||||
// Process entity
|
||||
}
|
||||
|
||||
// Method 2: Use this.entities property (same as parameter)
|
||||
for (const entity of this.entities) {
|
||||
// Process entity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling System Execution
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Conditional')
|
||||
class ConditionalSystem extends EntitySystem {
|
||||
private shouldProcess = true;
|
||||
|
||||
protected onCheckProcessing(): boolean {
|
||||
// Return false to skip this processing
|
||||
return this.shouldProcess && this.entities.length > 0;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.shouldProcess = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.shouldProcess = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
### Adding Systems to Scene
|
||||
|
||||
The framework provides two ways to add systems: pass an instance or pass a type (automatic dependency injection).
|
||||
|
||||
```typescript
|
||||
// Add systems in scene subclass
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Method 1: Pass instance
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Method 2: Pass type (automatic dependency injection)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// Set system update order
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Update Order
|
||||
|
||||
System execution order is determined by the `updateOrder` property. Lower values execute first:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(InputComponent));
|
||||
this.updateOrder = -100; // Input system executes first
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(RigidBody));
|
||||
this.updateOrder = 0; // Default order
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
this.updateOrder = 100; // Render system executes last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stable Sorting: addOrder
|
||||
|
||||
When multiple systems have the same `updateOrder`, the framework uses `addOrder` (add order) as a secondary sorting criterion:
|
||||
|
||||
```typescript
|
||||
// Both systems have default updateOrder of 0
|
||||
scene.addSystem(new SystemA()); // addOrder = 0, executes first
|
||||
scene.addSystem(new SystemB()); // addOrder = 1, executes second
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [System Types](/en/guide/system/types) - Learn about different system base classes
|
||||
- [System Lifecycle](/en/guide/system/lifecycle) - Lifecycle callbacks and event listening
|
||||
- [Command Buffer](/en/guide/system/command-buffer) - Deferred entity operations
|
||||
- [System Scheduling](/en/guide/system/scheduling) - Declarative system scheduling
|
||||
- [Change Detection](/en/guide/system/change-detection) - Frame-level change detection optimization
|
||||
- [Best Practices](/en/guide/system/best-practices) - System design best practices
|
||||
249
docs/src/content/docs/en/guide/system/lifecycle.md
Normal file
249
docs/src/content/docs/en/guide/system/lifecycle.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
title: "System Lifecycle"
|
||||
description: "System lifecycle callbacks, event listening and dependency injection"
|
||||
---
|
||||
|
||||
## Lifecycle Callbacks
|
||||
|
||||
Systems provide complete lifecycle callbacks:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
console.log('System initialized');
|
||||
// Called when system is added to scene, for initializing resources
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Called before each frame's processing begins
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Main processing logic
|
||||
for (const entity of entities) {
|
||||
// Process each entity
|
||||
// ✅ Safe to add/remove components here without affecting current iteration
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// Post-processing after main process
|
||||
// ✅ Safe to add/remove components here without affecting current iteration
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Called after each frame's processing ends
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('System destroyed');
|
||||
// Called when system is removed from scene, for cleaning up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Event Listening
|
||||
|
||||
Systems can listen for entity add and remove events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyManager')
|
||||
class EnemyManagerSystem extends EntitySystem {
|
||||
private enemyCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.enemyCount++;
|
||||
console.log(`Enemy joined battle, current enemy count: ${this.enemyCount}`);
|
||||
|
||||
// Can set initial state for new enemies here
|
||||
const health = entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.current = health.max;
|
||||
}
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.enemyCount--;
|
||||
console.log(`Enemy removed, remaining enemies: ${this.enemyCount}`);
|
||||
|
||||
// Check if all enemies are defeated
|
||||
if (this.enemyCount === 0) {
|
||||
this.scene?.eventSystem.emitSync('all_enemies_defeated');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important: Timing of onAdded/onRemoved Calls
|
||||
|
||||
:::caution
|
||||
`onAdded` and `onRemoved` callbacks are called **synchronously**, executing immediately **before** `addComponent`/`removeComponent` returns.
|
||||
:::
|
||||
|
||||
This means:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Chain assignment executes after onAdded
|
||||
const comp = entity.addComponent(new ClickComponent());
|
||||
comp.element = this._element; // At this point onAdded has already executed!
|
||||
|
||||
// ✅ Correct: Pass initial values through constructor
|
||||
const comp = entity.addComponent(new ClickComponent(this._element));
|
||||
|
||||
// ✅ Or use the createComponent method
|
||||
const comp = entity.createComponent(ClickComponent, this._element);
|
||||
```
|
||||
|
||||
**Why this design?**
|
||||
|
||||
The event-driven design ensures that `onAdded`/`onRemoved` callbacks are not affected by system registration order. When a component is added, all systems listening for that component receive notification immediately, rather than waiting until the next frame.
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
1. Component initial values should be passed through the **constructor**
|
||||
2. Don't rely on setting properties after `addComponent` returns
|
||||
3. If you need to access component properties in `onAdded`, ensure those properties are set at construction time
|
||||
|
||||
### Safely Modifying Components in process/lateProcess
|
||||
|
||||
When iterating entities in `process` or `lateProcess`, you can safely add or remove components without affecting the current iteration:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// ✅ Safe: removing component won't affect current iteration
|
||||
entity.removeComponent(damage);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// ✅ Safe: adding component won't affect current iteration
|
||||
entity.addComponent(new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The framework creates a snapshot of the entity list before each `process`/`lateProcess` call, ensuring that component changes during iteration won't cause entities to be skipped or processed multiple times.
|
||||
|
||||
## Event System Integration
|
||||
|
||||
Systems can conveniently listen for and send events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('GameLogic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// Add event listeners (automatically cleaned up when system is destroyed)
|
||||
this.addEventListener('player_died', this.onPlayerDied.bind(this));
|
||||
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died, restarting game');
|
||||
// Handle player death logic
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete, loading next level');
|
||||
// Handle level completion logic
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Send events during processing
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('entity_died', { entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
Systems have built-in performance monitoring:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Performance')
|
||||
class PerformanceSystem extends EntitySystem {
|
||||
protected onEnd(): void {
|
||||
// Get performance data
|
||||
const perfData = this.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log(`Execution time: ${perfData.executionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Get performance statistics
|
||||
const stats = this.getPerformanceStats();
|
||||
if (stats) {
|
||||
console.log(`Average execution time: ${stats.averageTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
public resetPerformance(): void {
|
||||
this.resetPerformanceData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Dependency Injection
|
||||
|
||||
Systems implement the `IService` interface and support obtaining other services or systems through dependency injection:
|
||||
|
||||
```typescript
|
||||
import { ECSSystem, Injectable, InjectProperty } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
@InjectProperty(CollisionService)
|
||||
private collision!: CollisionService;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Use injected service
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// Implement IService interface dispose method
|
||||
public dispose(): void {
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
|
||||
// Just pass the type when using, framework will auto-inject dependencies
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Automatic dependency injection
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Use `@Injectable()` decorator to mark systems that need dependency injection
|
||||
- Use `@InjectProperty()` decorator to declare dependencies
|
||||
- Systems must implement the `dispose()` method (IService interface requirement)
|
||||
- Use `addEntityProcessor(Type)` instead of `addSystem(new Type())` to enable dependency injection
|
||||
182
docs/src/content/docs/en/guide/system/scheduling.md
Normal file
182
docs/src/content/docs/en/guide/system/scheduling.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "System Scheduling"
|
||||
description: "Declarative system scheduling and execution stages"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
In addition to manually controlling execution order with `updateOrder`, the framework provides a declarative system scheduling mechanism that lets you define execution order through dependencies.
|
||||
|
||||
## Scheduling Decorators
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
|
||||
|
||||
// Use decorators to declare system scheduling
|
||||
@ECSSystem('Movement')
|
||||
@Stage('update') // Execute in update stage
|
||||
@After('InputSystem') // Execute after InputSystem
|
||||
@Before('RenderSystem') // Execute before RenderSystem
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Movement logic
|
||||
}
|
||||
}
|
||||
|
||||
// Use system sets for grouping
|
||||
@ECSSystem('Physics')
|
||||
@Stage('update')
|
||||
@InSet('CoreSystems') // Belongs to CoreSystems set
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// ...
|
||||
}
|
||||
|
||||
@ECSSystem('Collision')
|
||||
@Stage('update')
|
||||
@After('set:CoreSystems') // Execute after all systems in CoreSystems set
|
||||
class CollisionSystem extends EntitySystem {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## System Execution Stages
|
||||
|
||||
The framework defines the following system execution stages, executed in order:
|
||||
|
||||
| Stage | Description | Typical Usage |
|
||||
|-------|-------------|---------------|
|
||||
| `startup` | Startup stage | One-time initialization |
|
||||
| `preUpdate` | Pre-update stage | Input handling, state preparation |
|
||||
| `update` | Main update stage (default) | Core game logic |
|
||||
| `postUpdate` | Post-update stage | Physics, collision detection |
|
||||
| `cleanup` | Cleanup stage | Resource cleanup, state reset |
|
||||
|
||||
### Stage Usage Example
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
@Stage('preUpdate') // Handle input in pre-update stage
|
||||
class InputSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Read input, update input components
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Movement')
|
||||
@Stage('update') // Handle movement in main update stage
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Move entities based on input
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
@Stage('postUpdate') // Handle physics in post-update stage
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Physics simulation and collision detection
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Cleanup')
|
||||
@Stage('cleanup') // Reset state in cleanup stage
|
||||
class CleanupSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Clean up temporary data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fluent API Configuration
|
||||
|
||||
If you prefer not to use decorators, you can configure scheduling at runtime using the Fluent API:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
|
||||
// Configure scheduling using Fluent API
|
||||
this.stage('update')
|
||||
.after('InputSystem')
|
||||
.before('RenderSystem')
|
||||
.inSet('CoreSystems');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Sets
|
||||
|
||||
System sets allow you to group related systems, then define dependencies based on the entire set:
|
||||
|
||||
```typescript
|
||||
// Define core systems set
|
||||
@ECSSystem('Movement')
|
||||
@InSet('CoreSystems')
|
||||
class MovementSystem extends EntitySystem { }
|
||||
|
||||
@ECSSystem('Physics')
|
||||
@InSet('CoreSystems')
|
||||
class PhysicsSystem extends EntitySystem { }
|
||||
|
||||
@ECSSystem('AI')
|
||||
@InSet('CoreSystems')
|
||||
class AISystem extends EntitySystem { }
|
||||
|
||||
// Execute after core systems set
|
||||
@ECSSystem('Render')
|
||||
@After('set:CoreSystems')
|
||||
class RenderSystem extends EntitySystem { }
|
||||
|
||||
// Execute before core systems set
|
||||
@ECSSystem('Input')
|
||||
@Before('set:CoreSystems')
|
||||
class InputSystem extends EntitySystem { }
|
||||
```
|
||||
|
||||
## Cycle Dependency Detection
|
||||
|
||||
The framework automatically detects cyclic dependencies and throws clear errors:
|
||||
|
||||
```typescript
|
||||
// This will cause a cycle dependency error
|
||||
@ECSSystem('SystemA')
|
||||
@Before('SystemB')
|
||||
class SystemA extends EntitySystem { }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
@Before('SystemA') // Error: A -> B -> A forms a cycle
|
||||
class SystemB extends EntitySystem { }
|
||||
|
||||
// Error message: Cyclic dependency detected: SystemA -> SystemB -> SystemA
|
||||
```
|
||||
|
||||
## Scheduling Decorator Reference
|
||||
|
||||
| Decorator | Description | Example |
|
||||
|-----------|-------------|---------|
|
||||
| `@Stage(name)` | Specify execution stage | `@Stage('update')` |
|
||||
| `@Before(system)` | Execute before specified system | `@Before('RenderSystem')` |
|
||||
| `@After(system)` | Execute after specified system | `@After('InputSystem')` |
|
||||
| `@InSet(name)` | Join system set | `@InSet('CoreSystems')` |
|
||||
| `@Before('set:name')` | Execute before set | `@Before('set:UI')` |
|
||||
| `@After('set:name')` | Execute after set | `@After('set:Physics')` |
|
||||
|
||||
## updateOrder vs Declarative Scheduling
|
||||
|
||||
| Feature | updateOrder | Declarative Scheduling |
|
||||
|---------|-------------|------------------------|
|
||||
| Configuration | Manually set values | Declare dependencies |
|
||||
| Readability | Need to remember value meanings | Directly express intent |
|
||||
| Cycle detection | None | Automatic |
|
||||
| Refactor-friendly | Manually adjust values | Auto-handles ordering |
|
||||
| Use case | Simple projects | Complex dependencies |
|
||||
|
||||
:::tip
|
||||
For small projects, `updateOrder` is sufficient. When system count increases and dependencies become complex, declarative scheduling is recommended.
|
||||
:::
|
||||
200
docs/src/content/docs/en/guide/system/types.md
Normal file
200
docs/src/content/docs/en/guide/system/types.md
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: "System Types"
|
||||
description: "EntitySystem, ProcessingSystem, PassiveSystem, IntervalSystem and other base classes"
|
||||
---
|
||||
|
||||
The framework provides several different system base classes for different use cases.
|
||||
|
||||
## EntitySystem - Base System
|
||||
|
||||
The most basic system class, all other systems inherit from it:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Use Matcher to define entity conditions to process
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ProcessingSystem - Processing System
|
||||
|
||||
Suitable for systems that don't need to process entities individually:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(); // No Matcher needed
|
||||
}
|
||||
|
||||
public processSystem(): void {
|
||||
// Execute physics world step
|
||||
this.physicsWorld.step(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PassiveSystem - Passive System
|
||||
|
||||
Passive systems don't actively process, mainly used for listening to entity add and remove events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EntityTracker')
|
||||
class EntityTrackerSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
console.log(`Health entity added: ${entity.name}`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
console.log(`Health entity removed: ${entity.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## IntervalSystem - Interval System
|
||||
|
||||
Systems that execute at fixed time intervals:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AutoSave')
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// Execute every 5 seconds
|
||||
super(5.0, Matcher.all(SaveData));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
console.log('Executing auto save...');
|
||||
// Save game data
|
||||
this.saveGameData(entities);
|
||||
}
|
||||
|
||||
private saveGameData(entities: readonly Entity[]): void {
|
||||
// Save logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WorkerEntitySystem - Multi-threaded System
|
||||
|
||||
A Web Worker-based multi-threaded processing system, suitable for compute-intensive tasks, capable of fully utilizing multi-core CPU performance.
|
||||
|
||||
Worker systems provide true parallel computing capabilities, support SharedArrayBuffer optimization, and have automatic fallback support. Particularly suitable for physics simulation, particle systems, AI computation, and similar scenarios.
|
||||
|
||||
**For detailed content, please refer to: [Worker System](/en/guide/worker-system)**
|
||||
|
||||
## Entity Matcher
|
||||
|
||||
Matcher is used to define which entities a system needs to process. It provides flexible condition combinations:
|
||||
|
||||
### Basic Match Conditions
|
||||
|
||||
```typescript
|
||||
// Must have both Position and Velocity components
|
||||
const matcher1 = Matcher.all(Position, Velocity);
|
||||
|
||||
// Must have at least one of Health or Shield components
|
||||
const matcher2 = Matcher.any(Health, Shield);
|
||||
|
||||
// Must not have Dead component
|
||||
const matcher3 = Matcher.none(Dead);
|
||||
```
|
||||
|
||||
### Compound Match Conditions
|
||||
|
||||
```typescript
|
||||
// Complex combination conditions
|
||||
const complexMatcher = Matcher.all(Position, Velocity)
|
||||
.any(Player, Enemy)
|
||||
.none(Dead, Disabled);
|
||||
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(complexMatcher);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Special Match Conditions
|
||||
|
||||
```typescript
|
||||
// Match by tag
|
||||
const tagMatcher = Matcher.byTag(1); // Match entities with tag 1
|
||||
|
||||
// Match by name
|
||||
const nameMatcher = Matcher.byName("Player"); // Match entities named "Player"
|
||||
|
||||
// Single component match
|
||||
const componentMatcher = Matcher.byComponent(Health); // Match entities with Health component
|
||||
|
||||
// Match no entities
|
||||
const nothingMatcher = Matcher.nothing(); // For systems that only need lifecycle callbacks
|
||||
```
|
||||
|
||||
### Empty Matcher vs Nothing Matcher
|
||||
|
||||
```typescript
|
||||
// empty() - Empty condition, matches all entities
|
||||
const emptyMatcher = Matcher.empty();
|
||||
|
||||
// nothing() - Matches no entities, for systems that only need lifecycle methods
|
||||
const nothingMatcher = Matcher.nothing();
|
||||
|
||||
// Use case: Systems that only need onBegin/onEnd lifecycle
|
||||
@ECSSystem('FrameTimer')
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing()); // Process no entities
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at the start of each frame
|
||||
console.log('Frame started');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because there are no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at the end of each frame
|
||||
console.log('Frame ended');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
For more details on Matcher and entity queries, please refer to the [Entity Query System](/en/guide/entity-query) documentation.
|
||||
:::
|
||||
|
||||
## System Type Selection Guide
|
||||
|
||||
| System Type | Use Case |
|
||||
|-------------|----------|
|
||||
| `EntitySystem` | General system that processes matched entities one by one |
|
||||
| `ProcessingSystem` | No entity list needed, only executes global logic |
|
||||
| `PassiveSystem` | Only listens for entity add/remove events |
|
||||
| `IntervalSystem` | Executes at fixed time intervals |
|
||||
| `WorkerEntitySystem` | Compute-intensive tasks requiring multi-threading |
|
||||
@@ -1,4 +1,6 @@
|
||||
# Time and Timer System
|
||||
---
|
||||
title: "Time and Timer System"
|
||||
---
|
||||
|
||||
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Worker System
|
||||
---
|
||||
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.
|
||||
|
||||
67
docs/src/content/docs/en/index.mdx
Normal file
67
docs/src/content/docs/en/index.mdx
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: ESEngine
|
||||
description: High-performance TypeScript ECS Framework for Game Development
|
||||
template: splash
|
||||
hero:
|
||||
tagline: High-performance ECS game framework based on TypeScript, with multi-platform support and modular design
|
||||
image:
|
||||
file: ../../../assets/logo.svg
|
||||
actions:
|
||||
- text: Quick Start
|
||||
link: /en/guide/getting-started/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: GitHub
|
||||
link: https://github.com/esengine/esengine
|
||||
icon: external
|
||||
---
|
||||
|
||||
import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
|
||||
|
||||
## Core Features
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="High-Performance ECS Architecture" icon="rocket">
|
||||
Data-driven entity component system supporting large-scale entity processing with cache-friendly memory layout.
|
||||
</Card>
|
||||
<Card title="Full TypeScript Support" icon="seti:typescript">
|
||||
100% TypeScript with complete type definitions and compile-time checks for the best development experience.
|
||||
</Card>
|
||||
<Card title="Visual Behavior Tree" icon="puzzle">
|
||||
Built-in AI behavior tree system with visual editor, custom nodes and real-time debugging support.
|
||||
</Card>
|
||||
<Card title="Multi-Platform Support" icon="laptop">
|
||||
Supports browsers, Node.js, WeChat Mini Games, and integrates seamlessly with mainstream game engines.
|
||||
</Card>
|
||||
<Card title="Modular Design" icon="setting">
|
||||
Core features packaged independently. Import on demand with custom plugin extensions for flexible project adaptation.
|
||||
</Card>
|
||||
<Card title="Developer Tools" icon="open-book">
|
||||
Built-in performance monitoring, debugging tools, serialization system and complete development toolchain.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Quick Links
|
||||
|
||||
<CardGrid>
|
||||
<LinkCard
|
||||
title="5-Minute Start"
|
||||
description="From installation to creating your first ECS application, quickly understand core concepts."
|
||||
href="/en/guide/getting-started/"
|
||||
/>
|
||||
<LinkCard
|
||||
title="Behavior Tree System"
|
||||
description="Built-in AI behavior tree system with visual editing and real-time debugging."
|
||||
href="/en/modules/behavior-tree/"
|
||||
/>
|
||||
<LinkCard
|
||||
title="API Reference"
|
||||
description="Complete API documentation with detailed usage for every class and method."
|
||||
href="/en/api/"
|
||||
/>
|
||||
<LinkCard
|
||||
title="Example Projects"
|
||||
description="View complete example projects and learn best practices."
|
||||
href="/en/examples/"
|
||||
/>
|
||||
</CardGrid>
|
||||
@@ -1,4 +1,6 @@
|
||||
# Blueprint Visual Scripting
|
||||
---
|
||||
title: "Blueprint Visual Scripting"
|
||||
---
|
||||
|
||||
`@esengine/blueprint` provides a full-featured visual scripting system supporting node-based programming, event-driven execution, and blueprint composition.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# State Machine (FSM)
|
||||
---
|
||||
title: "State Machine (FSM)"
|
||||
---
|
||||
|
||||
`@esengine/fsm` provides a type-safe finite state machine implementation for characters, AI, or any scenario requiring state management.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Modules
|
||||
---
|
||||
title: "Modules"
|
||||
---
|
||||
|
||||
ESEngine provides a rich set of modules that can be imported as needed.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Network System
|
||||
---
|
||||
title: "Network System"
|
||||
---
|
||||
|
||||
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Pathfinding System
|
||||
---
|
||||
title: "Pathfinding System"
|
||||
---
|
||||
|
||||
`@esengine/pathfinding` provides a complete 2D pathfinding solution including A* algorithm, grid maps, navigation meshes, and path smoothing.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Procedural Generation (Procgen)
|
||||
---
|
||||
title: "Procedural Generation (Procgen)"
|
||||
---
|
||||
|
||||
`@esengine/procgen` provides core tools for procedural content generation, including noise functions, seeded random numbers, and various random utilities.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Spatial Index System
|
||||
---
|
||||
title: "Spatial Index System"
|
||||
---
|
||||
|
||||
`@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Timer System
|
||||
---
|
||||
title: "Timer System"
|
||||
---
|
||||
|
||||
`@esengine/timer` provides a flexible timer and cooldown system for delayed execution, repeating tasks, skill cooldowns, and more.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# 示例
|
||||
---
|
||||
title: "示例"
|
||||
---
|
||||
|
||||
这里展示了ECS Framework的各种使用示例,通过实际的演示帮助您理解框架的功能和最佳实践。
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Worker系统演示
|
||||
---
|
||||
title: "Worker系统演示"
|
||||
---
|
||||
|
||||
这是一个展示ECS框架Worker系统功能的交互式演示。
|
||||
|
||||
312
docs/src/content/docs/guide/component/best-practices.md
Normal file
312
docs/src/content/docs/guide/component/best-practices.md
Normal file
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: "最佳实践"
|
||||
description: "组件设计模式和复杂示例"
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 保持组件简单
|
||||
|
||||
```typescript
|
||||
// ✅ 好的组件设计 - 单一职责
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
}
|
||||
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ❌ 避免的组件设计 - 职责过多
|
||||
@ECSComponent('GameObject')
|
||||
class GameObject extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
health: number;
|
||||
damage: number;
|
||||
sprite: string;
|
||||
// 太多不相关的属性
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用构造函数初始化
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
x: number;
|
||||
y: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
|
||||
constructor(x = 0, y = 0, rotation = 0, scale = 1) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 明确的类型定义
|
||||
|
||||
```typescript
|
||||
interface InventoryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: 'weapon' | 'consumable' | 'misc';
|
||||
}
|
||||
|
||||
@ECSComponent('Inventory')
|
||||
class Inventory extends Component {
|
||||
items: InventoryItem[] = [];
|
||||
maxSlots: number;
|
||||
|
||||
constructor(maxSlots: number = 20) {
|
||||
super();
|
||||
this.maxSlots = maxSlots;
|
||||
}
|
||||
|
||||
addItem(item: InventoryItem): boolean {
|
||||
if (this.items.length < this.maxSlots) {
|
||||
this.items.push(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
removeItem(itemId: string): InventoryItem | null {
|
||||
const index = this.items.findIndex(item => item.id === itemId);
|
||||
if (index !== -1) {
|
||||
return this.items.splice(index, 1)[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 引用其他实体
|
||||
|
||||
当组件需要关联其他实体时(如父子关系、跟随目标等),**推荐方式是存储实体ID**,然后在 System 中查找:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number;
|
||||
followDistance: number = 50;
|
||||
|
||||
constructor(targetId: number) {
|
||||
super();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 System 中查找目标实体并处理逻辑
|
||||
class FollowerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Follower, Position));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const position = entity.getComponent(Position)!;
|
||||
|
||||
// 通过场景查找目标实体
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
const targetPos = target.getComponent(Position);
|
||||
if (targetPos) {
|
||||
// 跟随逻辑
|
||||
const dx = targetPos.x - position.x;
|
||||
const dy = targetPos.y - position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > follower.followDistance) {
|
||||
// 移动靠近目标
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式的优势:
|
||||
- 组件保持简单,只存储基本数据类型
|
||||
- 符合数据导向设计
|
||||
- 在 System 中统一处理查找和逻辑
|
||||
- 易于理解和维护
|
||||
|
||||
**避免在组件中直接存储实体引用**:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误示范:直接存储实体引用
|
||||
@ECSComponent('BadFollower')
|
||||
class BadFollower extends Component {
|
||||
target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
|
||||
}
|
||||
```
|
||||
|
||||
## 复杂组件示例
|
||||
|
||||
### 状态机组件
|
||||
|
||||
```typescript
|
||||
enum EntityState {
|
||||
Idle,
|
||||
Moving,
|
||||
Attacking,
|
||||
Dead
|
||||
}
|
||||
|
||||
@ECSComponent('StateMachine')
|
||||
class StateMachine extends Component {
|
||||
private _currentState: EntityState = EntityState.Idle;
|
||||
private _previousState: EntityState = EntityState.Idle;
|
||||
private _stateTimer: number = 0;
|
||||
|
||||
get currentState(): EntityState {
|
||||
return this._currentState;
|
||||
}
|
||||
|
||||
get previousState(): EntityState {
|
||||
return this._previousState;
|
||||
}
|
||||
|
||||
get stateTimer(): number {
|
||||
return this._stateTimer;
|
||||
}
|
||||
|
||||
changeState(newState: EntityState): void {
|
||||
if (this._currentState !== newState) {
|
||||
this._previousState = this._currentState;
|
||||
this._currentState = newState;
|
||||
this._stateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
updateTimer(deltaTime: number): void {
|
||||
this._stateTimer += deltaTime;
|
||||
}
|
||||
|
||||
isInState(state: EntityState): boolean {
|
||||
return this._currentState === state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置数据组件
|
||||
|
||||
```typescript
|
||||
interface WeaponData {
|
||||
damage: number;
|
||||
range: number;
|
||||
fireRate: number;
|
||||
ammo: number;
|
||||
}
|
||||
|
||||
@ECSComponent('WeaponConfig')
|
||||
class WeaponConfig extends Component {
|
||||
data: WeaponData;
|
||||
|
||||
constructor(weaponData: WeaponData) {
|
||||
super();
|
||||
this.data = { ...weaponData }; // 深拷贝避免共享引用
|
||||
}
|
||||
|
||||
// 提供便捷的访问方法
|
||||
getDamage(): number {
|
||||
return this.data.damage;
|
||||
}
|
||||
|
||||
canFire(): boolean {
|
||||
return this.data.ammo > 0;
|
||||
}
|
||||
|
||||
consumeAmmo(): boolean {
|
||||
if (this.data.ammo > 0) {
|
||||
this.data.ammo--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 标签组件
|
||||
|
||||
```typescript
|
||||
// 标签组件:无数据,仅用于标识
|
||||
@ECSComponent('Player')
|
||||
class PlayerTag extends Component {}
|
||||
|
||||
@ECSComponent('Enemy')
|
||||
class EnemyTag extends Component {}
|
||||
|
||||
@ECSComponent('Dead')
|
||||
class DeadTag extends Component {}
|
||||
|
||||
// 使用标签进行查询
|
||||
class EnemySystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(EnemyTag, Health).none(DeadTag));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓冲组件
|
||||
|
||||
```typescript
|
||||
// 事件/命令缓冲组件
|
||||
@ECSComponent('DamageBuffer')
|
||||
class DamageBuffer extends Component {
|
||||
damages: { amount: number; source: number; timestamp: number }[] = [];
|
||||
|
||||
addDamage(amount: number, sourceId: number): void {
|
||||
this.damages.push({
|
||||
amount,
|
||||
source: sourceId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.damages.length = 0;
|
||||
}
|
||||
|
||||
getTotalDamage(): number {
|
||||
return this.damages.reduce((sum, d) => sum + d.amount, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 组件应该有多大?
|
||||
|
||||
**A**: 遵循单一职责原则。如果组件包含不相关的数据,拆分成多个组件。
|
||||
|
||||
### Q: 组件可以有方法吗?
|
||||
|
||||
**A**: 可以,但应该是数据相关的辅助方法(如 `isDead()`),而非业务逻辑。业务逻辑放在 System 中。
|
||||
|
||||
### Q: 如何处理组件间的依赖?
|
||||
|
||||
**A**: 在 System 中处理组件间交互,不要在组件内部直接访问其他组件。
|
||||
|
||||
### Q: 什么时候使用 EntityRef?
|
||||
|
||||
**A**: 仅在需要频繁访问引用实体且引用关系稳定时使用(如父子关系)。大多数情况存储 ID 更好。
|
||||
|
||||
---
|
||||
|
||||
组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。
|
||||
228
docs/src/content/docs/guide/component/entity-ref.md
Normal file
228
docs/src/content/docs/guide/component/entity-ref.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "EntityRef 装饰器"
|
||||
description: "安全的实体引用追踪机制"
|
||||
---
|
||||
|
||||
框架提供了 `@EntityRef` 装饰器用于**特殊场景**下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
|
||||
|
||||
## 什么时候需要 EntityRef?
|
||||
|
||||
在以下场景中,`@EntityRef` 可以简化代码:
|
||||
|
||||
1. **父子关系**: 需要在组件中直接访问父实体或子实体
|
||||
2. **复杂关联**: 实体之间有多个引用关系
|
||||
3. **频繁访问**: 需要在多处访问引用的实体,使用ID查找会有性能开销
|
||||
|
||||
## 核心特性
|
||||
|
||||
`@EntityRef` 装饰器通过 **ReferenceTracker** 自动追踪引用关系:
|
||||
|
||||
- 当被引用的实体销毁时,所有指向它的 `@EntityRef` 属性自动设为 `null`
|
||||
- 防止跨场景引用(会输出警告并拒绝设置)
|
||||
- 防止引用已销毁的实体(会输出警告并设为 `null`)
|
||||
- 使用 WeakRef 避免内存泄漏(自动GC支持)
|
||||
- 组件移除时自动清理引用注册
|
||||
|
||||
## 基本用法
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Parent')
|
||||
class ParentComponent extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const scene = new Scene();
|
||||
const parent = scene.createEntity('Parent');
|
||||
const child = scene.createEntity('Child');
|
||||
|
||||
const comp = child.addComponent(new ParentComponent());
|
||||
comp.parent = parent;
|
||||
|
||||
console.log(comp.parent); // Entity { name: 'Parent' }
|
||||
|
||||
// 当 parent 被销毁时,comp.parent 自动变为 null
|
||||
parent.destroy();
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## 多个引用属性
|
||||
|
||||
一个组件可以有多个 `@EntityRef` 属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Combat')
|
||||
class CombatComponent extends Component {
|
||||
@EntityRef()
|
||||
target: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
ally: Entity | null = null;
|
||||
|
||||
@EntityRef()
|
||||
lastAttacker: Entity | null = null;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const player = scene.createEntity('Player');
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
const npc = scene.createEntity('NPC');
|
||||
|
||||
const combat = player.addComponent(new CombatComponent());
|
||||
combat.target = enemy;
|
||||
combat.ally = npc;
|
||||
|
||||
// enemy 销毁后,只有 target 变为 null,ally 仍然有效
|
||||
enemy.destroy();
|
||||
console.log(combat.target); // null
|
||||
console.log(combat.ally); // Entity { name: 'NPC' }
|
||||
```
|
||||
|
||||
## 安全检查
|
||||
|
||||
`@EntityRef` 提供了多重安全检查:
|
||||
|
||||
```typescript
|
||||
const scene1 = new Scene();
|
||||
const scene2 = new Scene();
|
||||
|
||||
const entity1 = scene1.createEntity('Entity1');
|
||||
const entity2 = scene2.createEntity('Entity2');
|
||||
|
||||
const comp = entity1.addComponent(new ParentComponent());
|
||||
|
||||
// 跨场景引用会失败
|
||||
comp.parent = entity2; // 输出错误日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
|
||||
// 引用已销毁的实体会失败
|
||||
const entity3 = scene1.createEntity('Entity3');
|
||||
entity3.destroy();
|
||||
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
|
||||
console.log(comp.parent); // null
|
||||
```
|
||||
|
||||
## 实现原理
|
||||
|
||||
`@EntityRef` 使用以下机制实现自动引用追踪:
|
||||
|
||||
1. **ReferenceTracker**: Scene 持有一个引用追踪器,记录所有实体引用关系
|
||||
2. **WeakRef**: 使用弱引用存储组件,避免循环引用导致内存泄漏
|
||||
3. **属性拦截**: 通过 `Object.defineProperty` 拦截 getter/setter
|
||||
4. **自动清理**: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
|
||||
|
||||
```typescript
|
||||
// 简化的实现原理
|
||||
class ReferenceTracker {
|
||||
// entityId -> 引用该实体的所有组件记录
|
||||
private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
|
||||
|
||||
// 实体销毁时调用
|
||||
clearReferencesTo(entityId: number): void {
|
||||
const records = this._references.get(entityId);
|
||||
if (records) {
|
||||
for (const record of records) {
|
||||
const component = record.component.deref();
|
||||
if (component) {
|
||||
// 将组件的引用属性设为 null
|
||||
(component as any)[record.propertyKey] = null;
|
||||
}
|
||||
}
|
||||
this._references.delete(entityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能考虑
|
||||
|
||||
`@EntityRef` 会带来一些性能开销:
|
||||
|
||||
- **写入开销**: 每次设置引用时需要更新 ReferenceTracker
|
||||
- **内存开销**: ReferenceTracker 需要维护引用映射表
|
||||
- **销毁开销**: 实体销毁时需要遍历所有引用并清理
|
||||
|
||||
对于大多数场景,这些开销是可以接受的。但如果有**大量实体和频繁的引用变更**,存储ID可能更高效。
|
||||
|
||||
## 调试支持
|
||||
|
||||
ReferenceTracker 提供了调试接口:
|
||||
|
||||
```typescript
|
||||
// 查看某个实体被哪些组件引用
|
||||
const references = scene.referenceTracker.getReferencesTo(entity.id);
|
||||
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
|
||||
|
||||
// 获取完整的调试信息
|
||||
const debugInfo = scene.referenceTracker.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## 与存储 ID 方式的对比
|
||||
|
||||
### 存储 ID(推荐大多数情况)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Follower')
|
||||
class Follower extends Component {
|
||||
targetId: number | null = null;
|
||||
}
|
||||
|
||||
// 在 System 中查找
|
||||
class FollowerSystem extends EntitySystem {
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const follower = entity.getComponent(Follower)!;
|
||||
const target = entity.scene?.findEntityById(follower.targetId);
|
||||
if (target) {
|
||||
// 跟随逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 EntityRef(适合复杂关联)
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Transform')
|
||||
class Transform extends Component {
|
||||
@EntityRef()
|
||||
parent: Entity | null = null;
|
||||
|
||||
position: { x: number, y: number } = { x: 0, y: 0 };
|
||||
|
||||
// 可以直接访问父实体的组件
|
||||
getWorldPosition(): { x: number, y: number } {
|
||||
if (!this.parent) {
|
||||
return { ...this.position };
|
||||
}
|
||||
|
||||
const parentTransform = this.parent.getComponent(Transform);
|
||||
if (parentTransform) {
|
||||
const parentPos = parentTransform.getWorldPosition();
|
||||
return {
|
||||
x: parentPos.x + this.position.x,
|
||||
y: parentPos.y + this.position.y
|
||||
};
|
||||
}
|
||||
|
||||
return { ...this.position };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
| 方式 | 适用场景 | 优点 | 缺点 |
|
||||
|------|----------|------|------|
|
||||
| 存储 ID | 大多数情况 | 简单、无额外开销 | 需要在 System 中查找 |
|
||||
| @EntityRef | 父子关系、复杂关联 | 自动清理、代码简洁 | 有性能开销 |
|
||||
|
||||
- **推荐做法**: 大部分情况使用存储ID + System查找的方式
|
||||
- **EntityRef 适用场景**: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
|
||||
- **核心优势**: 自动清理、防止悬空引用、代码更简洁
|
||||
- **注意事项**: 有性能开销,不适合大量动态引用的场景
|
||||
226
docs/src/content/docs/guide/component/index.md
Normal file
226
docs/src/content/docs/guide/component/index.md
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: "组件系统"
|
||||
description: "ECS 组件基础概念和创建方法"
|
||||
---
|
||||
|
||||
在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。
|
||||
|
||||
## 基本概念
|
||||
|
||||
组件是继承自 `Component` 抽象基类的具体类,用于:
|
||||
- 存储实体的数据(如位置、速度、健康值等)
|
||||
- 定义与数据相关的行为方法
|
||||
- 提供生命周期回调钩子
|
||||
- 支持序列化和调试
|
||||
|
||||
## 创建组件
|
||||
|
||||
### 基础组件定义
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Position')
|
||||
class Position extends Component {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
// 组件可以包含行为方法
|
||||
takeDamage(damage: number): void {
|
||||
this.current = Math.max(0, this.current - damage);
|
||||
}
|
||||
|
||||
heal(amount: number): void {
|
||||
this.current = Math.min(this.max, this.current + amount);
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## @ECSComponent 装饰器
|
||||
|
||||
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
|
||||
|
||||
### 为什么必须使用
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
|
||||
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
|
||||
| **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 |
|
||||
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
|
||||
|
||||
### 基本语法
|
||||
|
||||
```typescript
|
||||
@ECSComponent(typeName: string)
|
||||
```
|
||||
|
||||
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
|
||||
|
||||
### 使用示例
|
||||
|
||||
```typescript
|
||||
// ✅ 正确的用法
|
||||
@ECSComponent('Velocity')
|
||||
class Velocity extends Component {
|
||||
dx: number = 0;
|
||||
dy: number = 0;
|
||||
}
|
||||
|
||||
// ✅ 推荐:类型名与类名保持一致
|
||||
@ECSComponent('PlayerController')
|
||||
class PlayerController extends Component {
|
||||
speed: number = 5;
|
||||
}
|
||||
|
||||
// ❌ 错误的用法 - 没有装饰器
|
||||
class BadComponent extends Component {
|
||||
// 这样定义的组件可能在生产环境出现问题:
|
||||
// 1. 代码压缩后类名变化,无法正确序列化
|
||||
// 2. 组件未注册到框架,查询和匹配可能失效
|
||||
}
|
||||
```
|
||||
|
||||
### 与 @Serializable 配合使用
|
||||
|
||||
当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用:
|
||||
|
||||
```typescript
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('Player')
|
||||
@Serializable({ version: 1 })
|
||||
class PlayerComponent extends Component {
|
||||
@Serialize()
|
||||
name: string = '';
|
||||
|
||||
@Serialize()
|
||||
level: number = 1;
|
||||
|
||||
// 不使用 @Serialize() 的字段不会被序列化
|
||||
private _cachedData: any = null;
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。
|
||||
|
||||
### 组件类型名的唯一性
|
||||
|
||||
每个组件的类型名应该是唯一的:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:两个组件使用相同的类型名
|
||||
@ECSComponent('Health')
|
||||
class HealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('Health') // 冲突!
|
||||
class EnemyHealthComponent extends Component { }
|
||||
|
||||
// ✅ 正确:使用不同的类型名
|
||||
@ECSComponent('PlayerHealth')
|
||||
class PlayerHealthComponent extends Component { }
|
||||
|
||||
@ECSComponent('EnemyHealth')
|
||||
class EnemyHealthComponent extends Component { }
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
每个组件都有一些内置属性:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
someData: string = "example";
|
||||
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件ID: ${this.id}`); // 唯一的组件ID
|
||||
console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 组件与实体的关系
|
||||
|
||||
组件存储了所属实体的ID (`entityId`),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。
|
||||
|
||||
在实际使用中,**应该在 System 中处理实体和组件的交互**,而不是在组件内部:
|
||||
|
||||
```typescript
|
||||
@ECSComponent('Health')
|
||||
class Health extends Component {
|
||||
current: number;
|
||||
max: number;
|
||||
|
||||
constructor(max: number = 100) {
|
||||
super();
|
||||
this.max = max;
|
||||
this.current = max;
|
||||
}
|
||||
|
||||
isDead(): boolean {
|
||||
return this.current <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ECSComponent('Damage')
|
||||
class Damage extends Component {
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐:在 System 中处理逻辑
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(new Matcher().all(Health, Damage));
|
||||
}
|
||||
|
||||
process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health)!;
|
||||
const damage = entity.getComponent(Damage)!;
|
||||
|
||||
health.current -= damage.value;
|
||||
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
|
||||
// 应用伤害后移除 Damage 组件
|
||||
entity.removeComponent(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 更多主题
|
||||
|
||||
- [生命周期](/guide/component/lifecycle) - 组件生命周期钩子
|
||||
- [EntityRef 装饰器](/guide/component/entity-ref) - 安全的实体引用
|
||||
- [最佳实践](/guide/component/best-practices) - 组件设计模式和示例
|
||||
182
docs/src/content/docs/guide/component/lifecycle.md
Normal file
182
docs/src/content/docs/guide/component/lifecycle.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: "组件生命周期"
|
||||
description: "组件的生命周期钩子和事件"
|
||||
---
|
||||
|
||||
组件提供了生命周期钩子,可以重写来执行特定的逻辑。
|
||||
|
||||
## 生命周期方法
|
||||
|
||||
```typescript
|
||||
@ECSComponent('ExampleComponent')
|
||||
class ExampleComponent extends Component {
|
||||
private resource: SomeResource | null = null;
|
||||
|
||||
/**
|
||||
* 组件被添加到实体时调用
|
||||
* 用于初始化资源、建立引用等
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
|
||||
this.resource = new SomeResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* 用于清理资源、断开引用等
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log(`组件 ${this.constructor.name} 已移除`);
|
||||
if (this.resource) {
|
||||
this.resource.cleanup();
|
||||
this.resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 生命周期顺序
|
||||
|
||||
```
|
||||
实体创建
|
||||
↓
|
||||
addComponent() 调用
|
||||
↓
|
||||
onAddedToEntity() 触发
|
||||
↓
|
||||
组件正常使用中...
|
||||
↓
|
||||
removeComponent() 或 entity.destroy() 调用
|
||||
↓
|
||||
onRemovedFromEntity() 触发
|
||||
↓
|
||||
组件被移除/销毁
|
||||
```
|
||||
|
||||
## 实际用例
|
||||
|
||||
### 资源管理
|
||||
|
||||
```typescript
|
||||
@ECSComponent('TextureComponent')
|
||||
class TextureComponent extends Component {
|
||||
private _texture: Texture | null = null;
|
||||
texturePath: string = '';
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// 加载纹理资源
|
||||
this._texture = TextureManager.load(this.texturePath);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// 释放纹理资源
|
||||
if (this._texture) {
|
||||
TextureManager.release(this._texture);
|
||||
this._texture = null;
|
||||
}
|
||||
}
|
||||
|
||||
get texture(): Texture | null {
|
||||
return this._texture;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 事件监听
|
||||
|
||||
```typescript
|
||||
@ECSComponent('InputListener')
|
||||
class InputListener extends Component {
|
||||
private _boundHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
this._boundHandler = this.handleKeyDown.bind(this);
|
||||
window.addEventListener('keydown', this._boundHandler);
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
if (this._boundHandler) {
|
||||
window.removeEventListener('keydown', this._boundHandler);
|
||||
this._boundHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown(e: KeyboardEvent): void {
|
||||
// 处理键盘输入
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册到外部系统
|
||||
|
||||
```typescript
|
||||
@ECSComponent('PhysicsBody')
|
||||
class PhysicsBody extends Component {
|
||||
private _body: PhysicsWorld.Body | null = null;
|
||||
|
||||
onAddedToEntity(): void {
|
||||
// 在物理世界中创建刚体
|
||||
this._body = PhysicsWorld.createBody({
|
||||
entityId: this.entityId,
|
||||
type: 'dynamic'
|
||||
});
|
||||
}
|
||||
|
||||
onRemovedFromEntity(): void {
|
||||
// 从物理世界移除刚体
|
||||
if (this._body) {
|
||||
PhysicsWorld.removeBody(this._body);
|
||||
this._body = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 避免在生命周期中访问其他组件
|
||||
|
||||
```typescript
|
||||
@ECSComponent('BadComponent')
|
||||
class BadComponent extends Component {
|
||||
onAddedToEntity(): void {
|
||||
// ⚠️ 不推荐:此时其他组件可能还未添加
|
||||
const other = this.entity?.getComponent(OtherComponent);
|
||||
if (other) {
|
||||
// 可能为 null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 推荐:使用 System 处理组件间交互
|
||||
|
||||
```typescript
|
||||
@ECSSystem('InitializationSystem')
|
||||
class InitializationSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(ComponentA, ComponentB));
|
||||
}
|
||||
|
||||
// 使用 onAdded 事件,确保两个组件都存在
|
||||
onAdded(entity: Entity): void {
|
||||
const a = entity.getComponent(ComponentA)!;
|
||||
const b = entity.getComponent(ComponentB)!;
|
||||
// 安全地初始化交互
|
||||
a.linkTo(b);
|
||||
}
|
||||
|
||||
onRemoved(entity: Entity): void {
|
||||
// 清理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与 System 生命周期的对比
|
||||
|
||||
| 特性 | 组件生命周期 | System 生命周期 |
|
||||
|------|-------------|----------------|
|
||||
| 触发时机 | 组件添加/移除时 | 匹配条件满足时 |
|
||||
| 适用场景 | 资源初始化/清理 | 业务逻辑处理 |
|
||||
| 访问其他组件 | 不推荐 | 安全 |
|
||||
| 访问 Scene | 有限 | 完整 |
|
||||
225
docs/src/content/docs/guide/entity-query/best-practices.md
Normal file
225
docs/src/content/docs/guide/entity-query/best-practices.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "最佳实践"
|
||||
description: "实体查询优化和实际应用场景"
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 优先使用 EntitySystem
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐: 使用 EntitySystem
|
||||
class GoodSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HealthComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 自动获得符合条件的实体,每帧自动更新
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 不推荐: 在 update 中手动查询
|
||||
class BadSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty());
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 每帧手动查询,浪费性能
|
||||
const result = this.scene!.querySystem.queryAll(HealthComponent);
|
||||
for (const entity of result.entities) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 合理使用 none() 排除条件
|
||||
|
||||
```typescript
|
||||
// 排除已死亡的敌人
|
||||
class EnemyAISystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(EnemyTag, AIComponent)
|
||||
.none(DeadTag) // 不处理死亡的敌人
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用标签优化查询
|
||||
|
||||
```typescript
|
||||
// ❌ 不好: 查询所有实体再过滤
|
||||
const allEntities = scene.querySystem.getAllEntities();
|
||||
const players = allEntities.filter(e => e.hasComponent(PlayerTag));
|
||||
|
||||
// ✅ 好: 直接按标签查询
|
||||
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;
|
||||
```
|
||||
|
||||
### 4. 避免过于复杂的查询条件
|
||||
|
||||
```typescript
|
||||
// ❌ 不推荐: 过于复杂
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(A, B, C, D)
|
||||
.any(E, F, G)
|
||||
.none(H, I, J)
|
||||
);
|
||||
|
||||
// ✅ 推荐: 拆分成多个简单系统
|
||||
class SystemAB extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(A, B));
|
||||
}
|
||||
}
|
||||
|
||||
class SystemCD extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(C, D));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实际应用场景
|
||||
|
||||
### 场景1: 物理系统
|
||||
|
||||
```typescript
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const rigidbody = entity.getComponent(RigidbodyComponent)!;
|
||||
|
||||
// 应用重力
|
||||
rigidbody.velocity.y -= 9.8 * Time.deltaTime;
|
||||
|
||||
// 更新位置
|
||||
transform.position.x += rigidbody.velocity.x * Time.deltaTime;
|
||||
transform.position.y += rigidbody.velocity.y * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 渲染系统
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(TransformComponent, SpriteComponent)
|
||||
.none(InvisibleTag) // 排除不可见实体
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 按 z-order 排序
|
||||
const sorted = entities.slice().sort((a, b) => {
|
||||
const zA = a.getComponent(TransformComponent)!.z;
|
||||
const zB = b.getComponent(TransformComponent)!.z;
|
||||
return zA - zB;
|
||||
});
|
||||
|
||||
// 渲染实体
|
||||
for (const entity of sorted) {
|
||||
const transform = entity.getComponent(TransformComponent)!;
|
||||
const sprite = entity.getComponent(SpriteComponent)!;
|
||||
|
||||
renderer.drawSprite(sprite.texture, transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 碰撞检测
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(TransformComponent, ColliderComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 简单的 O(n²) 碰撞检测
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(a: Entity, b: Entity): void {
|
||||
const transA = a.getComponent(TransformComponent)!;
|
||||
const transB = b.getComponent(TransformComponent)!;
|
||||
const colliderA = a.getComponent(ColliderComponent)!;
|
||||
const colliderB = b.getComponent(ColliderComponent)!;
|
||||
|
||||
if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
|
||||
// 触发碰撞事件
|
||||
scene.eventSystem.emit('collision', { entityA: a, entityB: b });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4: 一次性查询
|
||||
|
||||
```typescript
|
||||
// 在系统外部执行一次性查询
|
||||
class GameManager {
|
||||
private scene: Scene;
|
||||
|
||||
public countEnemies(): number {
|
||||
const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
return result.count;
|
||||
}
|
||||
|
||||
public findNearestEnemy(playerPos: Vector2): Entity | null {
|
||||
const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);
|
||||
|
||||
let nearest: Entity | null = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const enemy of enemies.entities) {
|
||||
const transform = enemy.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
|
||||
const distance = Vector2.distance(playerPos, transform.position);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能统计
|
||||
|
||||
```typescript
|
||||
const stats = querySystem.getStats();
|
||||
console.log('总查询次数:', stats.queryStats.totalQueries);
|
||||
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
|
||||
console.log('缓存大小:', stats.cacheStats.size);
|
||||
```
|
||||
|
||||
## 相关 API
|
||||
|
||||
- [Matcher](/api/classes/Matcher/) - 查询条件描述符 API 参考
|
||||
- [QuerySystem](/api/classes/QuerySystem/) - 查询系统 API 参考
|
||||
- [EntitySystem](/api/classes/EntitySystem/) - 实体系统 API 参考
|
||||
- [Entity](/api/classes/Entity/) - 实体 API 参考
|
||||
133
docs/src/content/docs/guide/entity-query/compiled-query.md
Normal file
133
docs/src/content/docs/guide/entity-query/compiled-query.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "编译查询"
|
||||
description: "CompiledQuery 类型安全查询工具"
|
||||
---
|
||||
|
||||
> **v2.4.0+**
|
||||
|
||||
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
|
||||
|
||||
## 基本用法
|
||||
|
||||
```typescript
|
||||
// 创建编译查询
|
||||
const query = scene.querySystem.compile(Position, Velocity);
|
||||
|
||||
// 类型安全的遍历 - 组件参数自动推断类型
|
||||
query.forEach((entity, pos, vel) => {
|
||||
pos.x += vel.vx * deltaTime;
|
||||
pos.y += vel.vy * deltaTime;
|
||||
});
|
||||
|
||||
// 获取实体数量
|
||||
console.log(`匹配实体数: ${query.count}`);
|
||||
|
||||
// 获取第一个匹配的实体
|
||||
const first = query.first();
|
||||
if (first) {
|
||||
const [entity, pos, vel] = first;
|
||||
console.log(`第一个实体: ${entity.name}`);
|
||||
}
|
||||
```
|
||||
|
||||
## 变更检测
|
||||
|
||||
CompiledQuery 支持基于 epoch 的变更检测:
|
||||
|
||||
```typescript
|
||||
class RenderSystem extends EntitySystem {
|
||||
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
|
||||
private _lastEpoch = 0;
|
||||
|
||||
protected onInitialize(): void {
|
||||
this._query = this.scene!.querySystem.compile(Transform, Sprite);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理 Transform 或 Sprite 发生变化的实体
|
||||
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
|
||||
this.updateRenderData(entity, transform, sprite);
|
||||
});
|
||||
|
||||
// 保存当前 epoch 作为下次检查的起点
|
||||
this._lastEpoch = this.scene!.epochManager.current;
|
||||
}
|
||||
|
||||
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
|
||||
// 更新渲染数据
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 函数式 API
|
||||
|
||||
CompiledQuery 提供了丰富的函数式 API:
|
||||
|
||||
```typescript
|
||||
const query = scene.querySystem.compile(Position, Health);
|
||||
|
||||
// map - 转换实体数据
|
||||
const positions = query.map((entity, pos, health) => ({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
healthPercent: health.current / health.max
|
||||
}));
|
||||
|
||||
// filter - 过滤实体
|
||||
const lowHealthEntities = query.filter((entity, pos, health) => {
|
||||
return health.current < health.max * 0.2;
|
||||
});
|
||||
|
||||
// find - 查找第一个匹配的实体
|
||||
const target = query.find((entity, pos, health) => {
|
||||
return health.current > 0 && pos.x > 100;
|
||||
});
|
||||
|
||||
// toArray - 转换为数组
|
||||
const allData = query.toArray();
|
||||
for (const [entity, pos, health] of allData) {
|
||||
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
|
||||
}
|
||||
|
||||
// any/empty - 检查是否有匹配
|
||||
if (query.any()) {
|
||||
console.log('有匹配的实体');
|
||||
}
|
||||
if (query.empty()) {
|
||||
console.log('没有匹配的实体');
|
||||
}
|
||||
```
|
||||
|
||||
## CompiledQuery vs EntitySystem
|
||||
|
||||
| 特性 | CompiledQuery | EntitySystem |
|
||||
|------|---------------|--------------|
|
||||
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
|
||||
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
|
||||
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
|
||||
| **变更检测** | forEachChanged | forEachChanged |
|
||||
| **事件监听** | 无 | addEventListener |
|
||||
| **命令缓冲** | 无 | this.commands |
|
||||
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
|
||||
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
|
||||
|
||||
**选择建议**:
|
||||
|
||||
- 使用 **EntitySystem** 处理核心游戏逻辑(移动、战斗、AI 等)
|
||||
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
|
||||
|
||||
## API 参考
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
|
||||
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
|
||||
| `first()` | 获取第一个匹配的实体和组件 |
|
||||
| `toArray()` | 转换为 [entity, ...components] 数组 |
|
||||
| `map(callback)` | 映射转换 |
|
||||
| `filter(predicate)` | 过滤实体 |
|
||||
| `find(predicate)` | 查找第一个满足条件的实体 |
|
||||
| `any()` | 是否有任何匹配 |
|
||||
| `empty()` | 是否没有匹配 |
|
||||
| `count` | 匹配的实体数量 |
|
||||
| `entities` | 匹配的实体列表(只读) |
|
||||
244
docs/src/content/docs/guide/entity-query/index.md
Normal file
244
docs/src/content/docs/guide/entity-query/index.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
title: "实体查询系统"
|
||||
description: "ECS 实体查询核心概念和基础用法"
|
||||
---
|
||||
|
||||
实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Matcher - 查询条件描述符
|
||||
|
||||
Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。
|
||||
|
||||
### QuerySystem - 查询执行引擎
|
||||
|
||||
QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。
|
||||
|
||||
## 在 EntitySystem 中使用 Matcher
|
||||
|
||||
这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。
|
||||
|
||||
### 基础用法
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
}
|
||||
|
||||
class VelocityComponent extends Component {
|
||||
public vx: number = 0;
|
||||
public vy: number = 0;
|
||||
}
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 方式1: 使用 Matcher.empty().all()
|
||||
super(Matcher.empty().all(PositionComponent, VelocityComponent));
|
||||
|
||||
// 方式2: 直接使用 Matcher.all() (等价)
|
||||
// super(Matcher.all(PositionComponent, VelocityComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const pos = entity.getComponent(PositionComponent)!;
|
||||
const vel = entity.getComponent(VelocityComponent)!;
|
||||
|
||||
pos.x += vel.vx;
|
||||
pos.y += vel.vy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
### Matcher 链式 API
|
||||
|
||||
#### all() - 必须包含所有组件
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体必须同时拥有 Health 和 Position 组件
|
||||
super(Matcher.empty().all(HealthComponent, PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理同时拥有两个组件的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### any() - 至少包含一个组件
|
||||
|
||||
```typescript
|
||||
class DamageableSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体至少拥有 Health 或 Shield 其中之一
|
||||
super(Matcher.any(HealthComponent, ShieldComponent));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理拥有生命值或护盾的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### none() - 不能包含指定组件
|
||||
|
||||
```typescript
|
||||
class AliveEntitySystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 实体不能拥有 DeadTag 组件
|
||||
super(Matcher.all(HealthComponent).none(DeadTag));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 组合条件
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(
|
||||
Matcher.empty()
|
||||
.all(PositionComponent, HealthComponent) // 必须有位置和生命
|
||||
.any(WeaponComponent, MagicComponent) // 至少有武器或魔法
|
||||
.none(DeadTag, FrozenTag) // 不能是死亡或冰冻状态
|
||||
);
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 处理可以战斗的活着的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### nothing() - 不匹配任何实体
|
||||
|
||||
用于创建只需要生命周期方法(`onBegin`、`onEnd`)但不需要处理实体的系统。
|
||||
|
||||
```typescript
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 不匹配任何实体
|
||||
super(Matcher.nothing());
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// 每帧开始时执行
|
||||
Performance.markFrameStart();
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 永远不会被调用,因为没有匹配的实体
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// 每帧结束时执行
|
||||
Performance.markFrameEnd();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### empty() vs nothing() 的区别
|
||||
|
||||
| 方法 | 行为 | 使用场景 |
|
||||
|------|------|----------|
|
||||
| `Matcher.empty()` | 匹配**所有**实体 | 需要处理场景中所有实体 |
|
||||
| `Matcher.nothing()` | 不匹配**任何**实体 | 只需要生命周期回调,不处理实体 |
|
||||
|
||||
## 直接使用 QuerySystem
|
||||
|
||||
如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。
|
||||
|
||||
### 基础查询方法
|
||||
|
||||
```typescript
|
||||
// 获取场景的查询系统
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 查询拥有所有指定组件的实体
|
||||
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result1.count} 个移动实体`);
|
||||
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);
|
||||
|
||||
// 查询拥有任意指定组件的实体
|
||||
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
|
||||
console.log(`找到 ${result2.count} 个战斗单位`);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const result3 = querySystem.queryNone(DeadTag);
|
||||
console.log(`找到 ${result3.count} 个活着的实体`);
|
||||
```
|
||||
|
||||
### 按标签和名称查询
|
||||
|
||||
```typescript
|
||||
// 按标签查询
|
||||
const playerResult = querySystem.queryByTag(Tags.PLAYER);
|
||||
for (const player of playerResult.entities) {
|
||||
console.log('玩家:', player.name);
|
||||
}
|
||||
|
||||
// 按名称查询
|
||||
const bossResult = querySystem.queryByName('Boss');
|
||||
if (bossResult.count > 0) {
|
||||
const boss = bossResult.entities[0];
|
||||
console.log('找到Boss:', boss);
|
||||
}
|
||||
|
||||
// 按单个组件查询
|
||||
const healthResult = querySystem.queryByComponent(HealthComponent);
|
||||
console.log(`有 ${healthResult.count} 个实体拥有生命值`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 自动缓存
|
||||
|
||||
QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:
|
||||
|
||||
```typescript
|
||||
// 第一次查询,执行实际查询
|
||||
const result1 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result1.fromCache); // false
|
||||
|
||||
// 第二次相同查询,使用缓存
|
||||
const result2 = querySystem.queryAll(PositionComponent);
|
||||
console.log('fromCache:', result2.fromCache); // true
|
||||
```
|
||||
|
||||
### 实体变化自动更新
|
||||
|
||||
当实体添加/移除组件时,查询缓存会自动更新:
|
||||
|
||||
```typescript
|
||||
// 查询拥有武器的实体
|
||||
const before = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之前:', before.count); // 假设为 5
|
||||
|
||||
// 给实体添加武器
|
||||
const enemy = scene.createEntity('Enemy');
|
||||
enemy.addComponent(new WeaponComponent());
|
||||
|
||||
// 再次查询,自动包含新实体
|
||||
const after = querySystem.queryAll(WeaponComponent);
|
||||
console.log('之后:', after.count); // 现在是 6
|
||||
```
|
||||
|
||||
## 更多主题
|
||||
|
||||
- [Matcher API](/guide/entity-query/matcher-api) - 完整的 Matcher API 参考
|
||||
- [编译查询](/guide/entity-query/compiled-query) - CompiledQuery 高级用法
|
||||
- [最佳实践](/guide/entity-query/best-practices) - 查询优化和实际应用
|
||||
118
docs/src/content/docs/guide/entity-query/matcher-api.md
Normal file
118
docs/src/content/docs/guide/entity-query/matcher-api.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "Matcher API"
|
||||
description: "Matcher 完整 API 参考"
|
||||
---
|
||||
|
||||
## 静态创建方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `Matcher.all(...types)` | 必须包含所有指定组件 | `Matcher.all(Position, Velocity)` |
|
||||
| `Matcher.any(...types)` | 至少包含一个指定组件 | `Matcher.any(Health, Shield)` |
|
||||
| `Matcher.none(...types)` | 不能包含任何指定组件 | `Matcher.none(Dead)` |
|
||||
| `Matcher.byTag(tag)` | 按标签查询 | `Matcher.byTag(1)` |
|
||||
| `Matcher.byName(name)` | 按名称查询 | `Matcher.byName("Player")` |
|
||||
| `Matcher.byComponent(type)` | 按单个组件查询 | `Matcher.byComponent(Health)` |
|
||||
| `Matcher.empty()` | 创建空匹配器(匹配所有实体) | `Matcher.empty()` |
|
||||
| `Matcher.nothing()` | 不匹配任何实体 | `Matcher.nothing()` |
|
||||
| `Matcher.complex()` | 创建复杂查询构建器 | `Matcher.complex()` |
|
||||
|
||||
## 链式方法
|
||||
|
||||
| 方法 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `.all(...types)` | 添加必须包含的组件 | `.all(Position)` |
|
||||
| `.any(...types)` | 添加可选组件(至少一个) | `.any(Weapon, Magic)` |
|
||||
| `.none(...types)` | 添加排除的组件 | `.none(Dead)` |
|
||||
| `.exclude(...types)` | `.none()` 的别名 | `.exclude(Disabled)` |
|
||||
| `.one(...types)` | `.any()` 的别名 | `.one(Player, Enemy)` |
|
||||
| `.withTag(tag)` | 添加标签条件 | `.withTag(1)` |
|
||||
| `.withName(name)` | 添加名称条件 | `.withName("Boss")` |
|
||||
| `.withComponent(type)` | 添加单组件条件 | `.withComponent(Health)` |
|
||||
|
||||
## 实用方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `.getCondition()` | 获取查询条件(只读) |
|
||||
| `.isEmpty()` | 检查是否为空条件 |
|
||||
| `.isNothing()` | 检查是否为 nothing 匹配器 |
|
||||
| `.clone()` | 克隆匹配器 |
|
||||
| `.reset()` | 重置所有条件 |
|
||||
| `.toString()` | 获取字符串表示 |
|
||||
|
||||
## 常用组合示例
|
||||
|
||||
```typescript
|
||||
// 基础移动系统
|
||||
Matcher.all(Position, Velocity)
|
||||
|
||||
// 可攻击的活着的实体
|
||||
Matcher.all(Position, Health)
|
||||
.any(Weapon, Magic)
|
||||
.none(Dead, Disabled)
|
||||
|
||||
// 所有带标签的敌人
|
||||
Matcher.byTag(Tags.ENEMY)
|
||||
.all(AIComponent)
|
||||
|
||||
// 只需要生命周期的系统
|
||||
Matcher.nothing()
|
||||
```
|
||||
|
||||
## 按标签查询
|
||||
|
||||
```typescript
|
||||
class PlayerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定标签的实体
|
||||
super(Matcher.empty().withTag(Tags.PLAYER));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理玩家实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 按名称查询
|
||||
|
||||
```typescript
|
||||
class BossSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 查询特定名称的实体
|
||||
super(Matcher.empty().withName('Boss'));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 只处理名为 'Boss' 的实体
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### Matcher 是不可变的
|
||||
|
||||
```typescript
|
||||
const matcher = Matcher.empty().all(PositionComponent);
|
||||
|
||||
// 链式调用返回新的 Matcher 实例
|
||||
const matcher2 = matcher.any(VelocityComponent);
|
||||
|
||||
// matcher 本身不变
|
||||
console.log(matcher === matcher2); // false
|
||||
```
|
||||
|
||||
### 查询结果是只读的
|
||||
|
||||
```typescript
|
||||
const result = querySystem.queryAll(PositionComponent);
|
||||
|
||||
// 不要修改返回的数组
|
||||
result.entities.push(someEntity); // 错误!
|
||||
|
||||
// 如果需要修改,先复制
|
||||
const mutableArray = [...result.entities];
|
||||
mutableArray.push(someEntity); // 正确
|
||||
```
|
||||
@@ -1,4 +1,6 @@
|
||||
# 实体类
|
||||
---
|
||||
title: "实体类"
|
||||
---
|
||||
|
||||
在 ECS 架构中,实体(Entity)是游戏世界中的基本对象。实体本身不包含游戏逻辑或数据,它只是一个容器,用来组合不同的组件来实现各种功能。
|
||||
|
||||
@@ -12,7 +14,7 @@
|
||||
::: tip 关于父子层级关系
|
||||
实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
|
||||
|
||||
详见 [层级系统](./hierarchy.md) 文档。
|
||||
详见 [层级系统](./hierarchy/) 文档。
|
||||
:::
|
||||
|
||||
## 创建实体
|
||||
@@ -441,6 +443,6 @@ const emptyHandle = NULL_HANDLE;
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
|
||||
- 了解 [组件系统](./component.md) 为实体添加功能
|
||||
- 了解 [场景管理](./scene.md) 组织和管理实体
|
||||
- 了解 [层级系统](./hierarchy/) 建立实体间的父子关系
|
||||
- 了解 [组件系统](./component/) 为实体添加功能
|
||||
- 了解 [场景管理](./scene/) 组织和管理实体
|
||||
@@ -1,4 +1,6 @@
|
||||
# 事件系统
|
||||
---
|
||||
title: "事件系统"
|
||||
---
|
||||
|
||||
ECS 框架内置了强大的类型安全事件系统,支持同步/异步事件、优先级、批处理等高级功能。事件系统是实现组件间通信、系统间协作的核心机制。
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# 快速开始
|
||||
---
|
||||
title: 快速开始
|
||||
description: 5 分钟上手 ESEngine ECS 框架
|
||||
---
|
||||
|
||||
本指南将帮助你快速上手 ECS Framework,从安装到创建第一个 ECS 应用。
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# 层级系统
|
||||
---
|
||||
title: "层级系统"
|
||||
---
|
||||
|
||||
在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。
|
||||
|
||||
@@ -432,6 +434,6 @@ const found = hierarchySystem.findChild(parent, "Child");
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [实体类](./entity.md) 的其他功能
|
||||
- 了解 [场景管理](./scene.md) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component.md) 如何定义和使用组件
|
||||
- 了解 [实体类](./entity/) 的其他功能
|
||||
- 了解 [场景管理](./scene/) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component/) 如何定义和使用组件
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user