Compare commits

...

68 Commits

Author SHA1 Message Date
yhh
157d0b8067 fix(docs): 修复英文文档dead links问题 2025-12-03 22:59:08 +08:00
yhh
dd1ae97de7 feat(docs): 添加中英文国际化支持 2025-12-03 22:48:22 +08:00
YHH
63f006ab62 feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
2025-12-03 22:15:22 +08:00
YHH
caf7622aa0 Merge pull request #257 from esengine/feat/system-stable-sorting
feat(ecs): 添加系统稳定排序支持
2025-12-03 21:01:52 +08:00
yhh
d746cf3bb8 feat(ecs): 添加系统稳定排序支持 2025-12-03 20:54:34 +08:00
github-actions[bot]
88af781d78 chore(editor): bump version to 1.0.13 (#255)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-02 00:07:52 +08:00
yhh
15d5d37e50 fix(docs): 修复 hierarchy.md 中的死链接
将不存在的 ./transform.md 链接替换为 ./component.md
2025-12-01 23:36:20 +08:00
imgbot[bot]
b9aaf894d7 [ImgBot] Optimize images (#252)
*Total -- 1,159.60kb -> 862.13kb (25.65%)

/screenshots/main_screetshot.png -- 175.29kb -> 84.97kb (51.52%)
/screenshots/settings.png -- 44.17kb -> 25.59kb (42.06%)
/screenshots/plugin_manager.png -- 60.70kb -> 37.42kb (38.35%)
/screenshots/about.png -- 32.89kb -> 23.61kb (28.21%)
/screenshots/performance_profiler.png -- 564.36kb -> 420.67kb (25.46%)
/packages/editor-app/src-tauri/icons/128x128.png -- 4.76kb -> 4.40kb (7.55%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-512@2x.png -- 58.30kb -> 54.30kb (6.86%)
/packages/editor-app/src-tauri/icons/Square107x107Logo.png -- 3.96kb -> 3.73kb (5.75%)
/packages/editor-app/src-tauri/icons/Square142x142Logo.png -- 5.44kb -> 5.13kb (5.69%)
/packages/editor-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -- 3.16kb -> 2.98kb (5.69%)
/packages/editor-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -- 3.16kb -> 2.98kb (5.69%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -- 2.61kb -> 2.47kb (5.35%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-40x40@2x.png -- 2.61kb -> 2.47kb (5.35%)
/packages/editor-app/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -- 4.06kb -> 3.87kb (4.73%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -- 7.68kb -> 7.34kb (4.41%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -- 7.68kb -> 7.34kb (4.41%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-40x40@3x.png -- 4.58kb -> 4.38kb (4.33%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-60x60@2x.png -- 4.58kb -> 4.38kb (4.33%)
/packages/editor-app/src-tauri/icons/64x64.png -- 2.11kb -> 2.02kb (4.21%)
/packages/editor-app/src-tauri/icons/Square310x310Logo.png -- 13.92kb -> 13.36kb (4.04%)
/packages/editor-app/src-tauri/icons/Square89x89Logo.png -- 2.99kb -> 2.87kb (3.95%)
/packages/editor-app/src-tauri/icons/Square284x284Logo.png -- 12.53kb -> 12.04kb (3.89%)
/packages/editor-app/src-tauri/icons/Square150x150Logo.png -- 5.84kb -> 5.62kb (3.83%)
/packages/editor-app/src-tauri/icons/icon.png -- 25.58kb -> 24.61kb (3.81%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -- 6.73kb -> 6.47kb (3.75%)
/packages/editor-app/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -- 6.27kb -> 6.03kb (3.72%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -- 14.68kb -> 14.15kb (3.59%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-76x76@2x.png -- 6.06kb -> 5.85kb (3.51%)
/packages/editor-app/src-tauri/icons/128x128@2x.png -- 10.88kb -> 10.53kb (3.21%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -- 5.66kb -> 5.48kb (3.21%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -- 5.66kb -> 5.48kb (3.21%)
/packages/editor-app/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -- 9.02kb -> 8.78kb (2.74%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-60x60@3x.png -- 7.22kb -> 7.02kb (2.65%)
/packages/editor-app/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -- 20.92kb -> 20.40kb (2.49%)
/packages/editor-app/src-tauri/icons/Square71x71Logo.png -- 2.38kb -> 2.33kb (1.81%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-76x76@1x.png -- 2.52kb -> 2.47kb (1.79%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-20x20@3x.png -- 2.01kb -> 1.98kb (1.46%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-29x29@3x.png -- 2.84kb -> 2.81kb (1.1%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -- 1.91kb -> 1.89kb (0.82%)
/packages/editor-app/src-tauri/icons/ios/AppIcon-29x29@2x.png -- 1.91kb -> 1.89kb (0.82%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: YHH <359807859@qq.com>
2025-12-01 23:34:47 +08:00
YHH
460cdb5af4 Feature/docs improvement (#254)
* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能

* feat(docs): 重构文档站主题样式

* chore(ci): 升级所有 workflow 的 pnpm 版本从 v8 到 v10
2025-12-01 23:33:04 +08:00
github-actions[bot]
290bd9858e chore(core): release v2.2.18 (#253)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-01 22:34:05 +08:00
YHH
b42a7b4e43 Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级

* feat(core): 层级系统重构与UI变换矩阵修复

* refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题

* fix(physics): 修复跨包组件类引用问题

* feat: 统一运行时架构与浏览器运行支持

* feat(asset): 实现浏览器运行时资产加载系统

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题和CI类型检查错误

* fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误

* test: 补齐核心模块测试用例,修复CI构建配置

* fix: 修复测试用例中的类型错误和断言问题

* fix: 修复 turbo build:npm 任务的依赖顺序问题

* fix: 修复 CI 构建错误并优化构建性能
2025-12-01 22:28:51 +08:00
github-actions[bot]
189714c727 chore(editor): bump version to 1.0.12 (#250)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-30 01:07:19 +08:00
YHH
987051acd4 Feature/advanced profiler (#249)
* feat(profiler): 实现高级性能分析器

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
2025-11-30 00:53:01 +08:00
YHH
374e08a79e feat(profiler): 实现高级性能分析器 (#248)
* feat(profiler): 实现高级性能分析器

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖

* test(core): 添加 ProfilerSDK 和 AdvancedProfilerCollector 测试覆盖
2025-11-30 00:22:47 +08:00
YHH
359886c72f Feature/physics and tilemap enhancement (#247)
* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统

* feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统

* feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误
2025-11-29 23:00:48 +08:00
yhh
f03b73b58e docs: 完善装饰器和 Matcher API 文档 2025-11-28 11:03:34 +08:00
github-actions[bot]
18d20df4da chore(core): release v2.2.17 (#246)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-28 11:03:11 +08:00
github-actions[bot]
c5642a8605 chore(editor): bump version to 1.0.11 (#245)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-28 11:01:04 +08:00
YHH
673f5e5855 feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 (#244)
* feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题

* fix: 修复 CI 流程并清理代码
2025-11-28 10:32:28 +08:00
YHH
cabb625a17 Feature/UI input system fix (#243)
* feat(ui): 实现编辑器预览模式下的 UI 输入系统

* feat(platform-web): 为浏览器运行时添加 UI 输入系统绑定
2025-11-27 22:31:05 +08:00
github-actions[bot]
b8f05b79b0 chore(core): release v2.2.16 (#242)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-27 21:13:09 +08:00
github-actions[bot]
b22faaac86 chore(editor): bump version to 1.0.10 (#241)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-27 21:05:40 +08:00
YHH
107439d70c Feature/runtime cdn and plugin loader (#240)
* feat(ui): 完善 UI 布局系统和编辑器可视化工具

* refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统

* fix: 修复 CodeQL 警告并提升测试覆盖率

* refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题

* fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤

* docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明

* fix(ci): 修复 type-check 失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖

* fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖

* fix(ci): platform-web 添加缺失的 behavior-tree 依赖

* fix(lint): 移除正则表达式中不必要的转义字符
2025-11-27 20:42:46 +08:00
github-actions[bot]
71869b1a58 chore(editor): bump version to 1.0.9 (#239)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-26 11:47:09 +08:00
yhh
9aed3134cf fix(ci): 修复 Windows 上 mkdir 命令报错 2025-11-26 11:28:14 +08:00
yhh
3ff57aff37 fix(ci): 修复 release-editor workflow 构建顺序 2025-11-26 11:22:43 +08:00
yhh
152c0541b8 fix(ci): 修复 release-editor workflow 构建顺序 2025-11-26 11:15:38 +08:00
YHH
7b14fa2da4 feat(editor): 添加 ECS UI 系统和编辑器更新优化 (#238) 2025-11-26 11:08:10 +08:00
YHH
3fb6f919f8 Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持

* feat(editor-core): 添加声明式插件注册 API

* feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor

* feat: 添加 editor-runtime SDK 和插件系统改进

* fix(ci): 修复SceneResourceManager里变量未使用问题
2025-11-25 22:23:19 +08:00
github-actions[bot]
551ca7805d chore(core): release v2.2.15 (#236)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-23 22:52:05 +08:00
yhh
8ab25fe293 fix(ci): 使用纯 bash+node 实现版本更新绕过 workspace 协议问题 2025-11-23 22:49:54 +08:00
yhh
eea7ed9e58 fix(ci): 将 npm version 改为 pnpm version 修复 workspace 协议问题 2025-11-23 22:46:21 +08:00
yhh
0279cf6d27 fix(ci): 使用 pnpm publish 修复 workspace:* 协议不支持的问题 2025-11-23 22:42:23 +08:00
yhh
0dff1ad2ad fix(ci): 修复 npm 发布时 workspace:* 协议不支持的问题 2025-11-23 22:36:54 +08:00
yhh
95fbcca66f chore: 移除渲染系统调试日志 2025-11-23 22:26:46 +08:00
github-actions[bot]
a61baa83a7 chore(editor): bump version to 1.0.8 (#235)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-23 22:21:20 +08:00
yhh
afebeecd68 fix(ci): 修复 Tauri 构建缺少 runtime 文件的问题 2025-11-23 22:08:38 +08:00
yhh
f4e9925319 fix(ci): 修复 Tauri 构建缺少 runtime 文件的问题 2025-11-23 21:57:10 +08:00
YHH
32460ac133 feat(editor): 优化编辑器UI和改进核心功能 (#234)
* feat(editor): 优化编辑器UI和改进核心功能

* feat(editor): 优化编辑器UI和改进核心功能
2025-11-23 21:45:10 +08:00
github-actions[bot]
4d95a7f044 chore(editor): bump version to 1.0.7 (#233)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-23 16:30:14 +08:00
yhh
57f919fbe0 fix(ci): 移除release-editor工作流中有问题的TypeScript缓存步骤 2025-11-23 16:12:42 +08:00
yhh
1cb9a0e58f fix(ci): 修复release-editor工作流hashFiles语法错误 2025-11-23 15:47:23 +08:00
yhh
1da43ee822 fix: 修复 release-editor workflow 构建问题 2025-11-23 15:26:17 +08:00
yhh
f4c7563763 chore: 移除 network 相关包并修复 CI 问题 2025-11-23 15:13:51 +08:00
YHH
a3f7cc38b1 Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统

* feat(engine): 完善2D渲染管线和编辑器视口功能

* feat(editor): 实现Viewport变换工具系统

* feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示

* feat(editor): 实现Run on Device移动预览功能

* feat(editor): 添加组件属性控制和依赖关系系统

* feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器

* feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(ci): 迁移项目到pnpm并修复CI构建问题

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 移除 network 相关包

* chore: 移除 network 相关包
2025-11-23 14:49:37 +08:00
github-actions[bot]
b15cbab313 chore(editor): bump version to 1.0.6 (#231)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-21 12:42:30 +08:00
yhh
504b9ffb66 fix(ci): 添加编辑器工作流缺失的引擎构建步骤 2025-11-21 11:53:33 +08:00
github-actions[bot]
6226e3ff06 chore(core): release v2.2.14 (#230)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-21 11:40:22 +08:00
YHH
2621d7f659 refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty (#229)
* refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty

* refactor(core): 移除@Inject参数装饰器,统一使用@InjectProperty
2025-11-21 11:37:55 +08:00
YHH
a768b890fd feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架

* feat: 增强编辑器UI功能与跨平台支持

* fix: 修复CI测试和类型检查问题

* fix: 修复CI问题并提高测试覆盖率

* fix: 修复CI问题并提高测试覆盖率
2025-11-21 10:03:18 +08:00
yhh
8b9616837d style(editor-app): 移除log信息 2025-11-20 09:51:29 +08:00
yhh
0d2948e60c feat(tools): rust工具初始化工具 2025-11-19 16:27:11 +08:00
YHH
ecfef727c8 feat: 实现可扩展的字段编辑器系统与专业资产选择器 (#227) 2025-11-19 14:54:03 +08:00
YHH
caed5428d5 refactor(editor-app): 改进架构和类型安全 (#226)
* refactor(editor-app): 改进架构和类型安全

* refactor(editor-app): 开始拆分 Inspector.tsx - 创建基础架构

* refactor(editor-app): 完成 Inspector.tsx 拆分

* refactor(editor-app): 优化 Inspector 类型定义,消除所有 any 使用

* refactor(editor): 实现可扩展的属性渲染器系统

* Potential fix for code scanning alert no. 231: Unused variable, import, function or class

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix(ci): 防止 Codecov 服务故障阻塞 CI 流程

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-18 22:28:13 +08:00
YHH
bce3a6e253 refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构

* feat(editor): 添加插件市场功能

* feat(editor): 重构插件市场以支持版本管理和ZIP打包

* feat(editor): 重构插件发布流程并修复React渲染警告

* fix(plugin): 修复插件发布和市场的路径不一致问题

* feat: 重构插件发布流程并添加插件删除功能

* fix(editor): 完善插件删除功能并修复多个关键问题

* fix(auth): 修复自动登录与手动登录的竞态条件问题

* feat(editor): 重构插件管理流程

* feat(editor): 支持 ZIP 文件直接发布插件

- 新增 PluginSourceParser 解析插件源
- 重构发布流程支持文件夹和 ZIP 两种方式
- 优化发布向导 UI

* feat(editor): 插件市场支持多版本安装

- 插件解压到项目 plugins 目录
- 新增 Tauri 后端安装/卸载命令
- 支持选择任意版本安装
- 修复打包逻辑,保留完整 dist 目录结构

* feat(editor): 个人中心支持多版本管理

- 合并同一插件的不同版本
- 添加版本历史展开/折叠功能
- 禁止有待审核 PR 时更新插件

* fix(editor): 修复 InspectorRegistry 服务注册

- InspectorRegistry 实现 IService 接口
- 注册到 Core.services 供插件使用

* feat(behavior-tree-editor): 完善插件注册和文件操作

- 添加文件创建模板和操作处理器
- 实现右键菜单创建行为树功能
- 修复文件读取权限问题(使用 Tauri 命令)
- 添加 BehaviorTreeEditorPanel 组件
- 修复 rollup 配置支持动态导入

* feat(plugin): 完善插件构建和发布流程

* fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成

* fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能

* fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式

* refactor(behavior-tree-editor): 移除调试面板功能简化代码结构

* refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑

* feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性

* fix(lint): 修复ESLint错误确保CI通过

* refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能

* refactor(behavior-tree-editor): 清理技术债务,优化代码质量

* fix(editor-app): 修复字符串替换安全问题
2025-11-18 14:46:51 +08:00
YHH
eac660b1a0 refactor(core): 统一参数命名 - worldId/sceneId 改为 worldName/sceneName (#225)
* refactor(core): 统一参数命名 - worldId/sceneId 改为 worldName/sceneName

* test(core): 更新测试用例以匹配新的错误消息

* refactor(core): 提高代码覆盖率 - 添加参数验证和测试
2025-11-15 00:20:17 +08:00
github-actions[bot]
af49870084 chore(core): release v2.2.13 (#224)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-14 12:13:47 +08:00
YHH
e2b316b3cc Fix/entity system dispose ondestroy (#223)
* fix(core): 修复 EntitySystem dispose 未调用 onDestroy 导致资源泄漏

* fix(core): 修复 Scene.end() 中 unload 调用时机导致用户无法清理资源
2025-11-14 12:10:59 +08:00
YHH
3a0544629d feat(core): 为 World 添加独立的服务容器 (#222)
* feat(core): 为 World 添加独立的服务容器

* test(core): 为 World 服务容器添加完整测试覆盖
2025-11-14 09:55:31 +08:00
LINGYE
609baace73 fix(logger): 移除自定义 factory 的缓存, 由使用方管理 (#221)
* fix(logger): 移除自定义 factory 的缓存, 由使用方管理

* test
2025-11-13 16:53:07 +08:00
LINGYE
b12cfba353 refactor(core): 移除 _activeWorlds 并优化 WorldManager 清理机制 (#220)
* refactor(core): 将 WorldManager 清理机制从定时器改为帧驱动

* refactor(core): 移除 WorldManager _activeWorlds 优化,简化状态管理

* test(core): 补充 WorldManager 测试用例

* docs(core): 更新 WorldManager cleanupFrameInterval 配置说明
2025-11-09 17:35:07 +08:00
LINGYE
6242c6daf3 fix(core): 修复 PerformanceMonitor 未遵循 Core debug 参数的问题 (#219)
- Core 传递 debug 配置到 WorldManager
- WorldManager 传递 debug 配置到 World
- World 在 debug=true 时为 Scene 注册并启用 PerformanceMonitor
- new Scene 的情况默认未开启,但暴露了 `performanceMonitor` 由使用者处理
2025-11-09 11:32:04 +08:00
github-actions[bot]
b5337de278 chore(core): release v2.2.12 (#218)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-11-07 12:12:44 +08:00
YHH
3512199ff4 fix(core): 移除fflate依赖,修复TextEncoder兼容性问题 (#217)
* fix(core): 移除fflate依赖,修复TextEncoder兼容性问题

* fix(core): 移除fflate依赖,修复TextEncoder兼容性问题
2025-11-07 12:10:52 +08:00
YHH
e03b106652 refactor(editor): 优化布局管理和行为树文件处理 2025-11-04 23:53:26 +08:00
YHH
f9afa22406 refactor(editor): 重构编辑器架构并增强行为树执行可视化 2025-11-04 18:29:28 +08:00
YHH
adfc7e91b3 Refactor/clean architecture phase1 (#215)
* refactor(editor): 建立Clean Architecture领域模型层

* refactor(editor): 实现应用层架构 - 命令模式、用例和状态管理

* refactor(editor): 实现展示层核心Hooks

* refactor(editor): 实现基础设施层和展示层组件

* refactor(editor): 迁移画布和连接渲染到 Clean Architecture 组件

* feat(editor): 集成应用层架构和命令模式,实现撤销/重做功能

* refactor(editor): UI组件拆分

* refactor(editor): 提取快速创建菜单逻辑

* refactor(editor): 重构BehaviorTreeEditor,提取组件和Hook

* refactor(editor): 提取端口连接和键盘事件Hook

* refactor(editor): 提取拖放处理Hook

* refactor(editor): 提取画布交互Hook和工具函数

* refactor(editor): 完成核心重构

* fix(editor): 修复节点无法创建和连接

* refactor(behavior-tree,editor): 重构节点子节点约束系统,实现元数据驱动的架构
2025-11-03 21:22:16 +08:00
YHH
40cde9c050 fix(editor): 修复行为树删除连接时children数组未同步清理的bug (#214) 2025-11-03 09:57:18 +08:00
1481 changed files with 247009 additions and 65552 deletions

8
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
name: "CodeQL Config"
# Paths to exclude from analysis
paths-ignore:
- thirdparty
- "**/node_modules"
- "**/dist"
- "**/bin"

View File

@@ -6,8 +6,9 @@ on:
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
pull_request:
@@ -15,76 +16,112 @@ on:
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
jobs:
test:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
# 缓存 Rust 编译结果
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/engine
cache-on-failure: true
# 缓存 wasm-pack
- name: Cache wasm-pack
uses: actions/cache@v4
with:
path: ~/.cargo/bin/wasm-pack
key: wasm-pack-${{ runner.os }}
- name: Install wasm-pack
run: |
if ! command -v wasm-pack &> /dev/null; then
cargo install wasm-pack
fi
- name: Install dependencies
run: npm ci
run: pnpm install --no-frozen-lockfile
# 缓存 Turbo
- name: Cache Turbo
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
turbo-${{ runner.os }}-
# 构建所有包
- name: Build all packages
run: pnpm run build
- name: Copy WASM files to ecs-engine-bindgen
run: |
mkdir -p packages/ecs-engine-bindgen/src/wasm
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
# 类型检查
- name: Type check
run: npm run type-check
run: pnpm run type-check
# Lint 检查
- name: Lint check
run: npm run lint
- name: Build core package first
run: npm run build:core
run: pnpm run lint
# 测试
- name: Run tests with coverage
run: npm run test:ci
run: pnpm run test:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
continue-on-error: true
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
build:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Build npm package
run: npm run build:npm
# 构建 npm 包
- name: Build npm packages
run: pnpm run build:npm
# 上传构建产物
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
bin/
dist/
retention-days: 7
packages/*/dist/
packages/*/bin/
retention-days: 7

View File

@@ -14,28 +14,34 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install
- name: Run tests with coverage
run: |
cd packages/core
npm run test:coverage
pnpm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/core/coverage/coverage-final.json
flags: core
name: core-coverage
fail_ci_if_error: true
fail_ci_if_error: false
verbose: true
- name: Upload coverage artifact

View File

@@ -31,6 +31,7 @@ jobs:
with:
languages: ${{ matrix.language }}
queries: security-and-quality
config-file: ./.github/codeql/codeql-config.yml
- name: Autobuild
uses: github/codeql-action/autobuild@v3

View File

@@ -17,15 +17,20 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Install commitlint
run: |
npm install --save-dev @commitlint/config-conventional @commitlint/cli
pnpm add -D @commitlint/config-conventional @commitlint/cli
- name: Validate PR commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

View File

@@ -29,26 +29,31 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: npm ci
run: pnpm install
- name: Build core package
run: npm run build:core
run: pnpm run build:core
- name: Generate API documentation
run: npm run docs:api
run: pnpm run docs:api
- name: Build documentation
run: npm run docs:build
run: pnpm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

View File

@@ -33,11 +33,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -57,39 +62,36 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install frontend dependencies
run: npm ci
run: pnpm install
- name: Update version in config files (for manual trigger)
if: github.event_name == 'workflow_dispatch'
run: |
cd packages/editor-app
# 临时更新版本号用于构建(不提交到仓库)
npm version ${{ github.event.inputs.version }} --no-git-tag-version
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
node scripts/sync-version.js
- name: Cache TypeScript build
uses: actions/cache@v4
with:
path: |
packages/core/bin
packages/editor-core/dist
packages/behavior-tree/bin
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**', 'packages/behavior-tree/src/**') }}
restore-keys: |
${{ runner.os }}-ts-build-
- name: Install wasm-pack
run: cargo install wasm-pack
- name: Build core package
run: npm run build:core
# 使用 Turborepo 自动按依赖顺序构建所有包
# 这会自动处理core -> asset-system -> editor-core -> ui -> 等等
- name: Build all packages with Turborepo
run: pnpm run build
- name: Build editor-core package
- name: Copy WASM files to ecs-engine-bindgen
shell: bash
run: |
cd packages/editor-core
npm run build
mkdir -p packages/ecs-engine-bindgen/src/wasm
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
- name: Build behavior-tree package
- name: Bundle runtime files for Tauri
run: |
cd packages/behavior-tree
npm run build
cd packages/editor-app
node scripts/bundle-runtime.mjs
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0.5
@@ -126,7 +128,7 @@ jobs:
- name: Update version files
run: |
cd packages/editor-app
npm version ${{ github.event.inputs.version }} --no-git-tag-version
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
node scripts/sync-version.js
- name: Create Pull Request
@@ -138,16 +140,16 @@ jobs:
delete-branch: true
title: "chore(editor): Release v${{ github.event.inputs.version }}"
body: |
## 🚀 Release v${{ github.event.inputs.version }}
## Release v${{ github.event.inputs.version }}
This PR updates the editor version after successful release build.
### Changes
- Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
- Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
- Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}`
- Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}`
### Release
- 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }})
---
*This PR was automatically created by the release workflow.*

View File

@@ -11,6 +11,10 @@ on:
- core
- behavior-tree
- editor-core
- node-editor
- blueprint
- tilemap
- physics-rapier2d
version_type:
description: '版本更新类型'
required: true
@@ -41,21 +45,32 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install
- name: Build core package (if needed)
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
if: ${{ github.event.inputs.package != 'core' && github.event.inputs.package != 'node-editor' }}
run: |
cd packages/core
npm run build
pnpm run build
- name: Build node-editor package (if needed for blueprint)
if: ${{ github.event.inputs.package == 'blueprint' }}
run: |
cd packages/node-editor
pnpm run build
# - name: Run tests
# run: |
@@ -67,25 +82,33 @@ jobs:
run: |
cd packages/${{ github.event.inputs.package }}
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version --allow-same-version
NEW_VERSION=${{ github.event.inputs.custom_version }}
else
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
# Get current version and bump it
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ github.event.inputs.version_type }}" in
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
esac
fi
NEW_VERSION=$(node -p "require('./package.json').version")
# Update package.json using node
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "发布版本: $NEW_VERSION"
- name: Build package
run: |
cd packages/${{ github.event.inputs.package }}
npm run build:npm
pnpm run build:npm
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cd packages/${{ github.event.inputs.package }}/dist
npm publish --access public
pnpm publish --access public --no-git-checks
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6

View File

@@ -22,19 +22,24 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install
- name: Build core package
run: |
cd packages/core
npm run build:npm
pnpm run build:npm
- name: Check bundle size
uses: andresz1/size-limit-action@v1

8
.gitignore vendored
View File

@@ -16,6 +16,10 @@ dist/
*.tmp
*.temp
.cache/
.build-cache/
# Turborepo
.turbo/
# IDE 配置
.idea/
@@ -48,9 +52,9 @@ logs/
coverage/
*.lcov
# 包管理器锁文件保留npm的,忽略其他的
# 包管理器锁文件(忽略yarn保留pnpm
yarn.lock
pnpm-lock.yaml
package-lock.json
# 文档生成
docs/api/

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
link-workspace-packages=true
prefer-workspace-packages=true

View File

@@ -37,9 +37,6 @@ This project follows the [Conventional Commits](https://www.conventionalcommits.
- **core**: 核心包 @esengine/ecs-framework
- **math**: 数学库包
- **network-client**: 网络客户端包
- **network-server**: 网络服务端包
- **network-shared**: 网络共享包
- **editor**: 编辑器
- **docs**: 文档

395
README.md
View File

@@ -1,90 +1,61 @@
# ECS Framework
# ESEngine
[![CI](https://github.com/esengine/ecs-framework/workflows/CI/badge.svg)](https://github.com/esengine/ecs-framework/actions)
[![codecov](https://codecov.io/gh/esengine/ecs-framework/graph/badge.svg)](https://codecov.io/gh/esengine/ecs-framework)
[![npm version](https://badge.fury.io/js/%40esengine%2Fecs-framework.svg)](https://badge.fury.io/js/%40esengine%2Fecs-framework)
[![npm downloads](https://img.shields.io/npm/dm/@esengine/ecs-framework.svg)](https://www.npmjs.com/package/@esengine/ecs-framework)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@esengine/ecs-framework)](https://bundlephobia.com/package/@esengine/ecs-framework)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors)
[![GitHub stars](https://img.shields.io/github/stars/esengine/ecs-framework?style=social)](https://github.com/esengine/ecs-framework/stargazers)
[![DeepWiki](https://img.shields.io/badge/_AI_文档-DeepWiki-6366f1?style=flat&logo=gitbook&logoColor=white)](https://deepwiki.com/esengine/ecs-framework)
**English** | [中文](./README_CN.md)
<div align="center">
**[Documentation](https://esengine.github.io/ecs-framework/) | [API Reference](https://esengine.github.io/ecs-framework/api/) | [Examples](./examples/)**
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
ESEngine is a cross-platform 2D game engine for creating games from a unified interface. It provides a comprehensive set of common tools so that developers can focus on making games without having to reinvent the wheel.
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
Games can be exported to multiple platforms including Web browsers, WeChat Mini Games, and other mini-game platforms.
</div>
## Free and Open Source
---
ESEngine is completely free and open source under the MIT license. No strings attached, no royalties. Your games are yours.
## 📊 项目统计 / Project Stats
## Features
<div align="center">
- **Data-Driven Architecture**: Built on Entity-Component-System (ECS) pattern for flexible and performant game logic
- **High-Performance Rendering**: Rust/WebAssembly 2D renderer with sprite batching and WebGL 2.0 backend
- **Visual Editor**: Cross-platform desktop editor with scene management, asset browser, and visual tools
- **Modular Design**: Use only what you need. Each feature is a separate module that can be included independently
- **Multi-Platform**: Deploy to Web, WeChat Mini Games, and more from a single codebase
[![Star History Chart](https://api.star-history.com/svg?repos=esengine/ecs-framework&type=Date)](https://star-history.com/#esengine/ecs-framework&Date)
## Getting the Engine
</div>
<div align="center">
<a href="https://github.com/esengine/ecs-framework/graphs/contributors">
<img src="https://contrib.rocks/image?repo=esengine/ecs-framework" />
</a>
</div>
### 📈 下载趋势 / Download Trends
<div align="center">
[![NPM Downloads](https://img.shields.io/npm/dt/@esengine/ecs-framework?label=Total%20Downloads&style=for-the-badge&color=blue)](https://www.npmjs.com/package/@esengine/ecs-framework)
[![NPM Trends](https://img.shields.io/npm/dm/@esengine/ecs-framework?label=Monthly%20Downloads&style=for-the-badge&color=success)](https://npmtrends.com/@esengine/ecs-framework)
</div>
---
## 特性
- **高性能** - 针对大规模实体优化支持SoA存储和批量处理
- **多线程计算** - Worker系统支持真正的并行处理充分利用多核CPU性能
- **类型安全** - 完整的TypeScript支持编译时类型检查
- **现代架构** - 支持多World、多Scene的分层架构设计
- **开发友好** - 内置调试工具和性能监控
- **跨平台** - 支持Cocos Creator、Laya引擎和Web平台
## 安装
### Using npm
```bash
npm install @esengine/ecs-framework
```
## 快速开始
### Building from Source
See [Building from Source](#building-from-source) for detailed instructions.
### Editor Download
Pre-built editor binaries are available on the [Releases](https://github.com/esengine/ecs-framework/releases) page for Windows and macOS.
## Quick Start
```typescript
import { Core, Scene, Component, EntitySystem, ECSComponent, ECSSystem, Matcher, Time } from '@esengine/ecs-framework';
import {
Core, Scene, Entity, Component, EntitySystem,
Matcher, Time, ECSComponent, ECSSystem
} from '@esengine/ecs-framework';
// 定义组件
@ECSComponent('Position')
class Position extends Component {
constructor(public x = 0, public y = 0) {
super();
}
x = 0;
y = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
constructor(public dx = 0, public dy = 0) {
super();
}
dx = 0;
dy = 0;
}
// 创建系统
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
@@ -93,182 +64,182 @@ class MovementSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
position.x += velocity.dx * Time.deltaTime;
position.y += velocity.dy * Time.deltaTime;
const pos = entity.getComponent(Position);
const vel = entity.getComponent(Velocity);
pos.x += vel.dx * Time.deltaTime;
pos.y += vel.dy * Time.deltaTime;
}
}
}
// 创建场景并启动
class GameScene extends Scene {
protected initialize(): void {
this.addSystem(new MovementSystem());
const player = this.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 0));
}
}
// 启动游戏
Core.create();
Core.setScene(new GameScene());
const scene = new Scene();
scene.addSystem(new MovementSystem());
const player = scene.createEntity('Player');
player.addComponent(new Position());
player.addComponent(new Velocity());
Core.setScene(scene);
// Game loop
let lastTime = 0;
function gameLoop(currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// 游戏循环中更新
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## 核心特性
## Modules
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
- **事件系统** - 类型安全的事件发布/订阅机制
- **性能优化** - SoA 存储优化,支持大规模实体处理
- **多线程支持** - Worker系统实现真正的并行计算充分利用多核CPU
- **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统
ESEngine is organized into modular packages. Each feature has a runtime module and an optional editor extension.
## 🏗️ 架构设计 / Architecture
### Core
```mermaid
graph TB
A[Core 核心] --> B[World 世界]
B --> C[Scene 场景]
C --> D[EntityManager 实体管理器]
C --> E[SystemManager 系统管理器]
D --> F[Entity 实体]
F --> G[Component 组件]
E --> H[EntitySystem 实体系统]
E --> I[WorkerSystem 工作线程系统]
| Package | Description |
|---------|-------------|
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
| `@esengine/math` | Vector, matrix, and mathematical utilities |
| `@esengine/engine` | Rust/WASM 2D renderer |
| `@esengine/engine-core` | Engine module system and lifecycle management |
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#e8f5e9
style E fill:#fff9c4
style F fill:#ffebee
style G fill:#e0f2f1
style H fill:#fce4ec
style I fill:#f1f8e9
### Runtime Modules
| Package | Description |
|---------|-------------|
| `@esengine/sprite` | 2D sprite rendering and animation |
| `@esengine/tilemap` | Tile-based map rendering with animation support |
| `@esengine/physics-rapier2d` | 2D physics simulation powered by Rapier |
| `@esengine/behavior-tree` | Behavior tree AI system |
| `@esengine/blueprint` | Visual scripting runtime |
| `@esengine/camera` | Camera control and management |
| `@esengine/audio` | Audio playback |
| `@esengine/ui` | UI components |
| `@esengine/material-system` | Material and shader system |
| `@esengine/asset-system` | Asset loading and management |
### Editor Extensions
| Package | Description |
|---------|-------------|
| `@esengine/sprite-editor` | Sprite inspector and tools |
| `@esengine/tilemap-editor` | Visual tilemap editor with brush tools |
| `@esengine/physics-rapier2d-editor` | Physics collider visualization and editing |
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
| `@esengine/blueprint-editor` | Visual scripting editor |
| `@esengine/material-editor` | Material and shader editor |
| `@esengine/shader-editor` | Shader code editor |
### Platform
| Package | Description |
|---------|-------------|
| `@esengine/platform-common` | Platform abstraction interfaces |
| `@esengine/platform-web` | Web browser runtime |
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
## Editor
ESEngine Editor is a cross-platform desktop application built with Tauri and React.
### Features
- Scene hierarchy and entity management
- Component inspector with custom editors
- Asset browser with drag-and-drop support
- Tilemap editor with paint, fill, and selection tools
- Behavior tree visual editor
- Blueprint visual scripting
- Material and shader editing
- Built-in performance profiler
- Localization support (English, Chinese)
### Screenshot
![ESEngine Editor](screenshots/main_screetshot.png)
## Supported Platforms
| Platform | Runtime | Editor |
|----------|---------|--------|
| Web Browser | Yes | - |
| Windows | - | Yes |
| macOS | - | Yes |
| WeChat Mini Game | In Progress | - |
| Playable Ads | Planned | - |
| Android | Planned | - |
| iOS | Planned | - |
| Windows Native | Planned | - |
| Other Platforms | Planned | - |
## Building from Source
### Prerequisites
- Node.js 18 or later
- pnpm 10 or later
- Rust toolchain (for WASM renderer)
- wasm-pack
### Setup
```bash
# Clone repository
git clone https://github.com/esengine/ecs-framework.git
cd ecs-framework
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Build WASM renderer (optional)
pnpm build:wasm
```
## 平台支持
### Running the Editor
支持主流游戏引擎和 Web 平台:
```bash
cd packages/editor-app
pnpm tauri:dev
```
- **Cocos Creator**
- **Laya 引擎**
- **原生 Web** - 浏览器环境直接运行
- **小游戏平台** - 微信、支付宝等小游戏
### Project Structure
## ECS Framework Editor
```
ecs-framework/
├── packages/ Engine packages (runtime, editor, platform)
├── docs/ Documentation source
├── examples/ Example projects
├── scripts/ Build utilities
└── thirdparty/ Third-party dependencies
```
跨平台桌面编辑器,提供可视化开发和调试工具。
## Documentation
### 主要功能
- [Getting Started](https://esengine.github.io/ecs-framework/guide/getting-started.html)
- [Architecture Guide](https://esengine.github.io/ecs-framework/guide/)
- [API Reference](https://esengine.github.io/ecs-framework/api/)
- **场景管理** - 可视化场景层级和实体管理
- **组件检视** - 实时查看和编辑实体组件
- **性能分析** - 内置 Profiler 监控系统性能
- **插件系统** - 可扩展的插件架构
- **远程调试** - 连接运行中的游戏进行实时调试
- **自动更新** - 支持热更新,自动获取最新版本
## Community
### 下载
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - Questions and ideas
[![Latest Release](https://img.shields.io/github/v/release/esengine/ecs-framework?label=下载最新版本&style=for-the-badge)](https://github.com/esengine/ecs-framework/releases/latest)
## Contributing
支持 Windows、macOS (Intel & Apple Silicon)
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
### 截图
1. Fork the repository
2. Create a feature branch
3. Make changes with tests
4. Submit a pull request
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
## License
<details>
<summary>查看更多截图</summary>
**性能分析器**
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
**插件管理**
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
**设置界面**
<img src="screenshots/settings.png" alt="Settings" width="600">
</details>
## 示例项目
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
## 文档
- [📚 AI智能文档](https://deepwiki.com/esengine/ecs-framework) - AI助手随时解答你的问题
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html) - 详细教程和平台集成
- [完整指南](https://esengine.github.io/ecs-framework/guide/) - ECS 概念和使用指南
- [API 参考](https://esengine.github.io/ecs-framework/api/) - 完整 API 文档
## 生态系统
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
## 💪 支持项目 / Support the Project
如果这个项目对你有帮助,请考虑:
If this project helps you, please consider:
<div align="center">
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-ea4aaa?style=for-the-badge&logo=github)](https://github.com/sponsors/esengine)
[![Star on GitHub](https://img.shields.io/badge/⭐_Star-on_GitHub-yellow?style=for-the-badge&logo=github)](https://github.com/esengine/ecs-framework)
</div>
- ⭐ 给项目点个 Star
- 🐛 报告 Bug 或提出新功能
- 📝 改进文档
- 💖 成为赞助者
## 社区与支持
- [问题反馈](https://github.com/esengine/ecs-framework/issues) - Bug 报告和功能建议
- [讨论区](https://github.com/esengine/ecs-framework/discussions) - 提问、分享想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - ecs游戏框架交流
## 贡献者 / Contributors
感谢所有为这个项目做出贡献的人!
Thanks goes to these wonderful people:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/esengine"><img src="https://avatars.githubusercontent.com/esengine?s=100" width="100px;" alt="esengine"/><br /><sub><b>esengine</b></sub></a><br /><a href="#maintenance-esengine" title="Maintenance">🚧</a> <a href="https://github.com/esengine/ecs-framework/commits?author=esengine" title="Code">💻</a> <a href="#design-esengine" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/foxling"><img src="https://avatars.githubusercontent.com/foxling?s=100" width="100px;" alt="LING YE"/><br /><sub><b>LING YE</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=foxling" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MirageTank"><img src="https://avatars.githubusercontent.com/MirageTank?s=100" width="100px;" alt="MirageTank"/><br /><sub><b>MirageTank</b></sub></a><br /><a href="https://github.com/esengine/ecs-framework/commits?author=MirageTank" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。欢迎任何形式的贡献!
## 许可证
[MIT](LICENSE) © 2025 ECS Framework
ESEngine is licensed under the [MIT License](LICENSE).

246
README_CN.md Normal file
View File

@@ -0,0 +1,246 @@
# ESEngine
[English](./README.md) | **中文**
**[文档](https://esengine.github.io/ecs-framework/) | [API 参考](https://esengine.github.io/ecs-framework/api/) | [示例](./examples/)**
ESEngine 是一个跨平台 2D 游戏引擎,提供统一的开发界面。它包含完整的常用工具集,让开发者专注于游戏创作本身。
游戏可以导出到多个平台,包括 Web 浏览器、微信小游戏等小游戏平台。
## 免费开源
ESEngine 基于 MIT 协议完全免费开源。无附加条件,无版税。你的游戏完全属于你。
## 特性
- **数据驱动架构**:基于 ECS实体-组件-系统)模式构建,提供灵活高效的游戏逻辑
- **高性能渲染**Rust/WebAssembly 2D 渲染器,支持精灵批处理和 WebGL 2.0
- **可视化编辑器**:跨平台桌面编辑器,包含场景管理、资源浏览器和可视化工具
- **模块化设计**:按需使用,每个功能都是独立模块,可单独引入
- **多平台支持**:一套代码部署到 Web、微信小游戏等多个平台
## 获取引擎
### 通过 npm 安装
```bash
npm install @esengine/ecs-framework
```
### 从源码构建
详见 [从源码构建](#从源码构建) 章节。
### 编辑器下载
预编译的编辑器可在 [Releases](https://github.com/esengine/ecs-framework/releases) 页面下载,支持 Windows 和 macOS。
## 快速开始
```typescript
import {
Core, Scene, Entity, Component, EntitySystem,
Matcher, Time, ECSComponent, ECSSystem
} from '@esengine/ecs-framework';
@ECSComponent('Position')
class Position extends Component {
x = 0;
y = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx = 0;
dy = 0;
}
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const pos = entity.getComponent(Position);
const vel = entity.getComponent(Velocity);
pos.x += vel.dx * Time.deltaTime;
pos.y += vel.dy * Time.deltaTime;
}
}
}
Core.create();
const scene = new Scene();
scene.addSystem(new MovementSystem());
const player = scene.createEntity('Player');
player.addComponent(new Position());
player.addComponent(new Velocity());
Core.setScene(scene);
// 游戏循环
let lastTime = 0;
function gameLoop(currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## 模块
ESEngine 采用模块化组织。每个功能都有运行时模块和可选的编辑器扩展。
### 核心
| 包名 | 描述 |
|------|------|
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
| `@esengine/math` | 向量、矩阵和数学工具 |
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
### 运行时模块
| 包名 | 描述 |
|------|------|
| `@esengine/sprite` | 2D 精灵渲染和动画 |
| `@esengine/tilemap` | Tilemap 渲染,支持动画 |
| `@esengine/physics-rapier2d` | 基于 Rapier 的 2D 物理模拟 |
| `@esengine/behavior-tree` | 行为树 AI 系统 |
| `@esengine/blueprint` | 可视化脚本运行时 |
| `@esengine/camera` | 相机控制和管理 |
| `@esengine/audio` | 音频播放 |
| `@esengine/ui` | UI 组件 |
| `@esengine/material-system` | 材质和着色器系统 |
| `@esengine/asset-system` | 资源加载和管理 |
### 编辑器扩展
| 包名 | 描述 |
|------|------|
| `@esengine/sprite-editor` | 精灵检视器和工具 |
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器,支持笔刷工具 |
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化和编辑 |
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
| `@esengine/material-editor` | 材质和着色器编辑器 |
| `@esengine/shader-editor` | 着色器代码编辑器 |
### 平台
| 包名 | 描述 |
|------|------|
| `@esengine/platform-common` | 平台抽象接口 |
| `@esengine/platform-web` | Web 浏览器运行时 |
| `@esengine/platform-wechat` | 微信小游戏运行时 |
## 编辑器
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
### 功能
- 场景层级和实体管理
- 组件检视器,支持自定义编辑器
- 资源浏览器,支持拖放
- Tilemap 编辑器,支持绘制、填充、选择工具
- 行为树可视化编辑器
- 蓝图可视化脚本
- 材质和着色器编辑
- 内置性能分析器
- 多语言支持(英文、中文)
### 截图
![ESEngine Editor](screenshots/main_screetshot.png)
## 支持的平台
| 平台 | 运行时 | 编辑器 |
|------|--------|--------|
| Web 浏览器 | 支持 | - |
| Windows | - | 支持 |
| macOS | - | 支持 |
| 微信小游戏 | 开发中 | - |
| Playable 可玩广告 | 计划中 | - |
| Android | 计划中 | - |
| iOS | 计划中 | - |
| Windows 原生 | 计划中 | - |
| 其他平台 | 计划中 | - |
## 从源码构建
### 前置要求
- Node.js 18 或更高版本
- pnpm 10 或更高版本
- Rust 工具链(用于 WASM 渲染器)
- wasm-pack
### 安装
```bash
# 克隆仓库
git clone https://github.com/esengine/ecs-framework.git
cd ecs-framework
# 安装依赖
pnpm install
# 构建所有包
pnpm build
# 构建 WASM 渲染器(可选)
pnpm build:wasm
```
### 运行编辑器
```bash
cd packages/editor-app
pnpm tauri:dev
```
### 项目结构
```
ecs-framework/
├── packages/ 引擎包(运行时、编辑器、平台)
├── docs/ 文档源码
├── examples/ 示例项目
├── scripts/ 构建工具
└── thirdparty/ 第三方依赖
```
## 文档
- [快速入门](https://esengine.github.io/ecs-framework/guide/getting-started.html)
- [架构指南](https://esengine.github.io/ecs-framework/guide/)
- [API 参考](https://esengine.github.io/ecs-framework/api/)
## 社区
- [GitHub Issues](https://github.com/esengine/ecs-framework/issues) - Bug 反馈和功能建议
- [GitHub Discussions](https://github.com/esengine/ecs-framework/discussions) - 问题和想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
## 贡献
欢迎贡献代码。提交 PR 前请阅读贡献指南。
1. Fork 仓库
2. 创建功能分支
3. 修改代码并测试
4. 提交 PR
## 许可证
ESEngine 基于 [MIT 协议](LICENSE) 开源。

View File

@@ -9,6 +9,184 @@ const corePackageJson = JSON.parse(
readFileSync(join(__dirname, '../../packages/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.behaviorTree,
link: `${prefix}/guide/behavior-tree/`,
items: [
{ text: t.sidebar.btGettingStarted, link: `${prefix}/guide/behavior-tree/getting-started` },
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/guide/behavior-tree/core-concepts` },
{ text: t.sidebar.btEditorGuide, link: `${prefix}/guide/behavior-tree/editor-guide` },
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/guide/behavior-tree/editor-workflow` },
{ text: t.sidebar.btCustomActions, link: `${prefix}/guide/behavior-tree/custom-actions` },
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/guide/behavior-tree/cocos-integration` },
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/guide/behavior-tree/laya-integration` },
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/guide/behavior-tree/advanced-usage` },
{ text: t.sidebar.btBestPractices, link: `${prefix}/guide/behavior-tree/best-practices` }
]
},
{ 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` }
]
}
],
[`${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.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: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/ecs-framework/releases'
}
]
}
export default defineConfig({
vite: {
plugins: [
@@ -28,175 +206,49 @@ export default defineConfig({
}
}
},
title: 'ECS Framework',
description: '高性能TypeScript ECS框架 - 为游戏开发而生',
lang: 'zh-CN',
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/ecs-framework/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/ecs-framework/edit/master/docs/:path',
text: en.common.editOnGithub
},
outline: {
level: [2, 3],
label: en.common.onThisPage
}
}
}
},
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '指南', link: '/guide/' },
{ text: 'API', link: '/api/README' },
{
text: '示例',
items: [
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' },
{ text: '割草机演示', link: 'https://github.com/esengine/lawn-mower-demo' }
]
},
{
text: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/ecs-framework/releases'
}
],
sidebar: {
'/guide/': [
{
text: '开始使用',
items: [
{ text: '快速开始', link: '/guide/getting-started' },
{ text: '指南概览', link: '/guide/' }
]
},
{
text: '核心概念',
collapsed: false,
items: [
{ text: '实体类 (Entity)', link: '/guide/entity' },
{ text: '组件系统 (Component)', link: '/guide/component' },
{ text: '实体查询系统', link: '/guide/entity-query' },
{
text: '系统架构 (System)',
link: '/guide/system',
items: [
{ text: 'Worker系统 (多线程)', link: '/guide/worker-system' }
]
},
{
text: '场景管理 (Scene)',
link: '/guide/scene',
items: [
{ text: 'SceneManager', link: '/guide/scene-manager' },
{ text: 'WorldManager', link: '/guide/world-manager' }
]
},
{
text: '行为树系统 (Behavior Tree)',
link: '/guide/behavior-tree/',
items: [
{ text: '快速开始', link: '/guide/behavior-tree/getting-started' },
{ text: '核心概念', link: '/guide/behavior-tree/core-concepts' },
{ text: '编辑器指南', link: '/guide/behavior-tree/editor-guide' },
{ text: '编辑器工作流', link: '/guide/behavior-tree/editor-workflow' },
{ text: '自定义动作组件', link: '/guide/behavior-tree/custom-actions' },
{ text: 'Cocos Creator集成', link: '/guide/behavior-tree/cocos-integration' },
{ text: 'Laya引擎集成', link: '/guide/behavior-tree/laya-integration' },
{ text: '高级用法', link: '/guide/behavior-tree/advanced-usage' },
{ text: '最佳实践', link: '/guide/behavior-tree/best-practices' }
]
},
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
{ text: '事件系统 (Event)', link: '/guide/event-system' },
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
{ text: '日志系统 (Logger)', link: '/guide/logging' }
]
},
{
text: '高级特性',
collapsed: false,
items: [
{ text: '服务容器 (Service Container)', link: '/guide/service-container' },
{ text: '插件系统 (Plugin System)', link: '/guide/plugin-system' }
]
},
{
text: '平台适配器',
link: '/guide/platform-adapter',
collapsed: false,
items: [
{ text: '浏览器适配器', link: '/guide/platform-adapter/browser' },
{ text: '微信小游戏适配器', link: '/guide/platform-adapter/wechat-minigame' },
{ text: 'Node.js适配器', link: '/guide/platform-adapter/nodejs' }
]
}
],
'/examples/': [
{
text: '示例',
items: [
{ text: '示例概览', link: '/examples/' },
{ text: 'Worker系统演示', link: '/examples/worker-system-demo' }
]
}
],
'/api/': [
{
text: 'API 参考',
items: [
{ text: '概述', link: '/api/README' },
{
text: '核心类',
collapsed: false,
items: [
{ text: 'Core', link: '/api/classes/Core' },
{ text: 'Scene', link: '/api/classes/Scene' },
{ text: 'World', link: '/api/classes/World' },
{ text: 'Entity', link: '/api/classes/Entity' },
{ text: 'Component', link: '/api/classes/Component' },
{ text: 'EntitySystem', link: '/api/classes/EntitySystem' }
]
},
{
text: '系统类',
collapsed: true,
items: [
{ text: 'PassiveSystem', link: '/api/classes/PassiveSystem' },
{ text: 'ProcessingSystem', link: '/api/classes/ProcessingSystem' },
{ text: 'IntervalSystem', link: '/api/classes/IntervalSystem' }
]
},
{
text: '工具类',
collapsed: true,
items: [
{ text: 'Matcher', link: '/api/classes/Matcher' },
{ text: 'Time', link: '/api/classes/Time' },
{ text: 'PerformanceMonitor', link: '/api/classes/PerformanceMonitor' },
{ text: 'DebugManager', link: '/api/classes/DebugManager' }
]
},
{
text: '接口',
collapsed: true,
items: [
{ text: 'IScene', link: '/api/interfaces/IScene' },
{ text: 'IComponent', link: '/api/interfaces/IComponent' },
{ text: 'ISystemBase', link: '/api/interfaces/ISystemBase' },
{ text: 'ICoreConfig', link: '/api/interfaces/ICoreConfig' }
]
},
{
text: '装饰器',
collapsed: true,
items: [
{ text: '@ECSComponent', link: '/api/functions/ECSComponent' },
{ text: '@ECSSystem', link: '/api/functions/ECSSystem' }
]
},
{
text: '枚举',
collapsed: true,
items: [
{ text: 'ECSEventType', link: '/api/enumerations/ECSEventType' },
{ text: 'LogLevel', link: '/api/enumerations/LogLevel' }
]
}
]
}
]
},
siteTitle: 'ESEngine',
socialLinks: [
{ icon: 'github', link: 'https://github.com/esengine/ecs-framework' }
@@ -207,18 +259,8 @@ export default defineConfig({
copyright: 'Copyright © 2025 ECS Framework'
},
editLink: {
pattern: 'https://github.com/esengine/ecs-framework/edit/master/docs/:path',
text: '在 GitHub 上编辑此页'
},
search: {
provider: 'local'
},
outline: {
level: [2, 3],
label: '目录'
}
},
@@ -227,7 +269,7 @@ export default defineConfig({
['link', { rel: 'icon', href: '/favicon.ico' }]
],
base: '/ecs-framework/',
base: '/',
cleanUrls: true,
markdown: {
@@ -237,4 +279,4 @@ export default defineConfig({
dark: 'github-dark'
}
}
})
})

View File

@@ -0,0 +1,85 @@
{
"nav": {
"home": "Home",
"quickStart": "Quick Start",
"guide": "Guide",
"api": "API",
"examples": "Examples",
"workerDemo": "Worker System Demo",
"lawnMowerDemo": "Lawn Mower Demo"
},
"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",
"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"
},
"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"
}
}

View File

@@ -0,0 +1,21 @@
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
}

View File

@@ -0,0 +1,85 @@
{
"nav": {
"home": "首页",
"quickStart": "快速开始",
"guide": "指南",
"api": "API",
"examples": "示例",
"workerDemo": "Worker系统演示",
"lawnMowerDemo": "割草机演示"
},
"sidebar": {
"gettingStarted": "开始使用",
"quickStart": "快速开始",
"guideOverview": "指南概览",
"coreConcepts": "核心概念",
"entity": "实体类 (Entity)",
"hierarchy": "层级系统 (Hierarchy)",
"component": "组件系统 (Component)",
"entityQuery": "实体查询系统",
"system": "系统架构 (System)",
"workerSystem": "Worker系统 (多线程)",
"scene": "场景管理 (Scene)",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"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": "枚举"
},
"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": "在这个页面上"
}
}

View File

@@ -0,0 +1,93 @@
<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>

View File

@@ -0,0 +1,422 @@
<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/ecs-framework" 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>

View File

@@ -0,0 +1,422 @@
<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/ecs-framework" 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>

View File

@@ -0,0 +1,594 @@
:root {
color-scheme: dark;
--vp-nav-height: 64px;
--es-bg-base: #1e1e1e;
--es-bg-elevated: #252526;
--es-bg-overlay: #2d2d2d;
--es-bg-input: #3c3c3c;
--es-bg-inset: #181818;
--es-bg-hover: #2a2d2e;
--es-bg-active: #37373d;
--es-bg-sidebar: #262626;
--es-bg-card: #2a2a2a;
--es-bg-header: #2d2d2d;
--es-text-primary: #cccccc;
--es-text-secondary: #9d9d9d;
--es-text-tertiary: #6a6a6a;
--es-text-inverse: #ffffff;
--es-text-muted: #aaaaaa;
--es-text-dim: #6a6a6a;
--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;
}
}

View File

@@ -0,0 +1,14 @@
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)
}
}

View File

@@ -0,0 +1,412 @@
# Quick Start
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
## Installation
### NPM Installation
```bash
# Using npm
npm install @esengine/ecs-framework
```
## Initialize Core
### Basic Initialization
The core of ECS Framework is the `Core` class, a singleton that manages the entire framework lifecycle.
```typescript
import { Core } from '@esengine/ecs-framework'
// Method 1: Using config object (recommended)
const core = Core.create({
debug: true, // Enable debug mode for detailed logs and performance monitoring
debugConfig: { // Optional: Advanced debug configuration
enabled: false, // Whether to enable WebSocket debug server
websocketUrl: 'ws://localhost:8080',
debugFrameRate: 30, // Debug data send frame rate
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
// Method 2: Simplified creation (backward compatible)
const core = Core.create(true); // Equivalent to { debug: true }
// Method 3: Production environment configuration
const core = Core.create({
debug: false // Disable debug in production
});
```
### Core Configuration Details
```typescript
interface ICoreConfig {
/** Enable debug mode - affects log level and performance monitoring */
debug?: boolean;
/** Advanced debug configuration - for dev tools integration */
debugConfig?: {
enabled: boolean; // Enable debug server
websocketUrl: string; // WebSocket server URL
autoReconnect?: boolean; // Auto reconnect
debugFrameRate?: 60 | 30 | 15; // Debug data send frame rate
channels: { // Data channel configuration
entities: boolean; // Entity data
systems: boolean; // System data
performance: boolean; // Performance data
components: boolean; // Component data
scenes: boolean; // Scene data
};
};
}
```
### Core Instance Management
Core uses singleton pattern, accessible via static property after creation:
```typescript
// Create instance
const core = Core.create(true);
// Get created instance
const instance = Core.Instance; // Returns current instance, null if not created
```
### Game Loop Integration
**Important**: Before creating entities and systems, you need to understand how to integrate ECS Framework into your game engine.
`Core.update(deltaTime)` is the framework heartbeat, must be called every frame. It handles:
- Updating the built-in Time class
- Updating all global managers (timers, object pools, etc.)
- Updating all entity systems in all scenes
- Processing entity creation and destruction
- Collecting performance data (in debug mode)
See engine integration examples: [Game Engine Integration](#game-engine-integration)
## Create Your First ECS Application
### 1. Define Components
Components are pure data containers that store entity state:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework'
// Position component
@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
}
}
// Velocity component
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0
dy: number = 0
constructor(dx: number = 0, dy: number = 0) {
super()
this.dx = dx
this.dy = dy
}
}
// Sprite component
@ECSComponent('Sprite')
class Sprite extends Component {
texture: string = ''
width: number = 32
height: number = 32
constructor(texture: string, width: number = 32, height: number = 32) {
super()
this.texture = texture
this.width = width
this.height = height
}
}
```
### 2. Create Entity Systems
Systems contain game logic and process entities with specific components. ECS Framework provides Matcher-based entity filtering:
```typescript
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework'
// Movement system - handles position and velocity
@ECSSystem('MovementSystem')
class MovementSystem extends EntitySystem {
constructor() {
// Use Matcher to define target entities: must have both Position and Velocity
super(Matcher.empty().all(Position, Velocity))
}
protected process(entities: readonly Entity[]): void {
// process method receives all matching entities
for (const entity of entities) {
const position = entity.getComponent(Position)!
const velocity = entity.getComponent(Velocity)!
// Update position (using framework's Time class)
position.x += velocity.dx * Time.deltaTime
position.y += velocity.dy * Time.deltaTime
// Boundary check example
if (position.x < 0) position.x = 0
if (position.y < 0) position.y = 0
}
}
}
// Render system - handles visible objects
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
constructor() {
// Must have Position and Sprite, optional Velocity (for direction)
super(Matcher.empty().all(Position, Sprite).any(Velocity))
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(Position)!
const sprite = entity.getComponent(Sprite)!
const velocity = entity.getComponent(Velocity) // May be null
// Flip sprite based on velocity direction (optional logic)
let flipX = false
if (velocity && velocity.dx < 0) {
flipX = true
}
// Render logic (pseudocode here)
this.drawSprite(sprite.texture, position.x, position.y, sprite.width, sprite.height, flipX)
}
}
private drawSprite(texture: string, x: number, y: number, width: number, height: number, flipX: boolean = false) {
// Actual render implementation depends on your game engine
const direction = flipX ? '<-' : '->'
console.log(`Render ${texture} at (${x.toFixed(1)}, ${y.toFixed(1)}) direction: ${direction}`)
}
}
```
### 3. Create Scene
Recommended to extend Scene class for custom scenes:
```typescript
import { Scene } from '@esengine/ecs-framework'
// Recommended: Extend Scene for custom scene
class GameScene extends Scene {
initialize(): void {
// Scene initialization logic
this.name = "MainScene";
// Add systems to scene
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
onStart(): void {
// Logic when scene starts running
console.log("Game scene started");
}
unload(): void {
// Cleanup logic when scene unloads
console.log("Game scene unloaded");
}
}
// Create and set scene
const gameScene = new GameScene();
Core.setScene(gameScene);
```
### 4. Create Entities
```typescript
// Create player entity
const player = gameScene.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 30)); // Move 50px/sec (x), 30px/sec (y)
player.addComponent(new Sprite("player.png", 64, 64));
```
## Scene Management
Core has built-in scene management, very simple to use:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// Initialize Core
Core.create({ debug: true });
// Create and set scene
class GameScene extends Scene {
initialize(): void {
this.name = "GamePlay";
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
}
}
const gameScene = new GameScene();
Core.setScene(gameScene);
// Game loop (auto-updates scene)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Auto-updates global services and scene
}
// Switch scenes
Core.loadScene(new MenuScene()); // Delayed switch (next frame)
Core.setScene(new GameScene()); // Immediate switch
// Access current scene
const currentScene = Core.scene;
// Using fluent API
const player = Core.ecsAPI?.createEntity('Player')
.addComponent(Position, 100, 100)
.addComponent(Velocity, 50, 0);
```
### Advanced: Using WorldManager for Multi-World
Only for complex server-side applications (MMO game servers, game room systems, etc.):
```typescript
import { Core, WorldManager } from '@esengine/ecs-framework';
// Initialize Core
Core.create({ debug: true });
// Get WorldManager from service container (Core auto-creates and registers it)
const worldManager = Core.services.resolve(WorldManager);
// Create multiple independent game worlds
const room1 = worldManager.createWorld('room_001');
const room2 = worldManager.createWorld('room_002');
// Create scenes in each world
const gameScene1 = room1.createScene('game', new GameScene());
const gameScene2 = room2.createScene('game', new GameScene());
// Activate scenes
room1.setSceneActive('game', true);
room2.setSceneActive('game', true);
// Game loop (need to manually update worlds)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Update global services
worldManager.updateAll(); // Manually update all worlds
}
```
## Game Engine Integration
### Laya Engine Integration
```typescript
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
// Initialize Laya
Laya.init(800, 600).then(() => {
// Initialize ECS
Core.create(true);
Core.setScene(new GameScene());
// Start game loop
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // Auto-updates global services and scene
});
});
```
### Cocos Creator Integration
```typescript
import { Component, _decorator } from 'cc';
import { Core } from '@esengine/ecs-framework';
const { ccclass } = _decorator;
@ccclass('ECSGameManager')
export class ECSGameManager extends Component {
onLoad() {
// Initialize ECS
Core.create(true);
Core.setScene(new GameScene());
}
update(deltaTime: number) {
// Auto-updates global services and scene
Core.update(deltaTime);
}
onDestroy() {
// Cleanup resources
Core.destroy();
}
}
```
## Next Steps
You've successfully created your first ECS application! Next you can:
- Check the complete [API Documentation](/api/README)
- Explore more [practical examples](/examples/)
## FAQ
### Why isn't my system executing?
Ensure:
1. System is added to scene: `this.addSystem(system)` (in Scene's initialize method)
2. Scene is set: `Core.setScene(scene)`
3. Game loop is calling: `Core.update(deltaTime)`
### How to debug ECS applications?
Enable debug mode:
```typescript
Core.create({ debug: true })
// Get debug data
const debugData = Core.getDebugData()
console.log(debugData)
```

43
docs/en/guide/index.md Normal file
View File

@@ -0,0 +1,43 @@
# Guide
Welcome to the ECS Framework Guide. This guide covers the core concepts and usage of the framework.
## Core Concepts
### [Entity](/guide/entity)
Learn the basics of ECS architecture - how to use entities, lifecycle management, and best practices.
### [Component](/guide/component)
Learn how to create and use components for modular game feature design.
### [System](/guide/system)
Master system development to implement game logic processing.
### [Entity Query & Matcher](/guide/entity-query)
Learn to use Matcher for entity filtering and queries with `all`, `any`, `none`, `nothing` conditions.
### [Scene](/guide/scene)
Understand scene lifecycle, system management, and entity container features.
### [Event System](/guide/event-system)
Master the type-safe event system for component communication and system coordination.
### [Serialization](/guide/serialization)
Master serialization for scenes, entities, and components. Supports full and incremental serialization for game saves, network sync, and more.
### [Time and Timers](/guide/time-and-timers)
Learn time management and timer systems for precise game logic timing control.
### [Logging](/guide/logging)
Master the leveled logging system for debugging, monitoring, and error tracking.
### [Platform Adapter](/guide/platform-adapter)
Learn how to implement and register platform adapters for browsers, mini-games, Node.js, and more.
## Advanced Features
### [Service Container](/guide/service-container)
Master dependency injection and service management for loosely-coupled architecture.
### [Plugin System](/guide/plugin-system)
Learn how to develop and use plugins to extend framework functionality.

317
docs/en/index.md Normal file
View File

@@ -0,0 +1,317 @@
---
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>

View File

@@ -55,25 +55,92 @@ class Health extends Component {
}
```
### 组件装饰器
### @ECSComponent 装饰器
**必须使用 `@ECSComponent` 装饰器**,这确保了:
- 组件在代码混淆后仍能正确识别
- 提供稳定的类型名称用于序列化和调试
- 框架能正确管理组件注册
`@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。
#### 为什么必须使用
| 功能 | 说明 |
|------|------|
| **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 |
| **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 |
| **组件注册** | 自动注册到 ComponentRegistry分配唯一的位掩码 |
| **调试支持** | 在调试工具和日志中显示可读的组件名称 |
#### 基本语法
```typescript
// 正确的用法
@ECSComponent(typeName: string)
```
- `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称
#### 使用示例
```typescript
// ✅ 正确的用法
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// 错误的用法 - 没有装饰器
class BadComponent extends Component {
// 这样定义的组件可能在生产环境出现问题
// ✅ 推荐:类型名与类名保持一致
@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 { }
```
## 组件生命周期

View File

@@ -121,6 +121,65 @@ class CombatSystem extends EntitySystem {
}
```
#### 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
@@ -493,6 +552,65 @@ const matcher2 = matcher.any(VelocityComponent);
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 参考

View File

@@ -9,6 +9,12 @@
- 提供唯一标识ID
- 管理组件的生命周期
::: tip 关于父子层级关系
实体间的父子层级关系通过 `HierarchyComponent``HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。
详见 [层级系统](./hierarchy.md) 文档。
:::
## 创建实体
**重要提示:实体必须通过场景创建,不支持手动创建!**
@@ -285,4 +291,10 @@ entity.components.forEach(component => {
});
```
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
## 下一步
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
- 了解 [组件系统](./component.md) 为实体添加功能
- 了解 [场景管理](./scene.md) 组织和管理实体

View File

@@ -23,7 +23,6 @@ import { Core } from '@esengine/ecs-framework'
// 方式1使用配置对象推荐
const core = Core.create({
debug: true, // 启用调试模式,提供详细的日志和性能监控
enableEntitySystems: true, // 启用实体系统这是ECS的核心功能
debugConfig: { // 可选:高级调试配置
enabled: false, // 是否启用WebSocket调试服务器
websocketUrl: 'ws://localhost:8080',
@@ -39,12 +38,11 @@ const core = Core.create({
});
// 方式2简化创建向后兼容
const core = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
const core = Core.create(true); // 等同于 { debug: true }
// 方式3生产环境配置
const core = Core.create({
debug: false, // 生产环境关闭调试
enableEntitySystems: true
debug: false // 生产环境关闭调试
});
```
@@ -55,9 +53,6 @@ interface ICoreConfig {
/** 是否启用调试模式 - 影响日志级别和性能监控 */
debug?: boolean;
/** 是否启用实体系统 - 核心ECS功能开关 */
enableEntitySystems?: boolean;
/** 高级调试配置 - 用于开发工具集成 */
debugConfig?: {
enabled: boolean; // 是否启用调试服务器

437
docs/guide/hierarchy.md Normal file
View File

@@ -0,0 +1,437 @@
# 层级系统
在游戏开发中实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent``HierarchySystem` 实现,完全遵循 ECS 组合原则。
## 设计理念
### 为什么不在 Entity 中内置层级?
传统的游戏对象模型(如 Unity 的 GameObject将层级关系内置于实体中。ECS Framework 选择组件化方案的原因:
1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备
2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent`
3. **数据与逻辑分离**`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑
4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化
## 基本概念
### HierarchyComponent
存储层级关系数据的组件:
```typescript
import { HierarchyComponent } from '@esengine/ecs-framework';
// HierarchyComponent 的核心属性
interface HierarchyComponent {
parentId: number | null; // 父实体 IDnull 表示根实体
childIds: number[]; // 子实体 ID 列表
depth: number; // 在层级中的深度(由系统维护)
bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护)
}
```
### HierarchySystem
处理层级逻辑的系统,提供所有层级操作的 API
```typescript
import { HierarchySystem } from '@esengine/ecs-framework';
// 获取系统
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
```
## 快速开始
### 添加系统到场景
```typescript
import { Scene, HierarchySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 添加层级系统
this.addSystem(new HierarchySystem());
// 添加其他系统...
}
}
```
### 建立父子关系
```typescript
// 创建实体
const parent = scene.createEntity("Parent");
const child1 = scene.createEntity("Child1");
const child2 = scene.createEntity("Child2");
// 获取层级系统
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
// 设置父子关系(自动添加 HierarchyComponent
hierarchySystem.setParent(child1, parent);
hierarchySystem.setParent(child2, parent);
// 现在 parent 有两个子实体
```
### 查询层级
```typescript
// 获取父实体
const parentEntity = hierarchySystem.getParent(child1);
// 获取所有子实体
const children = hierarchySystem.getChildren(parent);
// 获取子实体数量
const count = hierarchySystem.getChildCount(parent);
// 检查是否有子实体
const hasKids = hierarchySystem.hasChildren(parent);
// 获取在层级中的深度
const depth = hierarchySystem.getDepth(child1); // 返回 1
```
## API 参考
### 父子关系操作
#### setParent
设置实体的父级:
```typescript
// 设置父级
hierarchySystem.setParent(child, parent);
// 移动到根级(无父级)
hierarchySystem.setParent(child, null);
```
#### insertChildAt
在指定位置插入子实体:
```typescript
// 在第一个位置插入
hierarchySystem.insertChildAt(parent, child, 0);
// 追加到末尾
hierarchySystem.insertChildAt(parent, child, -1);
```
#### removeChild
从父级移除子实体(子实体变为根级):
```typescript
const success = hierarchySystem.removeChild(parent, child);
```
#### removeAllChildren
移除所有子实体:
```typescript
hierarchySystem.removeAllChildren(parent);
```
### 层级查询
#### getParent / getChildren
```typescript
const parent = hierarchySystem.getParent(entity);
const children = hierarchySystem.getChildren(entity);
```
#### getRoot
获取实体的根节点:
```typescript
const root = hierarchySystem.getRoot(deepChild);
```
#### getRootEntities
获取所有根实体(没有父级的实体):
```typescript
const roots = hierarchySystem.getRootEntities();
```
#### isAncestorOf / isDescendantOf
检查祖先/后代关系:
```typescript
// grandparent -> parent -> child
const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true
const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true
```
### 层级遍历
#### findChild
根据名称查找子实体:
```typescript
// 直接子级中查找
const child = hierarchySystem.findChild(parent, "ChildName");
// 递归查找所有后代
const deepChild = hierarchySystem.findChild(parent, "DeepChild", true);
```
#### findChildrenByTag
根据标签查找子实体:
```typescript
// 查找直接子级
const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY);
// 递归查找
const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true);
```
#### forEachChild
遍历子实体:
```typescript
// 遍历直接子级
hierarchySystem.forEachChild(parent, (child) => {
console.log(child.name);
});
// 递归遍历所有后代
hierarchySystem.forEachChild(parent, (child) => {
console.log(child.name);
}, true);
```
### 层级状态
#### isActiveInHierarchy
检查实体在层级中是否激活(考虑所有祖先的激活状态):
```typescript
// 如果 parent.active = false即使 child.active = true
// isActiveInHierarchy(child) 也会返回 false
const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child);
```
#### getDepth
获取实体在层级中的深度(根实体深度为 0
```typescript
const depth = hierarchySystem.getDepth(entity);
```
### 扁平化层级(用于 UI 渲染)
```typescript
// 用于实现可展开/折叠的层级树视图
const expandedIds = new Set([parent.id]);
const flatNodes = hierarchySystem.flattenHierarchy(expandedIds);
// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...]
```
## 完整示例
### 创建游戏角色层级
```typescript
import {
Scene,
HierarchySystem,
HierarchyComponent
} from '@esengine/ecs-framework';
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);
// 特效(挂载在武器上)
const effect = this.createEntity("WeaponEffect");
effect.addComponent(new ParticleEmitter());
this.hierarchySystem.setParent(effect, weapon);
// 查询层级信息
console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0
console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2
console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3
}
public equipNewWeapon(weaponName: string): void {
const body = this.findEntity("Body");
const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon");
if (oldWeapon) {
// 移除旧武器的所有子实体
this.hierarchySystem.removeAllChildren(oldWeapon);
oldWeapon.destroy();
}
// 创建新武器
const newWeapon = this.createEntity("Weapon");
newWeapon.addComponent(new Sprite(`${weaponName}.png`));
this.hierarchySystem.setParent(newWeapon, body!);
}
}
```
### 层级变换系统
结合 Transform 组件实现层级变换:
```typescript
import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework';
class HierarchyTransformSystem extends EntitySystem {
private hierarchySystem!: HierarchySystem;
constructor() {
super(Matcher.empty().all(Transform, HierarchyComponent));
}
public onAddedToScene(): void {
// 获取层级系统引用
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
}
protected process(entities: readonly Entity[]): void {
// 按深度排序,确保父级先更新
const sorted = [...entities].sort((a, b) => {
return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b);
});
for (const entity of sorted) {
const transform = entity.getComponent(Transform)!;
const parent = this.hierarchySystem.getParent(entity);
if (parent) {
const parentTransform = parent.getComponent(Transform);
if (parentTransform) {
// 计算世界坐标
transform.worldX = parentTransform.worldX + transform.localX;
transform.worldY = parentTransform.worldY + transform.localY;
}
} else {
// 根实体,本地坐标即世界坐标
transform.worldX = transform.localX;
transform.worldY = transform.localY;
}
}
}
}
```
## 性能优化
### 缓存机制
`HierarchySystem` 内置了缓存机制:
- `depth``bActiveInHierarchy` 由系统自动维护
- 使用 `bCacheDirty` 标记优化更新
- 层级变化时自动标记所有子级缓存为脏
### 最佳实践
1. **避免深层嵌套**:系统限制最大深度为 32 层
2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系
3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent`
4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem`
```typescript
// 好的做法
class MySystem extends EntitySystem {
private hierarchySystem!: HierarchySystem;
onAddedToScene() {
this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!;
}
process() {
// 使用缓存的引用
const parent = this.hierarchySystem.getParent(entity);
}
}
// 避免的做法
process() {
// 每次都获取,性能较差
const system = this.scene!.getEntityProcessor(HierarchySystem);
}
```
## 迁移指南
如果你之前使用的是旧版 Entity 内置的层级 API请参考以下迁移指南
| 旧 API (已移除) | 新 API |
|----------------|--------|
| `entity.parent` | `hierarchySystem.getParent(entity)` |
| `entity.children` | `hierarchySystem.getChildren(entity)` |
| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` |
| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` |
| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` |
| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` |
### 迁移示例
```typescript
// 旧代码
const parent = scene.createEntity("Parent");
const child = scene.createEntity("Child");
parent.addChild(child);
const found = parent.findChild("Child");
// 新代码
const hierarchySystem = scene.getEntityProcessor(HierarchySystem);
const parent = scene.createEntity("Parent");
const child = scene.createEntity("Child");
hierarchySystem.setParent(child, parent);
const found = hierarchySystem.findChild(parent, "Child");
```
## 下一步
- 了解 [实体类](./entity.md) 的其他功能
- 了解 [场景管理](./scene.md) 如何组织实体和系统
- 了解 [组件系统](./component.md) 如何定义和使用组件

View File

@@ -13,6 +13,9 @@
### [系统架构 (System)](./system.md)
掌握系统的编写方法,实现游戏逻辑的处理。
### [实体查询与 Matcher](./entity-query.md)
学习使用 Matcher 进行实体筛选和查询,掌握 `all``any``none``nothing` 等匹配条件。
### [场景管理 (Scene)](./scene.md)
了解场景的生命周期、系统管理和实体容器功能。

View File

@@ -190,6 +190,106 @@ class CollectionsComponent extends Component {
}
```
### 组件继承与序列化
框架完整支持组件类的继承,子类会自动继承父类的序列化字段,同时可以添加自己的字段。
#### 基础继承
```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;
}
```
### 场景自定义数据
除了实体和组件,还可以序列化场景级别的配置数据:

View File

@@ -33,6 +33,26 @@ class MyService implements IService {
}
```
#### 服务标识符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-标识符模式)。
#### 生命周期
服务容器支持两种生命周期:
@@ -44,7 +64,13 @@ class MyService implements IService {
### 访问服务容器
Core 类内置了服务容器,可以通过 `Core.services` 访问
ECS Framework 提供了三级服务容器
> **版本说明**World 服务容器功能在 v2.2.13+ 版本中可用
#### Core 级别服务容器
应用程序全局服务容器,可以通过 `Core.services` 访问:
```typescript
import { Core } from '@esengine/ecs-framework';
@@ -52,10 +78,53 @@ 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 系统、场景特定逻辑等)
### 注册服务
#### 注册单例服务
@@ -284,21 +353,20 @@ class GameService implements IService {
}
```
### @Inject 装饰器
### @InjectProperty 装饰器
在构造函数中注入依赖
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成
```typescript
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
@Injectable()
class PlayerService implements IService {
constructor(
@Inject(DataService) private data: DataService,
@Inject(GameService) private game: GameService
) {
// data 和 game 会自动从容器中解析
}
@InjectProperty(DataService)
private data!: DataService;
@InjectProperty(GameService)
private game!: GameService;
dispose(): void {
// 清理资源
@@ -306,6 +374,35 @@ class PlayerService implements IService {
}
```
在 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` 自动处理依赖注入:
@@ -313,10 +410,10 @@ class PlayerService implements IService {
```typescript
import { registerInjectable } from '@esengine/ecs-framework';
// 注册服务(会自动解析@Inject依赖
// 注册服务(会自动解析 @InjectProperty 依赖)
registerInjectable(Core.services, PlayerService);
// 解析时会自动注入依赖
// 解析时会自动注入属性依赖
const player = Core.services.resolve(PlayerService);
```
@@ -444,22 +541,164 @@ registerInjectable(Core.services, NetworkService);
## 高级用法
### 服务替换(测试)
### 接口与 Symbol 标识符模式
测试中替换真实服务为模拟服务:
大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
#### 为什么使用 Symbol.for
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol确保不同包中使用相同的标识符
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配
#### 定义接口和标识符
以音频服务为例游戏需要在不同平台Web、微信小游戏、原生App使用不同的音频实现
```typescript
// 测试代码
class MockDataService implements IService {
getData(key: string) {
return 'mock data';
}
dispose(): void {}
// 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>;
}
// 注册模拟服务(用于测试)
Core.services.registerInstance(DataService, new MockDataService());
// 使用 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
```
### 循环依赖检测

View File

@@ -157,8 +157,45 @@ const nameMatcher = Matcher.byName("Player"); // 匹配名称为 "Player" 的实
// 单组件匹配
const componentMatcher = Matcher.byComponent(Health); // 匹配拥有 Health 组件的实体
// 不匹配任何实体
const nothingMatcher = Matcher.nothing(); // 用于只需要生命周期回调的系统
```
### 空匹配器 vs Nothing 匹配器
```typescript
// empty() - 空条件,匹配所有实体
const emptyMatcher = Matcher.empty();
// nothing() - 不匹配任何实体,用于只需要生命周期方法的系统
const nothingMatcher = Matcher.nothing();
// 使用场景:只需要 onBegin/onEnd 生命周期的系统
@ECSSystem('FrameTimer')
class FrameTimerSystem extends EntitySystem {
constructor() {
super(Matcher.nothing()); // 不处理任何实体
}
protected onBegin(): void {
// 每帧开始时执行,例如:记录帧开始时间
console.log('帧开始');
}
protected process(entities: readonly Entity[]): void {
// 永远不会被调用,因为没有匹配的实体
}
protected onEnd(): void {
// 每帧结束时执行
console.log('帧结束');
}
}
```
> 💡 **提示**:更多关于 Matcher 和实体查询的详细用法,请参考 [实体查询系统](/guide/entity-query) 文档。
## 系统生命周期
系统提供了完整的生命周期回调:
@@ -563,9 +600,28 @@ class GameSystem extends EntitySystem {
}
```
### 2. 使用装饰器
### 2. 使用 @ECSSystem 装饰器
**必须使用 `@ECSSystem` 装饰器**
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
#### 为什么必须使用
| 功能 | 说明 |
|------|------|
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
| **系统管理** | 通过名称查找和管理系统 |
| **序列化支持** | 场景序列化时可以记录系统配置 |
#### 基本语法
```typescript
@ECSSystem(systemName: string)
```
- `systemName`: 系统的名称,建议使用描述性的名称
#### 使用示例
```typescript
// ✅ 正确的用法
@@ -574,12 +630,41 @@ class PhysicsSystem extends EntitySystem {
// 系统实现
}
// ✅ 推荐:使用描述性的名称
@ECSSystem('PlayerMovement')
class PlayerMovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Player, Position, Velocity));
}
}
// ❌ 错误的用法 - 没有装饰器
class BadSystem extends EntitySystem {
// 这样定义的系统可能在生产环境出现问题
// 这样定义的系统可能在生产环境出现问题
// 1. 代码压缩后类名变化,无法正确识别
// 2. 性能监控和调试工具显示不正确的名称
}
```
#### 系统名称的作用
```typescript
@ECSSystem('Combat')
class CombatSystem extends EntitySystem {
protected onInitialize(): void {
// 使用 systemName 属性访问系统名称
console.log(`系统 ${this.systemName} 已初始化`); // 输出: 系统 Combat 已初始化
}
}
// 通过名称查找系统
const combat = scene.getSystemByName('Combat');
// 性能监控中会显示系统名称
const perfData = combatSystem.getPerformanceData();
console.log(`${combatSystem.systemName} 执行时间: ${perfData?.executionTime}ms`);
```
### 3. 合理的更新顺序
```typescript

View File

@@ -435,7 +435,7 @@ const worldManager = Core.services.resolve(WorldManager);
// {
// maxWorlds: 50,
// autoCleanup: true,
// cleanupInterval: 30000 // 30 秒
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
// }
```

View File

@@ -1,23 +1,317 @@
---
layout: home
layout: page
title: ESEngine - 高性能 TypeScript ECS 框架
---
hero:
name: "ECS Framework"
text: "高性能ECS框架"
tagline: "为Javascript游戏开发而设计"
actions:
- theme: brand
text: 快速开始
link: /guide/getting-started
- theme: alt
text: 查看示例
link: https://github.com/esengine/lawn-mower-demo
<ParticleHero />
features:
- title: 高性能
details: 支持大规模实体处理
- title: 类型安全
details: 完整的TypeScript支持编译时类型检查
- title: 模块化设计
details: 核心功能独立打包,支持多平台
---
<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>

1
docs/public/CNAME Normal file
View File

@@ -0,0 +1 @@
esengine.cn

View File

@@ -65,7 +65,8 @@ export default [
'examples/lawn-mower-demo/**',
'extensions/**',
'**/*.min.js',
'**/*.d.ts'
'**/*.d.ts',
'**/wasm/**'
]
}
];

352
examples/core-demos/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,352 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@esengine/ecs-framework':
specifier: file:../../packages/core
version: file:../../packages/core
devDependencies:
typescript:
specifier: ^5.0.0
version: 5.9.3
vite:
specifier: ^4.0.0
version: 4.5.14
packages:
'@esbuild/android-arm64@0.18.20':
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.18.20':
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.18.20':
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.18.20':
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.18.20':
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.18.20':
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.18.20':
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.18.20':
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.18.20':
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.18.20':
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.18.20':
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.18.20':
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.18.20':
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.18.20':
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.18.20':
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.18.20':
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.18.20':
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.18.20':
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.18.20':
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.18.20':
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.18.20':
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.18.20':
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esengine/ecs-framework@file:../../packages/core':
resolution: {directory: ../../packages/core, type: directory}
esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
rollup@3.29.5:
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
vite@4.5.14:
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
snapshots:
'@esbuild/android-arm64@0.18.20':
optional: true
'@esbuild/android-arm@0.18.20':
optional: true
'@esbuild/android-x64@0.18.20':
optional: true
'@esbuild/darwin-arm64@0.18.20':
optional: true
'@esbuild/darwin-x64@0.18.20':
optional: true
'@esbuild/freebsd-arm64@0.18.20':
optional: true
'@esbuild/freebsd-x64@0.18.20':
optional: true
'@esbuild/linux-arm64@0.18.20':
optional: true
'@esbuild/linux-arm@0.18.20':
optional: true
'@esbuild/linux-ia32@0.18.20':
optional: true
'@esbuild/linux-loong64@0.18.20':
optional: true
'@esbuild/linux-mips64el@0.18.20':
optional: true
'@esbuild/linux-ppc64@0.18.20':
optional: true
'@esbuild/linux-riscv64@0.18.20':
optional: true
'@esbuild/linux-s390x@0.18.20':
optional: true
'@esbuild/linux-x64@0.18.20':
optional: true
'@esbuild/netbsd-x64@0.18.20':
optional: true
'@esbuild/openbsd-x64@0.18.20':
optional: true
'@esbuild/sunos-x64@0.18.20':
optional: true
'@esbuild/win32-arm64@0.18.20':
optional: true
'@esbuild/win32-ia32@0.18.20':
optional: true
'@esbuild/win32-x64@0.18.20':
optional: true
'@esengine/ecs-framework@file:../../packages/core':
dependencies:
tslib: 2.8.1
esbuild@0.18.20:
optionalDependencies:
'@esbuild/android-arm': 0.18.20
'@esbuild/android-arm64': 0.18.20
'@esbuild/android-x64': 0.18.20
'@esbuild/darwin-arm64': 0.18.20
'@esbuild/darwin-x64': 0.18.20
'@esbuild/freebsd-arm64': 0.18.20
'@esbuild/freebsd-x64': 0.18.20
'@esbuild/linux-arm': 0.18.20
'@esbuild/linux-arm64': 0.18.20
'@esbuild/linux-ia32': 0.18.20
'@esbuild/linux-loong64': 0.18.20
'@esbuild/linux-mips64el': 0.18.20
'@esbuild/linux-ppc64': 0.18.20
'@esbuild/linux-riscv64': 0.18.20
'@esbuild/linux-s390x': 0.18.20
'@esbuild/linux-x64': 0.18.20
'@esbuild/netbsd-x64': 0.18.20
'@esbuild/openbsd-x64': 0.18.20
'@esbuild/sunos-x64': 0.18.20
'@esbuild/win32-arm64': 0.18.20
'@esbuild/win32-ia32': 0.18.20
'@esbuild/win32-x64': 0.18.20
fsevents@2.3.3:
optional: true
nanoid@3.3.11: {}
picocolors@1.1.1: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@3.29.5:
optionalDependencies:
fsevents: 2.3.3
source-map-js@1.2.1: {}
tslib@2.8.1: {}
typescript@5.9.3: {}
vite@4.5.14:
dependencies:
esbuild: 0.18.20
postcss: 8.5.6
rollup: 3.29.5
optionalDependencies:
fsevents: 2.3.3

27061
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"version": "2.1.29",
"description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件",
"private": true,
"packageManager": "pnpm@10.22.0",
"workspaces": [
"packages/*"
],
@@ -17,36 +18,26 @@
],
"scripts": {
"bootstrap": "lerna bootstrap",
"clean": "lerna run clean",
"build": "npm run build:core && npm run build:math && npm run build:network-shared && npm run build:network-client && npm run build:network-server",
"build:core": "cd packages/core && npm run build",
"build:math": "cd packages/math && npm run build",
"build:network-shared": "cd packages/network-shared && npm run build",
"build:network-client": "cd packages/network-client && npm run build",
"build:network-server": "cd packages/network-server && npm run build",
"build:npm": "npm run build:npm:core && npm run build:npm:math && npm run build:npm:network-shared && npm run build:npm:network-client && npm run build:npm:network-server",
"clean": "turbo run clean",
"build": "turbo run build",
"build:filter": "turbo run build --filter",
"build:core": "turbo run build --filter=@esengine/ecs-framework",
"build:math": "turbo run build --filter=@esengine/ecs-framework-math",
"build:editor": "turbo run build --filter=@esengine/editor-app...",
"build:npm": "turbo run build:npm",
"build:npm:core": "cd packages/core && npm run build:npm",
"build:npm:math": "cd packages/math && npm run build:npm",
"build:npm:network-shared": "cd packages/network-shared && npm run build:npm",
"build:npm:network-client": "cd packages/network-client && npm run build:npm",
"build:npm:network-server": "cd packages/network-server && npm run build:npm",
"test": "lerna run test",
"test:coverage": "lerna run test:coverage",
"test:ci": "lerna run test:ci",
"test": "turbo run test",
"test:coverage": "turbo run test:coverage",
"test:ci": "turbo run test:ci",
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
"sync:versions": "node scripts/sync-versions.cjs",
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
"publish:all:dist": "npm run publish:core && npm run publish:math && npm run publish:network-shared && npm run publish:network-client && npm run publish:network-server",
"publish:all:dist": "npm run publish:core && npm run publish:math",
"publish:core": "cd packages/core && npm run publish:npm",
"publish:core:patch": "cd packages/core && npm run publish:patch",
"publish:math": "cd packages/math && npm run publish:npm",
"publish:math:patch": "cd packages/math && npm run publish:patch",
"publish:network-shared": "cd packages/network-shared && npm run publish:npm",
"publish:network-shared:patch": "cd packages/network-shared && npm run publish:patch",
"publish:network-client": "cd packages/network-client && npm run publish:npm",
"publish:network-client:patch": "cd packages/network-client && npm run publish:patch",
"publish:network-server": "cd packages/network-server && npm run publish:npm",
"publish:network-server:patch": "cd packages/network-server && npm run publish:patch",
"publish": "lerna publish",
"version": "lerna version",
"release": "semantic-release",
@@ -63,15 +54,19 @@
"copy:worker-demo": "node scripts/update-worker-demo.js",
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"type-check": "lerna run type-check",
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix"
"type-check": "turbo run type-check",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg",
"copy-modules": "node scripts/copy-engine-modules.mjs"
},
"author": "yhh",
"license": "MIT",
"devDependencies": {
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@eslint/js": "^9.39.1",
"@iconify/json": "^2.2.388",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -101,9 +96,11 @@
"semver": "^7.6.3",
"size-limit": "^11.0.2",
"ts-jest": "^29.4.0",
"turbo": "^2.6.1",
"typedoc": "^0.28.13",
"typedoc-plugin-markdown": "^4.9.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.47.0",
"unplugin-icons": "^22.3.0",
"vitepress": "^1.6.4"
},
@@ -124,3 +121,4 @@
"ws": "^8.18.2"
}
}

View File

@@ -0,0 +1,50 @@
{
"name": "@esengine/asset-system-editor",
"version": "1.0.0",
"description": "Editor-side asset management: meta files, packing, and bundling",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"clean": "rimraf dist",
"type-check": "tsc --noEmit"
},
"keywords": [
"ecs",
"asset",
"editor",
"bundle",
"packing"
],
"author": "yhh",
"license": "MIT",
"dependencies": {
"@esengine/asset-system": "workspace:*"
},
"devDependencies": {
"@esengine/build-config": "workspace:*",
"rimraf": "^5.0.0",
"tsup": "^8.0.0",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/asset-system-editor"
}
}

View File

@@ -0,0 +1,39 @@
/**
* Asset System Editor
* 资产系统编辑器模块
*
* Editor-side asset management:
* - Meta files (.meta) management
* - Asset packing and bundling
* - Import settings
*
* 编辑器端资产管理:
* - 元数据文件 (.meta) 管理
* - 资产打包和捆绑
* - 导入设置
*/
// Meta file management
export {
AssetMetaManager,
type IAssetMeta,
type IImportSettings,
type IMetaFileSystem,
generateGUID,
getMetaFilePath,
inferAssetType,
getDefaultImportSettings,
createAssetMeta,
serializeAssetMeta,
parseAssetMeta,
isValidGUID
} from './meta/AssetMetaFile';
// Asset packing
export {
AssetPacker,
collectSceneAssets,
type IPackingResult,
type IPackedBundle,
type IAssetFileReader
} from './packing/AssetPacker';

View File

@@ -0,0 +1,424 @@
/**
* Asset Meta File (.meta) Management
* 资产元数据文件 (.meta) 管理
*
* Each asset file has a companion .meta file that stores:
* - GUID: Persistent unique identifier
* - Import settings: How to process the asset
* - Labels: User-defined tags
*
* 每个资产文件都有一个配套的 .meta 文件,存储:
* - GUID持久化唯一标识符
* - 导入设置:如何处理资产
* - 标签:用户定义的标签
*/
import { AssetGUID, AssetType } from '@esengine/asset-system';
/**
* Meta file content structure
* 元数据文件内容结构
*/
export interface IAssetMeta {
/** Persistent unique identifier | 持久化唯一标识符 */
guid: AssetGUID;
/** Asset type | 资产类型 */
type: AssetType;
/** Import settings | 导入设置 */
importSettings?: IImportSettings;
/** User-defined labels | 用户定义的标签 */
labels?: string[];
/** Meta file version | 元数据文件版本 */
version: number;
/** Last modified timestamp | 最后修改时间戳 */
lastModified?: number;
}
/**
* Import settings for different asset types
* 不同资产类型的导入设置
*/
export interface IImportSettings {
// Texture settings | 纹理设置
maxSize?: number;
compression?: 'none' | 'dxt' | 'etc2' | 'astc' | 'webp';
generateMipmaps?: boolean;
filterMode?: 'point' | 'bilinear' | 'trilinear';
wrapMode?: 'clamp' | 'repeat' | 'mirror';
premultiplyAlpha?: boolean;
// Audio settings | 音频设置
audioFormat?: 'mp3' | 'ogg' | 'wav';
sampleRate?: number;
channels?: 1 | 2;
normalize?: boolean;
// General settings | 通用设置
[key: string]: unknown;
}
/**
* Generate a new UUID v4
* 生成新的 UUID v4
*/
export function generateGUID(): AssetGUID {
// Use crypto.randomUUID if available (modern browsers/Node 19+)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback implementation
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Get meta file path for an asset
* 获取资产的元数据文件路径
*/
export function getMetaFilePath(assetPath: string): string {
return `${assetPath}.meta`;
}
/**
* Infer asset type from file extension
* 根据文件扩展名推断资产类型
*/
export function inferAssetType(path: string): AssetType {
const ext = path.split('.').pop()?.toLowerCase() || '';
const typeMap: Record<string, AssetType> = {
// Textures
png: 'texture',
jpg: 'texture',
jpeg: 'texture',
gif: 'texture',
webp: 'texture',
bmp: 'texture',
svg: 'texture',
// Audio
mp3: 'audio',
wav: 'audio',
ogg: 'audio',
m4a: 'audio',
flac: 'audio',
// Data
json: 'json',
txt: 'text',
xml: 'text',
csv: 'text',
// Scenes and prefabs
ecs: 'scene',
prefab: 'prefab',
// Fonts
ttf: 'font',
otf: 'font',
woff: 'font',
woff2: 'font',
// Shaders
glsl: 'shader',
vert: 'shader',
frag: 'shader',
// Custom types (plugins)
tilemap: 'tilemap',
tileset: 'tileset',
btree: 'behavior-tree',
bp: 'blueprint',
mat: 'material'
};
return typeMap[ext] || 'binary';
}
/**
* Get default import settings for asset type
* 获取资产类型的默认导入设置
*/
export function getDefaultImportSettings(type: AssetType): IImportSettings {
switch (type) {
case 'texture':
return {
maxSize: 2048,
compression: 'none',
generateMipmaps: false,
filterMode: 'bilinear',
wrapMode: 'clamp',
premultiplyAlpha: false
};
case 'audio':
return {
audioFormat: 'mp3',
sampleRate: 44100,
channels: 2,
normalize: false
};
default:
return {};
}
}
/**
* Create a new meta file content
* 创建新的元数据文件内容
*/
export function createAssetMeta(assetPath: string, overrides?: Partial<IAssetMeta>): IAssetMeta {
const type = overrides?.type || inferAssetType(assetPath);
return {
guid: overrides?.guid || generateGUID(),
type,
importSettings: overrides?.importSettings || getDefaultImportSettings(type),
labels: overrides?.labels || [],
version: 1,
lastModified: Date.now()
};
}
/**
* Serialize meta to JSON string
* 将元数据序列化为 JSON 字符串
*/
export function serializeAssetMeta(meta: IAssetMeta): string {
return JSON.stringify(meta, null, 2);
}
/**
* Parse meta from JSON string
* 从 JSON 字符串解析元数据
*/
export function parseAssetMeta(json: string): IAssetMeta {
const meta = JSON.parse(json) as IAssetMeta;
// Validate required fields
if (!meta.guid || typeof meta.guid !== 'string') {
throw new Error('Invalid meta file: missing or invalid guid');
}
if (!meta.type || typeof meta.type !== 'string') {
throw new Error('Invalid meta file: missing or invalid type');
}
// Set defaults for optional fields
meta.version = meta.version || 1;
meta.labels = meta.labels || [];
meta.importSettings = meta.importSettings || {};
return meta;
}
/**
* Validate GUID format (UUID v4)
* 验证 GUID 格式 (UUID v4)
*/
export function isValidGUID(guid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(guid);
}
/**
* Asset Meta File Manager
* 资产元数据文件管理器
*
* Handles reading/writing .meta files through a file system interface.
*/
export class AssetMetaManager {
private _cache = new Map<string, IAssetMeta>();
private _guidToPath = new Map<AssetGUID, string>();
/**
* File system interface for reading/writing files
* 用于读写文件的文件系统接口
*/
private _fs: IMetaFileSystem | null = null;
/**
* Set file system interface
* 设置文件系统接口
*/
setFileSystem(fs: IMetaFileSystem): void {
this._fs = fs;
}
/**
* Get or create meta for an asset
* 获取或创建资产的元数据
*/
async getOrCreateMeta(assetPath: string): Promise<IAssetMeta> {
// Check cache first
const cached = this._cache.get(assetPath);
if (cached) {
return cached;
}
const metaPath = getMetaFilePath(assetPath);
// Try to read existing meta file
if (this._fs) {
try {
if (await this._fs.exists(metaPath)) {
const content = await this._fs.readText(metaPath);
const meta = parseAssetMeta(content);
this._cache.set(assetPath, meta);
this._guidToPath.set(meta.guid, assetPath);
return meta;
}
} catch (e) {
console.warn(`Failed to read meta file: ${metaPath}`, e);
}
}
// Create new meta
const meta = createAssetMeta(assetPath);
this._cache.set(assetPath, meta);
this._guidToPath.set(meta.guid, assetPath);
// Save to file system
if (this._fs) {
try {
await this._fs.writeText(metaPath, serializeAssetMeta(meta));
} catch (e) {
console.warn(`Failed to write meta file: ${metaPath}`, e);
}
}
return meta;
}
/**
* Get meta by GUID
* 根据 GUID 获取元数据
*/
getMetaByGUID(guid: AssetGUID): IAssetMeta | undefined {
const path = this._guidToPath.get(guid);
return path ? this._cache.get(path) : undefined;
}
/**
* Get asset path by GUID
* 根据 GUID 获取资产路径
*/
getPathByGUID(guid: AssetGUID): string | undefined {
return this._guidToPath.get(guid);
}
/**
* Get GUID by asset path
* 根据资产路径获取 GUID
*/
async getGUIDByPath(assetPath: string): Promise<AssetGUID> {
const meta = await this.getOrCreateMeta(assetPath);
return meta.guid;
}
/**
* Update meta and save
* 更新元数据并保存
*/
async updateMeta(assetPath: string, updates: Partial<IAssetMeta>): Promise<void> {
const meta = await this.getOrCreateMeta(assetPath);
// Apply updates
Object.assign(meta, updates);
meta.lastModified = Date.now();
meta.version++;
// Update cache
this._cache.set(assetPath, meta);
// Handle GUID change (rare, but possible)
if (updates.guid && updates.guid !== meta.guid) {
this._guidToPath.delete(meta.guid);
this._guidToPath.set(updates.guid, assetPath);
}
// Save to file system
if (this._fs) {
const metaPath = getMetaFilePath(assetPath);
await this._fs.writeText(metaPath, serializeAssetMeta(meta));
}
}
/**
* Handle asset rename
* 处理资产重命名
*/
async handleAssetRename(oldPath: string, newPath: string): Promise<void> {
const meta = this._cache.get(oldPath);
if (meta) {
// Update cache with new path
this._cache.delete(oldPath);
this._cache.set(newPath, meta);
this._guidToPath.set(meta.guid, newPath);
// Move meta file
if (this._fs) {
const oldMetaPath = getMetaFilePath(oldPath);
const newMetaPath = getMetaFilePath(newPath);
if (await this._fs.exists(oldMetaPath)) {
const content = await this._fs.readText(oldMetaPath);
await this._fs.writeText(newMetaPath, content);
await this._fs.delete(oldMetaPath);
}
}
}
}
/**
* Handle asset delete
* 处理资产删除
*/
async handleAssetDelete(assetPath: string): Promise<void> {
const meta = this._cache.get(assetPath);
if (meta) {
this._cache.delete(assetPath);
this._guidToPath.delete(meta.guid);
// Delete meta file
if (this._fs) {
const metaPath = getMetaFilePath(assetPath);
if (await this._fs.exists(metaPath)) {
await this._fs.delete(metaPath);
}
}
}
}
/**
* Clear cache
* 清除缓存
*/
clear(): void {
this._cache.clear();
this._guidToPath.clear();
}
/**
* Get all cached metas
* 获取所有缓存的元数据
*/
getAllMetas(): Map<string, IAssetMeta> {
return new Map(this._cache);
}
}
/**
* File system interface for meta file operations
* 元数据文件操作的文件系统接口
*/
export interface IMetaFileSystem {
exists(path: string): Promise<boolean>;
readText(path: string): Promise<string>;
writeText(path: string, content: string): Promise<void>;
delete(path: string): Promise<void>;
}

View File

@@ -0,0 +1,408 @@
/**
* Asset Packer
* 资产打包器
*
* Collects and packs assets into bundles for runtime loading.
* 收集并将资产打包成运行时加载的包。
*/
import {
AssetGUID,
AssetType,
IBundleManifest,
IBundleAssetInfo,
IRuntimeCatalog,
IRuntimeBundleInfo,
IRuntimeAssetLocation,
IAssetToPack,
IBundlePackOptions
} from '@esengine/asset-system';
import { IAssetMeta } from '../meta/AssetMetaFile';
/**
* Packing result
* 打包结果
*/
export interface IPackingResult {
/** Generated bundles | 生成的包 */
bundles: IPackedBundle[];
/** Runtime catalog | 运行时目录 */
catalog: IRuntimeCatalog;
/** Total size in bytes | 总大小 */
totalSize: number;
/** Number of assets packed | 打包的资产数量 */
assetCount: number;
/** Packing duration in ms | 打包耗时 */
duration: number;
}
/**
* Packed bundle
* 已打包的包
*/
export interface IPackedBundle {
/** Bundle name | 包名称 */
name: string;
/** Bundle data | 包数据 */
data: ArrayBuffer;
/** Bundle manifest | 包清单 */
manifest: IBundleManifest;
}
/**
* Asset file reader interface
* 资产文件读取器接口
*/
export interface IAssetFileReader {
readBinary(path: string): Promise<ArrayBuffer>;
readText(path: string): Promise<string>;
exists(path: string): Promise<boolean>;
}
/**
* Asset Packer
* 资产打包器
*/
export class AssetPacker {
private _fileReader: IAssetFileReader | null = null;
private _assets: IAssetToPack[] = [];
private _metas = new Map<AssetGUID, IAssetMeta>();
/**
* Set file reader for loading asset data
* 设置用于加载资产数据的文件读取器
*/
setFileReader(reader: IAssetFileReader): void {
this._fileReader = reader;
}
/**
* Add asset to pack
* 添加要打包的资产
*/
addAsset(asset: IAssetToPack, meta?: IAssetMeta): void {
this._assets.push(asset);
if (meta) {
this._metas.set(asset.guid, meta);
}
}
/**
* Add multiple assets
* 添加多个资产
*/
addAssets(assets: IAssetToPack[]): void {
for (const asset of assets) {
this.addAsset(asset);
}
}
/**
* Clear all added assets
* 清除所有已添加的资产
*/
clear(): void {
this._assets = [];
this._metas.clear();
}
/**
* Pack assets into bundles
* 将资产打包成包
*/
async pack(options: IBundlePackOptions = { name: 'main' }): Promise<IPackingResult> {
const startTime = Date.now();
// Group assets for bundling
const groups = this._groupAssets(options);
// Pack each group into a bundle
const bundles: IPackedBundle[] = [];
const catalogAssets: Record<AssetGUID, IRuntimeAssetLocation> = {};
const catalogBundles: Record<string, IRuntimeBundleInfo> = {};
for (const [bundleName, assets] of groups) {
const packed = await this._packBundle(bundleName, assets, options);
bundles.push(packed);
// Add to catalog
catalogBundles[bundleName] = {
url: `assets/${bundleName}.bundle`,
size: packed.data.byteLength,
hash: await this._hashBuffer(packed.data),
preload: bundleName === 'core' || bundleName === 'main'
};
// Add asset locations
for (const assetInfo of packed.manifest.assets) {
catalogAssets[assetInfo.guid] = {
bundle: bundleName,
offset: assetInfo.offset,
size: assetInfo.size,
type: assetInfo.type,
name: assetInfo.name
};
}
}
// Create catalog
const catalog: IRuntimeCatalog = {
version: '1.0',
createdAt: Date.now(),
bundles: catalogBundles,
assets: catalogAssets
};
const totalSize = bundles.reduce((sum, b) => sum + b.data.byteLength, 0);
return {
bundles,
catalog,
totalSize,
assetCount: this._assets.length,
duration: Date.now() - startTime
};
}
/**
* Pack assets by type (textures.bundle, audio.bundle, etc.)
* 按类型打包资产
*/
async packByType(): Promise<IPackingResult> {
return this.pack({
name: 'main',
groupByType: true
});
}
/**
* Group assets for bundling
* 分组资产以便打包
*/
private _groupAssets(options: IBundlePackOptions): Map<string, IAssetToPack[]> {
const groups = new Map<string, IAssetToPack[]>();
if (options.groupByType) {
// Group by asset type
for (const asset of this._assets) {
const bundleName = this._getBundleNameForType(asset.type);
const group = groups.get(bundleName) || [];
group.push(asset);
groups.set(bundleName, group);
}
} else {
// Single bundle
groups.set(options.name, [...this._assets]);
}
// Handle max size splitting
if (options.maxSize) {
const splitGroups = new Map<string, IAssetToPack[]>();
for (const [name, assets] of groups) {
let currentSize = 0;
let partIndex = 0;
let currentGroup: IAssetToPack[] = [];
for (const asset of assets) {
const assetSize = asset.data?.byteLength || 0;
if (currentSize + assetSize > options.maxSize && currentGroup.length > 0) {
splitGroups.set(`${name}_${partIndex}`, currentGroup);
partIndex++;
currentGroup = [];
currentSize = 0;
}
currentGroup.push(asset);
currentSize += assetSize;
}
if (currentGroup.length > 0) {
const finalName = partIndex > 0 ? `${name}_${partIndex}` : name;
splitGroups.set(finalName, currentGroup);
}
}
return splitGroups;
}
return groups;
}
/**
* Get bundle name for asset type
* 获取资产类型的包名称
*/
private _getBundleNameForType(type: AssetType): string {
const typeGroups: Record<string, string[]> = {
textures: ['texture'],
audio: ['audio'],
data: ['json', 'text', 'binary', 'scene', 'prefab'],
fonts: ['font'],
shaders: ['shader', 'material'],
tilemaps: ['tilemap', 'tileset'],
scripts: ['behavior-tree', 'blueprint']
};
for (const [bundleName, types] of Object.entries(typeGroups)) {
if (types.includes(type)) {
return bundleName;
}
}
return 'misc';
}
/**
* Pack a single bundle
* 打包单个包
*/
private async _packBundle(
name: string,
assets: IAssetToPack[],
_options: IBundlePackOptions
): Promise<IPackedBundle> {
const assetInfos: IBundleAssetInfo[] = [];
const dataChunks: ArrayBuffer[] = [];
let currentOffset = 0;
// Load and pack each asset
for (const asset of assets) {
let data = asset.data;
// Load data if not provided
if (!data && this._fileReader) {
try {
data = await this._fileReader.readBinary(asset.path);
} catch (e) {
console.warn(`[AssetPacker] Failed to load asset: ${asset.path}`, e);
continue;
}
}
if (!data) {
console.warn(`[AssetPacker] No data for asset: ${asset.guid}`);
continue;
}
// Align to 4 bytes
const padding = (4 - (data.byteLength % 4)) % 4;
const paddedSize = data.byteLength + padding;
assetInfos.push({
guid: asset.guid,
name: asset.name,
type: asset.type,
offset: currentOffset,
size: data.byteLength
});
// Add data with padding
dataChunks.push(data);
if (padding > 0) {
dataChunks.push(new ArrayBuffer(padding));
}
currentOffset += paddedSize;
}
// Combine all data
const totalSize = dataChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
const bundleData = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of dataChunks) {
bundleData.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
}
// Create manifest
const manifest: IBundleManifest = {
name,
version: '1.0',
hash: await this._hashBuffer(bundleData.buffer),
compression: 'none',
size: bundleData.byteLength,
assets: assetInfos,
dependencies: [],
createdAt: Date.now()
};
return {
name,
data: bundleData.buffer,
manifest
};
}
/**
* Hash a buffer using SHA-256
* 使用 SHA-256 哈希缓冲区
*/
private async _hashBuffer(buffer: ArrayBuffer): Promise<string> {
// Use Web Crypto API if available
if (typeof crypto !== 'undefined' && crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
}
// Fallback: simple hash
const view = new Uint8Array(buffer);
let hash = 0;
for (let i = 0; i < view.length; i++) {
hash = ((hash << 5) - hash) + view[i];
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
}
/**
* Collect assets referenced by a scene
* 收集场景引用的资产
*/
export async function collectSceneAssets(
sceneData: unknown,
_metaManager: { getPathByGUID: (guid: AssetGUID) => string | undefined }
): Promise<AssetGUID[]> {
const guids = new Set<AssetGUID>();
// Recursively find all GUID references
function findGUIDs(obj: unknown): void {
if (!obj || typeof obj !== 'object') return;
if (Array.isArray(obj)) {
for (const item of obj) {
findGUIDs(item);
}
return;
}
const record = obj as Record<string, unknown>;
// Check for GUID fields
for (const [key, value] of Object.entries(record)) {
if (key.endsWith('Guid') || key.endsWith('GUID') || key === 'guid') {
if (typeof value === 'string' && isValidGUID(value)) {
guids.add(value);
}
} else if (typeof value === 'object') {
findGUIDs(value);
}
}
}
findGUIDs(sceneData);
return Array.from(guids);
}
/**
* Validate GUID format
* 验证 GUID 格式
*/
function isValidGUID(guid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(guid);
}

View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
external: ['@esengine/asset-system']
});

View File

@@ -0,0 +1,41 @@
{
"id": "asset-system",
"name": "@esengine/asset-system",
"displayName": "Asset System",
"description": "Asset loading, caching and management | 资源加载、缓存和管理",
"version": "1.0.0",
"category": "Core",
"icon": "FolderOpen",
"tags": [
"asset",
"resource",
"loader"
],
"isCore": true,
"defaultEnabled": true,
"isEngineModule": true,
"canContainContent": false,
"platforms": [
"web",
"desktop",
"mobile"
],
"dependencies": [
"core"
],
"exports": {
"loaders": [
"TextureLoader",
"JsonLoader",
"TextLoader",
"BinaryLoader"
],
"other": [
"AssetManager",
"AssetDatabase",
"AssetCache"
]
},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -0,0 +1,47 @@
{
"name": "@esengine/asset-system",
"version": "1.0.0",
"description": "Asset management system for ES Engine",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"clean": "rimraf dist",
"type-check": "tsc --noEmit"
},
"keywords": [
"ecs",
"asset",
"resource",
"bundle"
],
"author": "yhh",
"license": "MIT",
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/build-config": "workspace:*",
"rimraf": "^5.0.0",
"tsup": "^8.0.0",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/asset-system"
}
}

View File

@@ -0,0 +1,272 @@
/**
* Asset Bundle Format Definitions
* 资产包格式定义
*
* Binary format for efficient asset storage and loading.
* 用于高效资产存储和加载的二进制格式。
*/
import { AssetGUID, AssetType } from '../types/AssetTypes';
/**
* Bundle file magic number
* 包文件魔数
*/
export const BUNDLE_MAGIC = 'ESBNDL';
/**
* Bundle format version
* 包格式版本
*/
export const BUNDLE_VERSION = 1;
/**
* Bundle compression types
* 包压缩类型
*/
export enum BundleCompression {
None = 0,
Gzip = 1,
Brotli = 2
}
/**
* Bundle flags
* 包标志
*/
export enum BundleFlags {
None = 0,
Compressed = 1 << 0,
Encrypted = 1 << 1,
Streaming = 1 << 2
}
/**
* Asset type codes for binary serialization
* 用于二进制序列化的资产类型代码
*/
export const AssetTypeCode: Record<string, number> = {
texture: 1,
audio: 2,
json: 3,
text: 4,
binary: 5,
scene: 6,
prefab: 7,
font: 8,
shader: 9,
material: 10,
mesh: 11,
animation: 12,
tilemap: 20,
tileset: 21,
'behavior-tree': 22,
blueprint: 23
};
/**
* Bundle header structure (32 bytes)
* 包头结构 (32 字节)
*/
export interface IBundleHeader {
/** Magic number "ESBNDL" | 魔数 */
magic: string;
/** Format version | 格式版本 */
version: number;
/** Bundle flags | 包标志 */
flags: BundleFlags;
/** Compression type | 压缩类型 */
compression: BundleCompression;
/** Number of assets | 资产数量 */
assetCount: number;
/** TOC offset from start | TOC 偏移量 */
tocOffset: number;
/** Data offset from start | 数据偏移量 */
dataOffset: number;
}
/**
* Table of Contents entry (40 bytes per entry)
* 目录条目 (每条 40 字节)
*/
export interface IBundleTocEntry {
/** Asset GUID (16 bytes as UUID binary) | 资产 GUID */
guid: AssetGUID;
/** Asset type code | 资产类型代码 */
typeCode: number;
/** Offset from data section start | 相对于数据段起始的偏移 */
offset: number;
/** Compressed size in bytes | 压缩后大小 */
compressedSize: number;
/** Uncompressed size in bytes | 未压缩大小 */
uncompressedSize: number;
}
/**
* Bundle manifest (JSON sidecar file)
* 包清单 (JSON 附属文件)
*/
export interface IBundleManifest {
/** Bundle name | 包名称 */
name: string;
/** Bundle version | 包版本 */
version: string;
/** Content hash for integrity | 内容哈希 */
hash: string;
/** Compression type | 压缩类型 */
compression: 'none' | 'gzip' | 'brotli';
/** Total bundle size | 包总大小 */
size: number;
/** Assets in this bundle | 包含的资产 */
assets: IBundleAssetInfo[];
/** Dependencies on other bundles | 依赖的其他包 */
dependencies: string[];
/** Creation timestamp | 创建时间戳 */
createdAt: number;
}
/**
* Asset info in bundle manifest
* 包清单中的资产信息
*/
export interface IBundleAssetInfo {
/** Asset GUID | 资产 GUID */
guid: AssetGUID;
/** Asset name (for debugging) | 资产名称 (用于调试) */
name: string;
/** Asset type | 资产类型 */
type: AssetType;
/** Offset in bundle | 包内偏移 */
offset: number;
/** Size in bytes | 大小 */
size: number;
}
/**
* Runtime catalog format (loaded in browser)
* 运行时目录格式 (在浏览器中加载)
*/
export interface IRuntimeCatalog {
/** Catalog version | 目录版本 */
version: string;
/** Creation timestamp | 创建时间戳 */
createdAt: number;
/** Available bundles | 可用的包 */
bundles: Record<string, IRuntimeBundleInfo>;
/** Asset GUID to location mapping | 资产 GUID 到位置的映射 */
assets: Record<AssetGUID, IRuntimeAssetLocation>;
}
/**
* Bundle info in runtime catalog
* 运行时目录中的包信息
*/
export interface IRuntimeBundleInfo {
/** Bundle URL (relative to catalog) | 包 URL */
url: string;
/** Bundle size in bytes | 包大小 */
size: number;
/** Content hash | 内容哈希 */
hash: string;
/** Whether bundle is preloaded | 是否预加载 */
preload?: boolean;
}
/**
* Asset location in runtime catalog
* 运行时目录中的资产位置
*/
export interface IRuntimeAssetLocation {
/** Bundle name containing this asset | 包含此资产的包名 */
bundle: string;
/** Offset within bundle | 包内偏移 */
offset: number;
/** Size in bytes | 大小 */
size: number;
/** Asset type | 资产类型 */
type: AssetType;
/** Asset name (for debugging) | 资产名称 */
name?: string;
}
/**
* Bundle packing options
* 包打包选项
*/
export interface IBundlePackOptions {
/** Bundle name | 包名称 */
name: string;
/** Compression type | 压缩类型 */
compression?: BundleCompression;
/** Maximum bundle size (split if exceeded) | 最大包大小 */
maxSize?: number;
/** Group assets by type | 按类型分组资产 */
groupByType?: boolean;
/** Include asset names in bundle | 在包中包含资产名称 */
includeNames?: boolean;
}
/**
* Asset to pack
* 要打包的资产
*/
export interface IAssetToPack {
/** Asset GUID | 资产 GUID */
guid: AssetGUID;
/** Asset path (for reading) | 资产路径 */
path: string;
/** Asset type | 资产类型 */
type: AssetType;
/** Asset name | 资产名称 */
name: string;
/** Raw data (or null to read from path) | 原始数据 */
data?: ArrayBuffer;
}
/**
* Parse GUID from 16-byte binary
* 从 16 字节二进制解析 GUID
*/
export function parseGUIDFromBinary(bytes: Uint8Array): AssetGUID {
const hex = Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
}
/**
* Serialize GUID to 16-byte binary
* 将 GUID 序列化为 16 字节二进制
*/
export function serializeGUIDToBinary(guid: AssetGUID): Uint8Array {
const hex = guid.replace(/-/g, '');
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
/**
* Get type code from asset type string
* 从资产类型字符串获取类型代码
*/
export function getAssetTypeCode(type: AssetType): number {
return AssetTypeCode[type] || 0;
}
/**
* Get asset type string from type code
* 从类型代码获取资产类型字符串
*/
export function getAssetTypeFromCode(code: number): AssetType {
for (const [type, typeCode] of Object.entries(AssetTypeCode)) {
if (typeCode === code) {
return type as AssetType;
}
}
return 'binary';
}

View File

@@ -0,0 +1,130 @@
/**
* Asset cache implementation
* 资产缓存实现
*/
import { AssetGUID } from '../types/AssetTypes';
/**
* Cache entry
* 缓存条目
*/
interface CacheEntry {
guid: AssetGUID;
asset: unknown;
lastAccessTime: number;
accessCount: number;
}
/**
* Asset cache implementation
* 资产缓存实现
*/
export class AssetCache {
private readonly _cache = new Map<AssetGUID, CacheEntry>();
constructor() {
// 无配置,无限制缓存 / No config, unlimited cache
}
/**
* Get cached asset
* 获取缓存的资产
*/
get<T = unknown>(guid: AssetGUID): T | null {
const entry = this._cache.get(guid);
if (!entry) return null;
// 更新访问信息 / Update access info
entry.lastAccessTime = Date.now();
entry.accessCount++;
return entry.asset as T;
}
/**
* Set cached asset
* 设置缓存的资产
*/
set<T = unknown>(guid: AssetGUID, asset: T): void {
const now = Date.now();
const entry: CacheEntry = {
guid,
asset,
lastAccessTime: now,
accessCount: 1
};
// 如果已存在,更新 / Update if exists
const oldEntry = this._cache.get(guid);
if (oldEntry) {
entry.accessCount = oldEntry.accessCount + 1;
}
this._cache.set(guid, entry);
}
/**
* Check if asset is cached
* 检查资产是否缓存
*/
has(guid: AssetGUID): boolean {
return this._cache.has(guid);
}
/**
* Remove asset from cache
* 从缓存移除资产
*/
remove(guid: AssetGUID): void {
this._cache.delete(guid);
}
/**
* Clear all cache
* 清空所有缓存
*/
clear(): void {
this._cache.clear();
}
/**
* Get cache size
* 获取缓存大小
*/
getSize(): number {
return this._cache.size;
}
/**
* Get all cached GUIDs
* 获取所有缓存的GUID
*/
getAllGuids(): AssetGUID[] {
return Array.from(this._cache.keys());
}
/**
* Get cache statistics
* 获取缓存统计
*/
getStatistics(): {
count: number;
entries: Array<{
guid: AssetGUID;
accessCount: number;
lastAccessTime: number;
}>;
} {
const entries = Array.from(this._cache.values()).map((entry) => ({
guid: entry.guid,
accessCount: entry.accessCount,
lastAccessTime: entry.lastAccessTime
}));
return {
count: this._cache.size,
entries
};
}
}

View File

@@ -0,0 +1,501 @@
/**
* Asset database for managing asset metadata
* 用于管理资产元数据的资产数据库
*/
import {
AssetGUID,
AssetType,
IAssetMetadata,
IAssetCatalogEntry
} from '../types/AssetTypes';
/**
* Asset database implementation
* 资产数据库实现
*/
export class AssetDatabase {
private readonly _metadata = new Map<AssetGUID, IAssetMetadata>();
private readonly _pathToGuid = new Map<string, AssetGUID>();
private readonly _typeToGuids = new Map<AssetType, Set<AssetGUID>>();
private readonly _labelToGuids = new Map<string, Set<AssetGUID>>();
private readonly _dependencies = new Map<AssetGUID, Set<AssetGUID>>();
private readonly _dependents = new Map<AssetGUID, Set<AssetGUID>>();
/** Project root path for resolving relative paths. | 项目根路径,用于解析相对路径。 */
private _projectRoot: string | null = null;
/**
* Set project root path.
* 设置项目根路径。
*
* @param path - Absolute path to project root. | 项目根目录的绝对路径。
*/
setProjectRoot(path: string): void {
this._projectRoot = path;
}
/**
* Get project root path.
* 获取项目根路径。
*/
getProjectRoot(): string | null {
return this._projectRoot;
}
/**
* Resolve relative path to absolute path.
* 将相对路径解析为绝对路径。
*
* @param relativePath - Relative asset path (e.g., "assets/texture.png"). | 相对资产路径。
* @returns Absolute file system path. | 绝对文件系统路径。
*/
resolveAbsolutePath(relativePath: string): string {
// Already absolute path (Windows or Unix).
// 已经是绝对路径。
if (relativePath.match(/^[a-zA-Z]:/) || relativePath.startsWith('/')) {
return relativePath;
}
// No project root set, return as-is.
// 未设置项目根路径,原样返回。
if (!this._projectRoot) {
return relativePath;
}
// Join with project root.
// 与项目根路径拼接。
const separator = this._projectRoot.includes('\\') ? '\\' : '/';
const normalizedPath = relativePath.replace(/[/\\]/g, separator);
return `${this._projectRoot}${separator}${normalizedPath}`;
}
/**
* Convert absolute path to relative path.
* 将绝对路径转换为相对路径。
*
* @param absolutePath - Absolute file system path. | 绝对文件系统路径。
* @returns Relative asset path, or null if not under project root. | 相对资产路径。
*/
toRelativePath(absolutePath: string): string | null {
if (!this._projectRoot) {
return null;
}
const normalizedAbs = absolutePath.replace(/\\/g, '/');
const normalizedRoot = this._projectRoot.replace(/\\/g, '/');
if (normalizedAbs.startsWith(normalizedRoot)) {
return normalizedAbs.substring(normalizedRoot.length + 1);
}
return null;
}
/**
* Add asset to database
* 添加资产到数据库
*/
addAsset(metadata: IAssetMetadata): void {
const { guid, path, type, labels, dependencies } = metadata;
// 存储元数据 / Store metadata
this._metadata.set(guid, metadata);
this._pathToGuid.set(path, guid);
// 按类型索引 / Index by type
if (!this._typeToGuids.has(type)) {
this._typeToGuids.set(type, new Set());
}
this._typeToGuids.get(type)!.add(guid);
// 按标签索引 / Index by labels
labels.forEach((label) => {
if (!this._labelToGuids.has(label)) {
this._labelToGuids.set(label, new Set());
}
this._labelToGuids.get(label)!.add(guid);
});
// 建立依赖关系 / Establish dependencies
this.updateDependencies(guid, dependencies);
}
/**
* Remove asset from database
* 从数据库移除资产
*/
removeAsset(guid: AssetGUID): void {
const metadata = this._metadata.get(guid);
if (!metadata) return;
// 清理元数据 / Clean up metadata
this._metadata.delete(guid);
this._pathToGuid.delete(metadata.path);
// 清理类型索引 / Clean up type index
const typeSet = this._typeToGuids.get(metadata.type);
if (typeSet) {
typeSet.delete(guid);
if (typeSet.size === 0) {
this._typeToGuids.delete(metadata.type);
}
}
// 清理标签索引 / Clean up label indices
metadata.labels.forEach((label) => {
const labelSet = this._labelToGuids.get(label);
if (labelSet) {
labelSet.delete(guid);
if (labelSet.size === 0) {
this._labelToGuids.delete(label);
}
}
});
// 清理依赖关系 / Clean up dependencies
this.clearDependencies(guid);
}
/**
* Update asset metadata
* 更新资产元数据
*/
updateAsset(guid: AssetGUID, updates: Partial<IAssetMetadata>): void {
const metadata = this._metadata.get(guid);
if (!metadata) return;
// 如果路径改变,更新索引 / Update index if path changed
if (updates.path && updates.path !== metadata.path) {
this._pathToGuid.delete(metadata.path);
this._pathToGuid.set(updates.path, guid);
}
// 如果类型改变,更新索引 / Update index if type changed
if (updates.type && updates.type !== metadata.type) {
const oldTypeSet = this._typeToGuids.get(metadata.type);
if (oldTypeSet) {
oldTypeSet.delete(guid);
}
if (!this._typeToGuids.has(updates.type)) {
this._typeToGuids.set(updates.type, new Set());
}
this._typeToGuids.get(updates.type)!.add(guid);
}
// 如果依赖改变,更新关系 / Update relations if dependencies changed
if (updates.dependencies) {
this.updateDependencies(guid, updates.dependencies);
}
// 合并更新 / Merge updates
Object.assign(metadata, updates);
metadata.lastModified = Date.now();
metadata.version++;
}
/**
* Get asset metadata
* 获取资产元数据
*/
getMetadata(guid: AssetGUID): IAssetMetadata | undefined {
return this._metadata.get(guid);
}
/**
* Get metadata by path
* 通过路径获取元数据
*/
getMetadataByPath(path: string): IAssetMetadata | undefined {
const guid = this._pathToGuid.get(path);
return guid ? this._metadata.get(guid) : undefined;
}
/**
* Find assets by type
* 按类型查找资产
*/
findAssetsByType(type: AssetType): AssetGUID[] {
const guids = this._typeToGuids.get(type);
return guids ? Array.from(guids) : [];
}
/**
* Find assets by label
* 按标签查找资产
*/
findAssetsByLabel(label: string): AssetGUID[] {
const guids = this._labelToGuids.get(label);
return guids ? Array.from(guids) : [];
}
/**
* Find assets by multiple labels (AND operation)
* 按多个标签查找资产AND操作
*/
findAssetsByLabels(labels: string[]): AssetGUID[] {
if (labels.length === 0) return [];
let result: Set<AssetGUID> | null = null;
for (const label of labels) {
const labelGuids = this._labelToGuids.get(label);
if (!labelGuids || labelGuids.size === 0) return [];
if (!result) {
result = new Set(labelGuids);
} else {
// 交集 / Intersection
const intersection = new Set<AssetGUID>();
labelGuids.forEach((guid) => {
if (result!.has(guid)) {
intersection.add(guid);
}
});
result = intersection;
}
}
return result ? Array.from(result) : [];
}
/**
* Search assets by query
* 通过查询搜索资产
*/
searchAssets(query: {
name?: string;
type?: AssetType;
labels?: string[];
path?: string;
}): AssetGUID[] {
let results = Array.from(this._metadata.keys());
// 按名称过滤 / Filter by name
if (query.name) {
const nameLower = query.name.toLowerCase();
results = results.filter((guid) => {
const metadata = this._metadata.get(guid)!;
return metadata.name.toLowerCase().includes(nameLower);
});
}
// 按类型过滤 / Filter by type
if (query.type) {
const typeGuids = this._typeToGuids.get(query.type);
if (!typeGuids) return [];
results = results.filter((guid) => typeGuids.has(guid));
}
// 按标签过滤 / Filter by labels
if (query.labels && query.labels.length > 0) {
const labelResults = this.findAssetsByLabels(query.labels);
const labelSet = new Set(labelResults);
results = results.filter((guid) => labelSet.has(guid));
}
// 按路径过滤 / Filter by path
if (query.path) {
const pathLower = query.path.toLowerCase();
results = results.filter((guid) => {
const metadata = this._metadata.get(guid)!;
return metadata.path.toLowerCase().includes(pathLower);
});
}
return results;
}
/**
* Get asset dependencies
* 获取资产依赖
*/
getDependencies(guid: AssetGUID): AssetGUID[] {
const deps = this._dependencies.get(guid);
return deps ? Array.from(deps) : [];
}
/**
* Get asset dependents (assets that depend on this one)
* 获取资产的依赖者(依赖此资产的其他资产)
*/
getDependents(guid: AssetGUID): AssetGUID[] {
const deps = this._dependents.get(guid);
return deps ? Array.from(deps) : [];
}
/**
* Get all dependencies recursively
* 递归获取所有依赖
*/
getAllDependencies(guid: AssetGUID, visited = new Set<AssetGUID>()): AssetGUID[] {
if (visited.has(guid)) return [];
visited.add(guid);
const result: AssetGUID[] = [];
const directDeps = this.getDependencies(guid);
for (const dep of directDeps) {
result.push(dep);
const transitiveDeps = this.getAllDependencies(dep, visited);
result.push(...transitiveDeps);
}
return result;
}
/**
* Check for circular dependencies
* 检查循环依赖
*/
hasCircularDependency(guid: AssetGUID): boolean {
const visited = new Set<AssetGUID>();
const recursionStack = new Set<AssetGUID>();
const checkCycle = (current: AssetGUID): boolean => {
visited.add(current);
recursionStack.add(current);
const deps = this.getDependencies(current);
for (const dep of deps) {
if (!visited.has(dep)) {
if (checkCycle(dep)) return true;
} else if (recursionStack.has(dep)) {
return true;
}
}
recursionStack.delete(current);
return false;
};
return checkCycle(guid);
}
/**
* Update dependencies
* 更新依赖关系
*/
private updateDependencies(guid: AssetGUID, newDependencies: AssetGUID[]): void {
// 清除旧的依赖关系 / Clear old dependencies
this.clearDependencies(guid);
// 建立新的依赖关系 / Establish new dependencies
if (newDependencies.length > 0) {
this._dependencies.set(guid, new Set(newDependencies));
// 更新被依赖关系 / Update dependent relations
newDependencies.forEach((dep) => {
if (!this._dependents.has(dep)) {
this._dependents.set(dep, new Set());
}
this._dependents.get(dep)!.add(guid);
});
}
}
/**
* Clear dependencies
* 清除依赖关系
*/
private clearDependencies(guid: AssetGUID): void {
// 清除依赖 / Clear dependencies
const deps = this._dependencies.get(guid);
if (deps) {
deps.forEach((dep) => {
const dependents = this._dependents.get(dep);
if (dependents) {
dependents.delete(guid);
if (dependents.size === 0) {
this._dependents.delete(dep);
}
}
});
this._dependencies.delete(guid);
}
// 清除被依赖 / Clear dependents
const dependents = this._dependents.get(guid);
if (dependents) {
dependents.forEach((dependent) => {
const dependencies = this._dependencies.get(dependent);
if (dependencies) {
dependencies.delete(guid);
if (dependencies.size === 0) {
this._dependencies.delete(dependent);
}
}
});
this._dependents.delete(guid);
}
}
/**
* Get database statistics
* 获取数据库统计
*/
getStatistics(): {
totalAssets: number;
assetsByType: Map<AssetType, number>;
totalDependencies: number;
assetsWithDependencies: number;
circularDependencies: number;
} {
const assetsByType = new Map<AssetType, number>();
this._typeToGuids.forEach((guids, type) => {
assetsByType.set(type, guids.size);
});
let circularDependencies = 0;
this._metadata.forEach((_, guid) => {
if (this.hasCircularDependency(guid)) {
circularDependencies++;
}
});
return {
totalAssets: this._metadata.size,
assetsByType,
totalDependencies: Array.from(this._dependencies.values()).reduce(
(sum, deps) => sum + deps.size,
0
),
assetsWithDependencies: this._dependencies.size,
circularDependencies
};
}
/**
* Export to catalog entries
* 导出为目录条目
*/
exportToCatalog(): IAssetCatalogEntry[] {
const entries: IAssetCatalogEntry[] = [];
this._metadata.forEach((metadata) => {
entries.push({
guid: metadata.guid,
path: metadata.path,
type: metadata.type,
size: metadata.size,
hash: metadata.hash
});
});
return entries;
}
/**
* Clear database
* 清空数据库
*/
clear(): void {
this._metadata.clear();
this._pathToGuid.clear();
this._typeToGuids.clear();
this._labelToGuids.clear();
this._dependencies.clear();
this._dependents.clear();
}
}

View File

@@ -0,0 +1,193 @@
/**
* Priority-based asset loading queue
* 基于优先级的资产加载队列
*/
import { AssetGUID, IAssetLoadOptions } from '../types/AssetTypes';
import { IAssetLoadQueue } from '../interfaces/IAssetManager';
/**
* Queue item
* 队列项
*/
interface QueueItem {
guid: AssetGUID;
priority: number;
options?: IAssetLoadOptions;
timestamp: number;
}
/**
* Asset load queue implementation
* 资产加载队列实现
*/
export class AssetLoadQueue implements IAssetLoadQueue {
private readonly _queue: QueueItem[] = [];
private readonly _guidToIndex = new Map<AssetGUID, number>();
/**
* Add to queue
* 添加到队列
*/
enqueue(guid: AssetGUID, priority: number, options?: IAssetLoadOptions): void {
// 检查是否已在队列中 / Check if already in queue
if (this._guidToIndex.has(guid)) {
this.reprioritize(guid, priority);
return;
}
const item: QueueItem = {
guid,
priority,
options,
timestamp: Date.now()
};
// 二分查找插入位置 / Binary search for insertion position
const index = this.findInsertIndex(priority);
this._queue.splice(index, 0, item);
// 更新索引映射 / Update index mapping
this.updateIndices(index);
}
/**
* Remove from queue
* 从队列移除
*/
dequeue(): { guid: AssetGUID; options?: IAssetLoadOptions } | null {
if (this._queue.length === 0) return null;
const item = this._queue.shift();
if (!item) return null;
// 更新索引映射 / Update index mapping
this._guidToIndex.delete(item.guid);
this.updateIndices(0);
return {
guid: item.guid,
options: item.options
};
}
/**
* Check if queue is empty
* 检查队列是否为空
*/
isEmpty(): boolean {
return this._queue.length === 0;
}
/**
* Get queue size
* 获取队列大小
*/
getSize(): number {
return this._queue.length;
}
/**
* Clear queue
* 清空队列
*/
clear(): void {
this._queue.length = 0;
this._guidToIndex.clear();
}
/**
* Reprioritize item
* 重新设置优先级
*/
reprioritize(guid: AssetGUID, newPriority: number): void {
const index = this._guidToIndex.get(guid);
if (index === undefined) return;
const item = this._queue[index];
if (!item || item.priority === newPriority) return;
// 移除旧项 / Remove old item
this._queue.splice(index, 1);
this._guidToIndex.delete(guid);
// 重新插入 / Reinsert with new priority
item.priority = newPriority;
const newIndex = this.findInsertIndex(newPriority);
this._queue.splice(newIndex, 0, item);
// 更新索引 / Update indices
this.updateIndices(Math.min(index, newIndex));
}
/**
* Find insertion index for priority
* 查找优先级的插入索引
*/
private findInsertIndex(priority: number): number {
let left = 0;
let right = this._queue.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
// 高优先级在前 / Higher priority first
if (this._queue[mid].priority >= priority) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
/**
* Update indices after modification
* 修改后更新索引
*/
private updateIndices(startIndex: number): void {
for (let i = startIndex; i < this._queue.length; i++) {
this._guidToIndex.set(this._queue[i].guid, i);
}
}
/**
* Get queue items (for debugging)
* 获取队列项(用于调试)
*/
getItems(): ReadonlyArray<{
guid: AssetGUID;
priority: number;
waitTime: number;
}> {
const now = Date.now();
return this._queue.map((item) => ({
guid: item.guid,
priority: item.priority,
waitTime: now - item.timestamp
}));
}
/**
* Remove specific item from queue
* 从队列中移除特定项
*/
remove(guid: AssetGUID): boolean {
const index = this._guidToIndex.get(guid);
if (index === undefined) return false;
this._queue.splice(index, 1);
this._guidToIndex.delete(guid);
this.updateIndices(index);
return true;
}
/**
* Check if guid is in queue
* 检查guid是否在队列中
*/
contains(guid: AssetGUID): boolean {
return this._guidToIndex.has(guid);
}
}

View File

@@ -0,0 +1,684 @@
/**
* Asset manager implementation
* 资产管理器实现
*/
import {
AssetGUID,
AssetHandle,
AssetType,
AssetState,
IAssetLoadOptions,
IAssetLoadResult,
IAssetReferenceInfo,
IAssetPreloadGroup,
IAssetLoadProgress,
IAssetMetadata,
AssetLoadError,
IAssetCatalog
} from '../types/AssetTypes';
import {
IAssetManager,
IAssetLoadQueue
} from '../interfaces/IAssetManager';
import { IAssetLoader, IAssetLoaderFactory, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetReader, IAssetContent } from '../interfaces/IAssetReader';
import { AssetCache } from './AssetCache';
import { AssetLoadQueue } from './AssetLoadQueue';
import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory';
import { AssetDatabase } from './AssetDatabase';
/**
* Asset entry in the manager
* 管理器中的资产条目
*/
interface AssetEntry {
guid: AssetGUID;
handle: AssetHandle;
asset: unknown;
metadata: IAssetMetadata;
state: AssetState;
referenceCount: number;
lastAccessTime: number;
loadPromise?: Promise<IAssetLoadResult>;
}
/**
* Asset manager implementation
* 资产管理器实现
*/
export class AssetManager implements IAssetManager {
private readonly _assets = new Map<AssetGUID, AssetEntry>();
private readonly _handleToGuid = new Map<AssetHandle, AssetGUID>();
private readonly _pathToGuid = new Map<string, AssetGUID>();
private readonly _cache: AssetCache;
private readonly _loadQueue: IAssetLoadQueue;
private readonly _loaderFactory: IAssetLoaderFactory;
private readonly _database: AssetDatabase;
/** Asset reader for file operations. | 用于文件操作的资产读取器。 */
private _reader: IAssetReader | null = null;
private _nextHandle: AssetHandle = 1;
private _statistics = {
loadedCount: 0,
failedCount: 0
};
private _isDisposed = false;
private _loadingCount = 0;
constructor(catalog?: IAssetCatalog) {
this._cache = new AssetCache();
this._loadQueue = new AssetLoadQueue();
this._loaderFactory = new AssetLoaderFactory();
this._database = new AssetDatabase();
if (catalog) {
this.initializeFromCatalog(catalog);
}
}
/**
* Set asset reader.
* 设置资产读取器。
*/
setReader(reader: IAssetReader): void {
this._reader = reader;
}
/**
* Set project root path for resolving relative paths.
* 设置项目根路径用于解析相对路径。
*/
setProjectRoot(path: string): void {
this._database.setProjectRoot(path);
}
/**
* Get the asset database.
* 获取资产数据库。
*/
getDatabase(): AssetDatabase {
return this._database;
}
/**
* Initialize from catalog
* 从目录初始化
*/
private initializeFromCatalog(catalog: IAssetCatalog): void {
catalog.entries.forEach((entry, guid) => {
const metadata: IAssetMetadata = {
guid,
path: entry.path,
type: entry.type,
name: entry.path.split('/').pop() || '',
size: entry.size,
hash: entry.hash,
dependencies: [],
labels: [],
tags: new Map(),
lastModified: Date.now(),
version: 1
};
this._database.addAsset(metadata);
this._pathToGuid.set(entry.path, guid);
});
}
/**
* Load asset by GUID
* 通过GUID加载资产
*/
async loadAsset<T = unknown>(
guid: AssetGUID,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<T>> {
// 检查是否已加载 / Check if already loaded
const entry = this._assets.get(guid);
if (entry) {
if (entry.state === AssetState.Loaded && !options?.forceReload) {
entry.lastAccessTime = Date.now();
return {
asset: entry.asset as T,
handle: entry.handle,
metadata: entry.metadata,
loadTime: 0
};
}
if (entry.state === AssetState.Loading && entry.loadPromise) {
return entry.loadPromise as Promise<IAssetLoadResult<T>>;
}
}
// 获取元数据 / Get metadata
const metadata = this._database.getMetadata(guid);
if (!metadata) {
throw AssetLoadError.fileNotFound(guid, 'Unknown');
}
// 创建加载器 / Create loader
let loader = this._loaderFactory.createLoader(metadata.type);
// 如果没有找到 loader 且类型是 Custom尝试重新解析类型
// If no loader found and type is Custom, try to re-resolve the type
if (!loader && metadata.type === AssetType.Custom) {
const newType = this.resolveAssetType(metadata.path);
if (newType !== AssetType.Custom) {
// 更新 metadata 类型 / Update metadata type
this._database.updateAsset(guid, { type: newType });
loader = this._loaderFactory.createLoader(newType);
}
}
if (!loader) {
throw AssetLoadError.unsupportedType(guid, metadata.type);
}
// 开始加载 / Start loading
const loadStartTime = performance.now();
const newEntry: AssetEntry = {
guid,
handle: this._nextHandle++,
asset: null,
metadata,
state: AssetState.Loading,
referenceCount: 0,
lastAccessTime: Date.now()
};
this._assets.set(guid, newEntry);
this._handleToGuid.set(newEntry.handle, guid);
this._loadingCount++;
// 创建加载Promise / Create loading promise
const loadPromise = this.performLoad<T>(loader, metadata, options, loadStartTime, newEntry);
newEntry.loadPromise = loadPromise;
try {
const result = await loadPromise;
return result;
} catch (error) {
this._statistics.failedCount++;
newEntry.state = AssetState.Failed;
throw error;
} finally {
this._loadingCount--;
delete newEntry.loadPromise;
}
}
/**
* Perform asset loading
* 执行资产加载
*/
private async performLoad<T>(
loader: IAssetLoader,
metadata: IAssetMetadata,
options: IAssetLoadOptions | undefined,
startTime: number,
entry: AssetEntry
): Promise<IAssetLoadResult<T>> {
if (!this._reader) {
throw new Error('Asset reader not set. Call setReader() first.');
}
// Load dependencies first.
// 先加载依赖。
if (metadata.dependencies.length > 0) {
await this.loadDependencies(metadata.dependencies, options);
}
// Resolve absolute path.
// 解析绝对路径。
const absolutePath = this._database.resolveAbsolutePath(metadata.path);
// Read content based on loader's content type.
// 根据加载器的内容类型读取内容。
const content = await this.readContent(loader.contentType, absolutePath);
// Create parse context.
// 创建解析上下文。
const context: IAssetParseContext = {
metadata,
options,
loadDependency: async <D>(relativePath: string) => {
const result = await this.loadAssetByPath<D>(relativePath, options);
return result.asset;
}
};
// Parse asset.
// 解析资产。
const asset = await loader.parse(content, context);
// Update entry.
// 更新条目。
entry.asset = asset;
entry.state = AssetState.Loaded;
// Cache asset.
// 缓存资产。
this._cache.set(metadata.guid, asset);
// Update statistics.
// 更新统计。
this._statistics.loadedCount++;
return {
asset: asset as T,
handle: entry.handle,
metadata,
loadTime: performance.now() - startTime
};
}
/**
* Read content based on content type.
* 根据内容类型读取内容。
*/
private async readContent(contentType: string, absolutePath: string): Promise<IAssetContent> {
if (!this._reader) {
throw new Error('Asset reader not set');
}
switch (contentType) {
case 'text': {
const text = await this._reader.readText(absolutePath);
return { type: 'text', text };
}
case 'binary': {
const binary = await this._reader.readBinary(absolutePath);
return { type: 'binary', binary };
}
case 'image': {
const image = await this._reader.loadImage(absolutePath);
return { type: 'image', image };
}
case 'audio': {
const audioBuffer = await this._reader.loadAudio(absolutePath);
return { type: 'audio', audioBuffer };
}
default:
throw new Error(`Unknown content type: ${contentType}`);
}
}
/**
* Load dependencies
* 加载依赖
*/
private async loadDependencies(
dependencies: AssetGUID[],
options?: IAssetLoadOptions
): Promise<void> {
const promises = dependencies.map((dep) => this.loadAsset(dep, options));
await Promise.all(promises);
}
/**
* Load asset by path
* 通过路径加载资产
*/
async loadAssetByPath<T = unknown>(
path: string,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<T>> {
const guid = this._pathToGuid.get(path);
if (!guid) {
// 尝试从数据库查找 / Try to find from database
let metadata = this._database.getMetadataByPath(path);
if (!metadata) {
// 动态创建元数据 / Create metadata dynamically
const assetType = this.resolveAssetType(path);
// 生成唯一GUID / Generate unique GUID
const dynamicGuid = `dynamic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
metadata = {
guid: dynamicGuid,
path: path,
type: assetType,
name: path.split('/').pop() || path.split('\\').pop() || 'unnamed',
size: 0, // 动态加载时未知大小 / Unknown size for dynamic loading
hash: '',
dependencies: [],
labels: [],
tags: new Map(),
lastModified: Date.now(),
version: 1
};
// 注册到数据库 / Register to database
this._database.addAsset(metadata);
this._pathToGuid.set(path, metadata.guid);
} else {
// 如果之前缓存的类型是 Custom检查是否现在有注册的 loader 可以处理
// If previously cached as Custom, check if a registered loader can now handle it
if (metadata.type === AssetType.Custom) {
const newType = this.resolveAssetType(path);
if (newType !== AssetType.Custom) {
metadata.type = newType;
}
}
this._pathToGuid.set(path, metadata.guid);
}
return this.loadAsset<T>(metadata.guid, options);
}
// 同样检查已缓存的资产,如果类型是 Custom 但现在有 loader 可以处理
// Also check cached assets, if type is Custom but now a loader can handle it
const entry = this._assets.get(guid);
if (entry && entry.metadata.type === AssetType.Custom) {
const newType = this.resolveAssetType(path);
if (newType !== AssetType.Custom) {
entry.metadata.type = newType;
}
}
return this.loadAsset<T>(guid, options);
}
/**
* Resolve asset type from path
* 从路径解析资产类型
*/
private resolveAssetType(path: string): AssetType {
// 首先尝试从已注册的加载器获取资产类型 / First try to get asset type from registered loaders
const loaderType = (this._loaderFactory as AssetLoaderFactory).getAssetTypeByPath(path);
if (loaderType !== null) {
return loaderType;
}
// 如果没有找到匹配的加载器,使用默认的扩展名映射 / Fallback to default extension mapping
const fileExt = path.substring(path.lastIndexOf('.')).toLowerCase();
// 默认支持的基础类型 / Default supported basic types
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(fileExt)) {
return AssetType.Texture;
} else if (['.json'].includes(fileExt)) {
return AssetType.Json;
} else if (['.txt', '.md', '.xml', '.yaml'].includes(fileExt)) {
return AssetType.Text;
}
return AssetType.Custom;
}
/**
* Load multiple assets
* 批量加载资产
*/
async loadAssets(
guids: AssetGUID[],
options?: IAssetLoadOptions
): Promise<Map<AssetGUID, IAssetLoadResult>> {
const results = new Map<AssetGUID, IAssetLoadResult>();
// 并行加载所有资产 / Load all assets in parallel
const promises = guids.map(async (guid) => {
try {
const result = await this.loadAsset(guid, options);
results.set(guid, result);
} catch (error) {
console.error(`Failed to load asset ${guid}:`, error);
}
});
await Promise.all(promises);
return results;
}
/**
* Preload asset group
* 预加载资产组
*/
async preloadGroup(
group: IAssetPreloadGroup,
onProgress?: (progress: IAssetLoadProgress) => void
): Promise<void> {
const totalCount = group.assets.length;
let loadedCount = 0;
let loadedBytes = 0;
let totalBytes = 0;
// 计算总大小 / Calculate total size
for (const guid of group.assets) {
const metadata = this._database.getMetadata(guid);
if (metadata) {
totalBytes += metadata.size;
}
}
// 加载每个资产 / Load each asset
for (const guid of group.assets) {
const metadata = this._database.getMetadata(guid);
if (!metadata) continue;
if (onProgress) {
onProgress({
currentAsset: metadata.name,
loadedCount,
totalCount,
loadedBytes,
totalBytes,
progress: loadedCount / totalCount
});
}
await this.loadAsset(guid, { priority: group.priority });
loadedCount++;
loadedBytes += metadata.size;
}
// 最终进度 / Final progress
if (onProgress) {
onProgress({
currentAsset: '',
loadedCount: totalCount,
totalCount,
loadedBytes: totalBytes,
totalBytes,
progress: 1
});
}
}
/**
* Get loaded asset
* 获取已加载的资产
*/
getAsset<T = unknown>(guid: AssetGUID): T | null {
const entry = this._assets.get(guid);
if (entry && entry.state === AssetState.Loaded) {
entry.lastAccessTime = Date.now();
return entry.asset as T;
}
return null;
}
/**
* Get asset by handle
* 通过句柄获取资产
*/
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null {
const guid = this._handleToGuid.get(handle);
if (!guid) return null;
return this.getAsset<T>(guid);
}
/**
* Get loaded asset by path (synchronous)
* 通过路径获取已加载的资产(同步)
*
* Returns the asset if it's already loaded, null otherwise.
* 如果资产已加载则返回资产,否则返回 null。
*/
getAssetByPath<T = unknown>(path: string): T | null {
const guid = this._pathToGuid.get(path);
if (!guid) return null;
return this.getAsset<T>(guid);
}
/**
* Check if asset is loaded
* 检查资产是否已加载
*/
isLoaded(guid: AssetGUID): boolean {
const entry = this._assets.get(guid);
return entry?.state === AssetState.Loaded;
}
/**
* Get asset state
* 获取资产状态
*/
getAssetState(guid: AssetGUID): AssetState {
const entry = this._assets.get(guid);
return entry?.state || AssetState.Unloaded;
}
/**
* Unload asset
* 卸载资产
*/
unloadAsset(guid: AssetGUID): void {
const entry = this._assets.get(guid);
if (!entry) return;
// 检查引用计数 / Check reference count
if (entry.referenceCount > 0) {
return;
}
// 获取加载器以释放资源 / Get loader to dispose resources
const loader = this._loaderFactory.createLoader(entry.metadata.type);
if (loader) {
loader.dispose(entry.asset);
}
// 清理条目 / Clean up entry
this._handleToGuid.delete(entry.handle);
this._assets.delete(guid);
this._cache.remove(guid);
// 更新统计 / Update statistics
this._statistics.loadedCount--;
entry.state = AssetState.Unloaded;
}
/**
* Unload all assets
* 卸载所有资产
*/
unloadAllAssets(): void {
const guids = Array.from(this._assets.keys());
guids.forEach((guid) => this.unloadAsset(guid));
}
/**
* Unload unused assets
* 卸载未使用的资产
*/
unloadUnusedAssets(): void {
const guids = Array.from(this._assets.keys());
guids.forEach((guid) => {
const entry = this._assets.get(guid);
if (entry && entry.referenceCount === 0) {
this.unloadAsset(guid);
}
});
}
/**
* Add reference to asset
* 增加资产引用
*/
addReference(guid: AssetGUID): void {
const entry = this._assets.get(guid);
if (entry) {
entry.referenceCount++;
}
}
/**
* Remove reference from asset
* 移除资产引用
*/
removeReference(guid: AssetGUID): void {
const entry = this._assets.get(guid);
if (entry && entry.referenceCount > 0) {
entry.referenceCount--;
}
}
/**
* Get reference info
* 获取引用信息
*/
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null {
const entry = this._assets.get(guid);
if (!entry) return null;
return {
guid,
handle: entry.handle,
referenceCount: entry.referenceCount,
lastAccessTime: entry.lastAccessTime,
state: entry.state
};
}
/**
* Register custom loader
* 注册自定义加载器
*/
registerLoader(type: AssetType, loader: IAssetLoader): void {
this._loaderFactory.registerLoader(type, loader);
}
/**
* Get asset statistics
* 获取资产统计信息
*/
getStatistics(): { loadedCount: number; loadQueue: number; failedCount: number } {
return {
loadedCount: this._statistics.loadedCount,
loadQueue: this._loadQueue.getSize(),
failedCount: this._statistics.failedCount
};
}
/**
* Clear cache
* 清空缓存
*/
clearCache(): void {
this._cache.clear();
}
/**
* Dispose manager
* 释放管理器
*/
dispose(): void {
if (this._isDisposed) return;
this.unloadAllAssets();
this._cache.clear();
this._loadQueue.clear();
this._assets.clear();
this._handleToGuid.clear();
this._pathToGuid.clear();
this._isDisposed = true;
}
}

View File

@@ -0,0 +1,241 @@
/**
* Asset path resolver for different platforms and protocols
* 不同平台和协议的资产路径解析器
*/
import { AssetPlatform } from '../types/AssetTypes';
import { PathValidator } from '../utils/PathValidator';
/**
* Asset path resolver configuration
* 资产路径解析器配置
*/
export interface IAssetPathConfig {
/** Base URL for web assets | Web资产的基础URL */
baseUrl?: string;
/** Asset directory path | 资产目录路径 */
assetDir?: string;
/** Asset host for asset:// protocol | 资产协议的主机名 */
assetHost?: string;
/** Current platform | 当前平台 */
platform?: AssetPlatform;
/** Custom path transformer | 自定义路径转换器 */
pathTransformer?: (path: string) => string;
}
/**
* Asset path resolver
* 资产路径解析器
*/
export class AssetPathResolver {
private config: IAssetPathConfig;
constructor(config: IAssetPathConfig = {}) {
this.config = {
baseUrl: '',
assetDir: 'assets',
platform: AssetPlatform.H5,
...config
};
}
/**
* Update configuration
* 更新配置
*/
updateConfig(config: Partial<IAssetPathConfig>): void {
this.config = { ...this.config, ...config };
}
/**
* Resolve asset path to full URL
* 解析资产路径为完整URL
*/
resolve(path: string): string {
// Validate input path
const validation = PathValidator.validate(path);
if (!validation.valid) {
console.warn(`Invalid asset path: ${path} - ${validation.reason}`);
// Sanitize the path instead of throwing
path = PathValidator.sanitize(path);
if (!path) {
throw new Error(`Cannot resolve invalid path: ${validation.reason}`);
}
}
// Already a full URL
// 已经是完整URL
if (this.isAbsoluteUrl(path)) {
return path;
}
// Data URL
// 数据URL
if (path.startsWith('data:')) {
return path;
}
// Normalize the path
path = PathValidator.normalize(path);
// Apply custom transformer if provided
// 应用自定义转换器(如果提供)
if (this.config.pathTransformer) {
path = this.config.pathTransformer(path);
// Transformer output is trusted (may be absolute path or asset:// URL)
// 转换器输出是可信的(可能是绝对路径或 asset:// URL
return path;
}
// Platform-specific resolution
// 平台特定解析
switch (this.config.platform) {
case AssetPlatform.H5:
return this.resolveH5Path(path);
case AssetPlatform.WeChat:
return this.resolveWeChatPath(path);
case AssetPlatform.Playable:
return this.resolvePlayablePath(path);
case AssetPlatform.Android:
case AssetPlatform.iOS:
return this.resolveMobilePath(path);
case AssetPlatform.Editor:
return this.resolveEditorPath(path);
default:
return this.resolveH5Path(path);
}
}
/**
* Resolve path for H5 platform
* 解析H5平台路径
*/
private resolveH5Path(path: string): string {
// Remove leading slash if present
// 移除开头的斜杠(如果存在)
path = path.replace(/^\//, '');
// Combine with base URL and asset directory
// 与基础URL和资产目录结合
const base = this.config.baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
const assetDir = this.config.assetDir || 'assets';
return `${base}/${assetDir}/${path}`.replace(/\/+/g, '/');
}
/**
* Resolve path for WeChat Mini Game
* 解析微信小游戏路径
*/
private resolveWeChatPath(path: string): string {
// WeChat mini games use relative paths
// 微信小游戏使用相对路径
return `${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
}
/**
* Resolve path for Playable Ads platform
* 解析试玩广告平台路径
*/
private resolvePlayablePath(path: string): string {
// Playable ads typically use base64 embedded resources or relative paths
// 试玩广告通常使用base64内嵌资源或相对路径
// If custom transformer is provided (e.g., for base64 encoding)
// 如果提供了自定义转换器例如用于base64编码
if (this.config.pathTransformer) {
return this.config.pathTransformer(path);
}
// Default to relative path without directory prefix
// 默认使用不带目录前缀的相对路径
return path;
}
/**
* Resolve path for mobile platform (Android/iOS)
* 解析移动平台路径Android/iOS
*/
private resolveMobilePath(path: string): string {
// Mobile platforms use relative paths or file:// protocol
// 移动平台使用相对路径或file://协议
return `./${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
}
/**
* Resolve path for Editor platform (Tauri)
* 解析编辑器平台路径Tauri
*/
private resolveEditorPath(path: string): string {
// For Tauri editor, use pathTransformer if provided
// 对于Tauri编辑器使用pathTransformer如果提供
if (this.config.pathTransformer) {
return this.config.pathTransformer(path);
}
// Use configurable asset host or default to 'localhost'
// 使用可配置的资产主机或默认为 'localhost'
const host = this.config.assetHost || 'localhost';
const sanitizedPath = PathValidator.sanitize(path);
return `asset://${host}/${sanitizedPath}`;
}
/**
* Check if path is absolute URL
* 检查路径是否为绝对URL
*/
private isAbsoluteUrl(path: string): boolean {
return /^(https?:\/\/|file:\/\/|asset:\/\/)/.test(path);
}
/**
* Get asset directory from path
* 从路径获取资产目录
*/
getAssetDirectory(path: string): string {
const resolved = this.resolve(path);
const lastSlash = resolved.lastIndexOf('/');
return lastSlash >= 0 ? resolved.substring(0, lastSlash) : '';
}
/**
* Get asset filename from path
* 从路径获取资产文件名
*/
getAssetFilename(path: string): string {
const resolved = this.resolve(path);
const lastSlash = resolved.lastIndexOf('/');
return lastSlash >= 0 ? resolved.substring(lastSlash + 1) : resolved;
}
/**
* Join paths
* 连接路径
*/
join(...paths: string[]): string {
return paths.join('/').replace(/\/+/g, '/');
}
/**
* Normalize path
* 规范化路径
*/
normalize(path: string): string {
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
}
}
/**
* Global asset path resolver instance
* 全局资产路径解析器实例
*/
export const globalPathResolver = new AssetPathResolver();

View File

@@ -0,0 +1,338 @@
/**
* Asset reference for lazy loading
* 用于懒加载的资产引用
*/
import { AssetGUID, IAssetLoadOptions, AssetState } from '../types/AssetTypes';
import { IAssetManager } from '../interfaces/IAssetManager';
/**
* Asset reference class for lazy loading
* 懒加载资产引用类
*/
export class AssetReference<T = unknown> {
private _guid: AssetGUID;
private _asset?: T;
private _loadPromise?: Promise<T>;
private _manager?: IAssetManager;
private _isReleased = false;
private _autoRelease = false;
/**
* Constructor
* 构造函数
*/
constructor(guid: AssetGUID, manager?: IAssetManager) {
this._guid = guid;
this._manager = manager;
}
/**
* Get asset GUID
* 获取资产GUID
*/
get guid(): AssetGUID {
return this._guid;
}
/**
* Check if asset is loaded
* 检查资产是否已加载
*/
get isLoaded(): boolean {
return this._asset !== undefined && !this._isReleased;
}
/**
* Get asset synchronously (returns null if not loaded)
* 同步获取资产如果未加载则返回null
*/
get asset(): T | null {
return this._asset ?? null;
}
/**
* Set asset manager
* 设置资产管理器
*/
setManager(manager: IAssetManager): void {
this._manager = manager;
}
/**
* Load asset asynchronously
* 异步加载资产
*/
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
if (this._isReleased) {
throw new Error(`Asset reference ${this._guid} has been released`);
}
// 如果已经加载,直接返回 / Return if already loaded
if (this._asset !== undefined) {
return this._asset;
}
// 如果正在加载返回加载Promise / Return loading promise if loading
if (this._loadPromise) {
return this._loadPromise;
}
if (!this._manager) {
throw new Error('Asset manager not set for AssetReference');
}
// 开始加载 / Start loading
this._loadPromise = this.performLoad(options);
try {
const asset = await this._loadPromise;
return asset;
} finally {
this._loadPromise = undefined;
}
}
/**
* Perform asset loading
* 执行资产加载
*/
private async performLoad(options?: IAssetLoadOptions): Promise<T> {
if (!this._manager) {
throw new Error('Asset manager not set');
}
const result = await this._manager.loadAsset<T>(this._guid, options);
this._asset = result.asset;
// 增加引用计数 / Increase reference count
this._manager.addReference(this._guid);
return this._asset;
}
/**
* Release asset reference
* 释放资产引用
*/
release(): void {
if (this._isReleased) return;
if (this._manager && this._asset !== undefined) {
// 减少引用计数 / Decrease reference count
this._manager.removeReference(this._guid);
// 如果引用计数为0可以考虑卸载 / Consider unloading if reference count is 0
const refInfo = this._manager.getReferenceInfo(this._guid);
if (refInfo && refInfo.referenceCount === 0 && this._autoRelease) {
this._manager.unloadAsset(this._guid);
}
}
this._asset = undefined;
this._isReleased = true;
}
/**
* Set auto-release mode
* 设置自动释放模式
*/
setAutoRelease(autoRelease: boolean): void {
this._autoRelease = autoRelease;
}
/**
* Validate reference
* 验证引用
*/
validate(): boolean {
if (!this._manager) return false;
const state = this._manager.getAssetState(this._guid);
return state !== AssetState.Failed;
}
/**
* Get asset state
* 获取资产状态
*/
getState(): AssetState {
if (this._isReleased) return AssetState.Unloaded;
if (!this._manager) return AssetState.Unloaded;
return this._manager.getAssetState(this._guid);
}
/**
* Clone reference
* 克隆引用
*/
clone(): AssetReference<T> {
const newRef = new AssetReference<T>(this._guid, this._manager);
newRef.setAutoRelease(this._autoRelease);
return newRef;
}
/**
* Convert to JSON
* 转换为JSON
*/
toJSON(): { guid: AssetGUID } {
return { guid: this._guid };
}
/**
* Create from JSON
* 从JSON创建
*/
static fromJSON<T = unknown>(
json: { guid: AssetGUID },
manager?: IAssetManager
): AssetReference<T> {
return new AssetReference<T>(json.guid, manager);
}
}
/**
* Weak asset reference that doesn't prevent unloading
* 不阻止卸载的弱资产引用
*/
export class WeakAssetReference<T = unknown> {
private _guid: AssetGUID;
private _manager?: IAssetManager;
constructor(guid: AssetGUID, manager?: IAssetManager) {
this._guid = guid;
this._manager = manager;
}
/**
* Get asset GUID
* 获取资产GUID
*/
get guid(): AssetGUID {
return this._guid;
}
/**
* Try get asset without loading
* 尝试获取资产而不加载
*/
tryGet(): T | null {
if (!this._manager) return null;
return this._manager.getAsset<T>(this._guid);
}
/**
* Load asset if not loaded
* 如果未加载则加载资产
*/
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
if (!this._manager) {
throw new Error('Asset manager not set');
}
const result = await this._manager.loadAsset<T>(this._guid, options);
// 不增加引用计数 / Don't increase reference count for weak reference
return result.asset;
}
/**
* Check if asset is loaded
* 检查资产是否已加载
*/
isLoaded(): boolean {
if (!this._manager) return false;
return this._manager.isLoaded(this._guid);
}
/**
* Set asset manager
* 设置资产管理器
*/
setManager(manager: IAssetManager): void {
this._manager = manager;
}
}
/**
* Asset reference array for managing multiple references
* 用于管理多个引用的资产引用数组
*/
export class AssetReferenceArray<T = unknown> {
private _references: AssetReference<T>[] = [];
private _manager?: IAssetManager;
constructor(guids: AssetGUID[] = [], manager?: IAssetManager) {
this._manager = manager;
this._references = guids.map((guid) => new AssetReference<T>(guid, manager));
}
/**
* Add reference
* 添加引用
*/
add(guid: AssetGUID): void {
this._references.push(new AssetReference<T>(guid, this._manager));
}
/**
* Remove reference
* 移除引用
*/
remove(guid: AssetGUID): boolean {
const index = this._references.findIndex((ref) => ref.guid === guid);
if (index >= 0) {
this._references[index].release();
this._references.splice(index, 1);
return true;
}
return false;
}
/**
* Load all assets
* 加载所有资产
*/
async loadAllAsync(options?: IAssetLoadOptions): Promise<T[]> {
const promises = this._references.map((ref) => ref.loadAsync(options));
return Promise.all(promises);
}
/**
* Release all references
* 释放所有引用
*/
releaseAll(): void {
this._references.forEach((ref) => ref.release());
this._references = [];
}
/**
* Get all loaded assets
* 获取所有已加载的资产
*/
getLoadedAssets(): T[] {
return this._references
.filter((ref) => ref.isLoaded)
.map((ref) => ref.asset!)
.filter((asset) => asset !== null);
}
/**
* Get reference count
* 获取引用数量
*/
get count(): number {
return this._references.length;
}
/**
* Set asset manager
* 设置资产管理器
*/
setManager(manager: IAssetManager): void {
this._manager = manager;
this._references.forEach((ref) => ref.setManager(manager));
}
}

View File

@@ -0,0 +1,76 @@
/**
* Asset System for ECS Framework
* ECS框架的资产系统
*
* Runtime-focused asset management:
* - Asset loading and caching
* - GUID-based asset resolution
* - Bundle loading
*
* For editor-side functionality (meta files, packing), use @esengine/asset-system-editor
*/
// Types
export * from './types/AssetTypes';
// Bundle format (shared types for runtime and editor)
export * from './bundle/BundleFormat';
// Runtime catalog
export { RuntimeCatalog, runtimeCatalog } from './runtime/RuntimeCatalog';
// Interfaces
export * from './interfaces/IAssetLoader';
export * from './interfaces/IAssetManager';
export * from './interfaces/IAssetReader';
export * from './interfaces/IResourceComponent';
// Core
export { AssetManager } from './core/AssetManager';
export { AssetCache } from './core/AssetCache';
export { AssetDatabase } from './core/AssetDatabase';
export { AssetLoadQueue } from './core/AssetLoadQueue';
export { AssetReference, WeakAssetReference, AssetReferenceArray } from './core/AssetReference';
export { AssetPathResolver, globalPathResolver } from './core/AssetPathResolver';
export type { IAssetPathConfig } from './core/AssetPathResolver';
// Loaders
export { AssetLoaderFactory } from './loaders/AssetLoaderFactory';
export { TextureLoader } from './loaders/TextureLoader';
export { JsonLoader } from './loaders/JsonLoader';
export { TextLoader } from './loaders/TextLoader';
export { BinaryLoader } from './loaders/BinaryLoader';
// Integration
export { EngineIntegration } from './integration/EngineIntegration';
export type { IEngineBridge } from './integration/EngineIntegration';
// Services
export { SceneResourceManager } from './services/SceneResourceManager';
export type { IResourceLoader } from './services/SceneResourceManager';
// Utils
export { UVHelper } from './utils/UVHelper';
// Default instance
import { AssetManager } from './core/AssetManager';
/**
* Default asset manager instance
* 默认资产管理器实例
*/
export const assetManager = new AssetManager();
/**
* Initialize asset system with catalog
* 使用目录初始化资产系统
*/
export function initializeAssetSystem(catalog?: IAssetCatalog): AssetManager {
if (catalog) {
return new AssetManager(catalog);
}
return assetManager;
}
// Re-export IAssetCatalog for initializeAssetSystem signature
import type { IAssetCatalog } from './types/AssetTypes';

View File

@@ -0,0 +1,247 @@
/**
* Engine integration for asset system
* 资产系统的引擎集成
*/
import { AssetManager } from '../core/AssetManager';
import { AssetGUID } from '../types/AssetTypes';
import { ITextureAsset } from '../interfaces/IAssetLoader';
import { globalPathResolver } from '../core/AssetPathResolver';
/**
* Engine bridge interface
* 引擎桥接接口
*/
export interface IEngineBridge {
/**
* Load texture to GPU
* 加载纹理到GPU
*/
loadTexture(id: number, url: string): Promise<void>;
/**
* Load multiple textures
* 批量加载纹理
*/
loadTextures(requests: Array<{ id: number; url: string }>): Promise<void>;
/**
* Unload texture from GPU
* 从GPU卸载纹理
*/
unloadTexture(id: number): void;
/**
* Get texture info
* 获取纹理信息
*/
getTextureInfo(id: number): { width: number; height: number } | null;
}
/**
* Asset system engine integration
* 资产系统引擎集成
*/
export class EngineIntegration {
private _assetManager: AssetManager;
private _engineBridge?: IEngineBridge;
private _textureIdMap = new Map<AssetGUID, number>();
private _pathToTextureId = new Map<string, number>();
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
this._assetManager = assetManager;
this._engineBridge = engineBridge;
}
/**
* Set engine bridge
* 设置引擎桥接
*/
setEngineBridge(bridge: IEngineBridge): void {
this._engineBridge = bridge;
}
/**
* Load texture for component
* 为组件加载纹理
*
* AssetManager 内部会处理路径解析,这里只需传入原始路径。
* AssetManager handles path resolution internally, just pass the original path here.
*/
async loadTextureForComponent(texturePath: string): Promise<number> {
// 检查缓存(使用原始路径作为键)
// Check cache (using original path as key)
const existingId = this._pathToTextureId.get(texturePath);
if (existingId) {
return existingId;
}
// 通过资产系统加载AssetManager 内部会解析路径)
// Load through asset system (AssetManager resolves path internally)
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
const textureAsset = result.asset;
// 如果有引擎桥接上传到GPU
// Upload to GPU if bridge exists
// 使用 globalPathResolver 将路径转换为引擎可用的 URL
// Use globalPathResolver to convert path to engine-compatible URL
if (this._engineBridge && textureAsset.data) {
const engineUrl = globalPathResolver.resolve(texturePath);
await this._engineBridge.loadTexture(textureAsset.textureId, engineUrl);
}
// 缓存映射(使用原始路径作为键,避免重复解析)
// Cache mapping (using original path as key to avoid re-resolving)
this._pathToTextureId.set(texturePath, textureAsset.textureId);
return textureAsset.textureId;
}
/**
* Load texture by GUID
* 通过GUID加载纹理
*/
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
// 检查是否已有纹理ID / Check if texture ID exists
const existingId = this._textureIdMap.get(guid);
if (existingId) {
return existingId;
}
// 通过资产系统加载 / Load through asset system
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
const textureAsset = result.asset;
// 如果有引擎桥接上传到GPU / Upload to GPU if bridge exists
if (this._engineBridge && textureAsset.data) {
const metadata = result.metadata;
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
}
// 缓存映射 / Cache mapping
this._textureIdMap.set(guid, textureAsset.textureId);
return textureAsset.textureId;
}
/**
* Batch load textures
* 批量加载纹理
*/
async loadTexturesBatch(paths: string[]): Promise<Map<string, number>> {
const results = new Map<string, number>();
// 收集需要加载的纹理 / Collect textures to load
const toLoad: string[] = [];
for (const path of paths) {
const existingId = this._pathToTextureId.get(path);
if (existingId) {
results.set(path, existingId);
} else {
toLoad.push(path);
}
}
if (toLoad.length === 0) {
return results;
}
// 并行加载所有纹理 / Load all textures in parallel
const loadPromises = toLoad.map(async (path) => {
try {
const id = await this.loadTextureForComponent(path);
results.set(path, id);
} catch (error) {
console.error(`Failed to load texture: ${path}`, error);
results.set(path, 0); // 使用默认纹理ID / Use default texture ID
}
});
await Promise.all(loadPromises);
return results;
}
/**
* 批量加载资源(通用方法,支持 IResourceLoader 接口)
* Load resources in batch (generic method for IResourceLoader interface)
*
* @param paths 资源路径数组 / Array of resource paths
* @param type 资源类型 / Resource type
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
*/
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
// 目前只支持纹理 / Currently only supports textures
if (type === 'texture') {
return this.loadTexturesBatch(paths);
}
// 其他资源类型暂未实现 / Other resource types not yet implemented
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
return new Map();
}
/**
* Unload texture
* 卸载纹理
*/
unloadTexture(textureId: number): void {
// 从引擎卸载 / Unload from engine
if (this._engineBridge) {
this._engineBridge.unloadTexture(textureId);
}
// 清理映射 / Clean up mappings
for (const [path, id] of this._pathToTextureId.entries()) {
if (id === textureId) {
this._pathToTextureId.delete(path);
break;
}
}
for (const [guid, id] of this._textureIdMap.entries()) {
if (id === textureId) {
this._textureIdMap.delete(guid);
// 也从资产管理器卸载 / Also unload from asset manager
this._assetManager.unloadAsset(guid);
break;
}
}
}
/**
* Get texture ID for path
* 获取路径的纹理ID
*/
getTextureId(path: string): number | null {
return this._pathToTextureId.get(path) || null;
}
/**
* Preload textures for scene
* 为场景预加载纹理
*/
async preloadSceneTextures(texturePaths: string[]): Promise<void> {
await this.loadTexturesBatch(texturePaths);
}
/**
* Clear all texture mappings
* 清空所有纹理映射
*/
clearTextureMappings(): void {
this._textureIdMap.clear();
this._pathToTextureId.clear();
}
/**
* Get statistics
* 获取统计信息
*/
getStatistics(): {
loadedTextures: number;
} {
return {
loadedTextures: this._pathToTextureId.size
};
}
}

View File

@@ -0,0 +1,258 @@
/**
* Asset loader interfaces
* 资产加载器接口
*/
import {
AssetType,
AssetGUID,
IAssetLoadOptions,
IAssetMetadata
} from '../types/AssetTypes';
import type { IAssetContent, AssetContentType } from './IAssetReader';
/**
* Parse context provided to loaders.
* 提供给加载器的解析上下文。
*/
export interface IAssetParseContext {
/** Asset metadata. | 资产元数据。 */
metadata: IAssetMetadata;
/** Load options. | 加载选项。 */
options?: IAssetLoadOptions;
/**
* Load a dependency asset by relative path.
* 通过相对路径加载依赖资产。
*/
loadDependency<D = unknown>(relativePath: string): Promise<D>;
}
/**
* Asset loader interface.
* 资产加载器接口。
*
* Loaders only parse content, file reading is handled by AssetManager.
* 加载器只负责解析内容,文件读取由 AssetManager 处理。
*/
export interface IAssetLoader<T = unknown> {
/** Supported asset type. | 支持的资产类型。 */
readonly supportedType: AssetType;
/** Supported file extensions. | 支持的文件扩展名。 */
readonly supportedExtensions: string[];
/**
* Required content type for this loader.
* 此加载器需要的内容类型。
*
* - 'text': For JSON, shader, material files
* - 'binary': For binary formats
* - 'image': For textures
* - 'audio': For audio files
*/
readonly contentType: AssetContentType;
/**
* Parse asset from content.
* 从内容解析资产。
*
* @param content - File content. | 文件内容。
* @param context - Parse context. | 解析上下文。
* @returns Parsed asset. | 解析后的资产。
*/
parse(content: IAssetContent, context: IAssetParseContext): Promise<T>;
/**
* Dispose loaded asset and free resources.
* 释放已加载的资产。
*/
dispose(asset: T): void;
}
/**
* Asset loader factory interface
* 资产加载器工厂接口
*/
export interface IAssetLoaderFactory {
/**
* Create loader for specific asset type
* 为特定资产类型创建加载器
*/
createLoader(type: AssetType): IAssetLoader | null;
/**
* Register custom loader
* 注册自定义加载器
*/
registerLoader(type: AssetType, loader: IAssetLoader): void;
/**
* Unregister loader
* 注销加载器
*/
unregisterLoader(type: AssetType): void;
/**
* Check if loader exists for type
* 检查类型是否有加载器
*/
hasLoader(type: AssetType): boolean;
/**
* Get asset type by file extension
* 根据文件扩展名获取资产类型
*/
getAssetTypeByExtension(extension: string): AssetType | null;
/**
* Get asset type by file path
* 根据文件路径获取资产类型
*/
getAssetTypeByPath(path: string): AssetType | null;
}
/**
* Texture asset interface
* 纹理资产接口
*/
export interface ITextureAsset {
/** WebGL纹理ID / WebGL texture ID */
textureId: number;
/** 宽度 / Width */
width: number;
/** 高度 / Height */
height: number;
/** 格式 / Format */
format: 'rgba' | 'rgb' | 'alpha';
/** 是否有Mipmap / Has mipmaps */
hasMipmaps: boolean;
/** 原始数据(如果可用) / Raw image data if available */
data?: ImageData | HTMLImageElement;
}
/**
* Mesh asset interface
* 网格资产接口
*/
export interface IMeshAsset {
/** 顶点数据 / Vertex data */
vertices: Float32Array;
/** 索引数据 / Index data */
indices: Uint16Array | Uint32Array;
/** 法线数据 / Normal data */
normals?: Float32Array;
/** UV坐标 / UV coordinates */
uvs?: Float32Array;
/** 切线数据 / Tangent data */
tangents?: Float32Array;
/** 边界盒 / Axis-aligned bounding box */
bounds: {
min: [number, number, number];
max: [number, number, number];
};
}
/**
* Audio asset interface
* 音频资产接口
*/
export interface IAudioAsset {
/** 音频缓冲区 / Audio buffer */
buffer: AudioBuffer;
/** 时长(秒) / Duration in seconds */
duration: number;
/** 采样率 / Sample rate */
sampleRate: number;
/** 声道数 / Number of channels */
channels: number;
}
/**
* Material asset interface
* 材质资产接口
*/
export interface IMaterialAsset {
/** 着色器名称 / Shader name */
shader: string;
/** 材质属性 / Material properties */
properties: Map<string, unknown>;
/** 纹理映射 / Texture slot mappings */
textures: Map<string, AssetGUID>;
/** 渲染状态 / Render states */
renderStates: {
cullMode?: 'none' | 'front' | 'back';
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
depthTest?: boolean;
depthWrite?: boolean;
};
}
/**
* Prefab asset interface
* 预制体资产接口
*/
export interface IPrefabAsset {
/** 根实体数据 / Serialized entity hierarchy */
root: unknown;
/** 包含的组件类型 / Component types used in prefab */
componentTypes: string[];
/** 引用的资产 / All referenced assets */
referencedAssets: AssetGUID[];
}
/**
* Scene asset interface
* 场景资产接口
*/
export interface ISceneAsset {
/** 场景名称 / Scene name */
name: string;
/** 实体列表 / Serialized entity list */
entities: unknown[];
/** 场景设置 / Scene settings */
settings: {
/** 环境光 / Ambient light */
ambientLight?: [number, number, number];
/** 雾效 / Fog settings */
fog?: {
enabled: boolean;
color: [number, number, number];
density: number;
};
/** 天空盒 / Skybox asset */
skybox?: AssetGUID;
};
/** 引用的资产 / All referenced assets */
referencedAssets: AssetGUID[];
}
/**
* JSON asset interface
* JSON资产接口
*/
export interface IJsonAsset {
/** JSON数据 / JSON data */
data: unknown;
}
/**
* Text asset interface
* 文本资产接口
*/
export interface ITextAsset {
/** 文本内容 / Text content */
content: string;
/** 编码格式 / Encoding */
encoding: 'utf8' | 'utf16' | 'ascii';
}
/**
* Binary asset interface
* 二进制资产接口
*/
export interface IBinaryAsset {
/** 二进制数据 / Binary data */
data: ArrayBuffer;
/** MIME类型 / MIME type */
mimeType?: string;
}

View File

@@ -0,0 +1,334 @@
/**
* Asset manager interfaces
* 资产管理器接口
*/
import {
AssetGUID,
AssetHandle,
AssetType,
AssetState,
IAssetLoadOptions,
IAssetLoadResult,
IAssetReferenceInfo,
IAssetPreloadGroup,
IAssetLoadProgress
} from '../types/AssetTypes';
import { IAssetLoader } from './IAssetLoader';
/**
* Asset manager interface
* 资产管理器接口
*/
export interface IAssetManager {
/**
* Load asset by GUID
* 通过GUID加载资产
*/
loadAsset<T = unknown>(
guid: AssetGUID,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<T>>;
/**
* Load asset by path
* 通过路径加载资产
*/
loadAssetByPath<T = unknown>(
path: string,
options?: IAssetLoadOptions
): Promise<IAssetLoadResult<T>>;
/**
* Load multiple assets
* 批量加载资产
*/
loadAssets(
guids: AssetGUID[],
options?: IAssetLoadOptions
): Promise<Map<AssetGUID, IAssetLoadResult>>;
/**
* Preload asset group
* 预加载资产组
*/
preloadGroup(
group: IAssetPreloadGroup,
onProgress?: (progress: IAssetLoadProgress) => void
): Promise<void>;
/**
* Get loaded asset
* 获取已加载的资产
*/
getAsset<T = unknown>(guid: AssetGUID): T | null;
/**
* Get asset by handle
* 通过句柄获取资产
*/
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null;
/**
* Get loaded asset by path (synchronous)
* 通过路径获取已加载的资产(同步)
*/
getAssetByPath<T = unknown>(path: string): T | null;
/**
* Check if asset is loaded
* 检查资产是否已加载
*/
isLoaded(guid: AssetGUID): boolean;
/**
* Get asset state
* 获取资产状态
*/
getAssetState(guid: AssetGUID): AssetState;
/**
* Unload asset
* 卸载资产
*/
unloadAsset(guid: AssetGUID): void;
/**
* Unload all assets
* 卸载所有资产
*/
unloadAllAssets(): void;
/**
* Unload unused assets
* 卸载未使用的资产
*/
unloadUnusedAssets(): void;
/**
* Add reference to asset
* 增加资产引用
*/
addReference(guid: AssetGUID): void;
/**
* Remove reference from asset
* 移除资产引用
*/
removeReference(guid: AssetGUID): void;
/**
* Get reference info
* 获取引用信息
*/
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null;
/**
* Register custom loader
* 注册自定义加载器
*/
registerLoader(type: AssetType, loader: IAssetLoader): void;
/**
* Get asset statistics
* 获取资产统计信息
*/
getStatistics(): {
loadedCount: number;
loadQueue: number;
failedCount: number;
};
/**
* Clear cache
* 清空缓存
*/
clearCache(): void;
/**
* Dispose manager
* 释放管理器
*/
dispose(): void;
}
/**
* Asset cache interface
* 资产缓存接口
*/
export interface IAssetCache {
/**
* Get cached asset
* 获取缓存的资产
*/
get<T = unknown>(guid: AssetGUID): T | null;
/**
* Set cached asset
* 设置缓存的资产
*/
set<T = unknown>(guid: AssetGUID, asset: T, size: number): void;
/**
* Check if asset is cached
* 检查资产是否已缓存
*/
has(guid: AssetGUID): boolean;
/**
* Remove from cache
* 从缓存中移除
*/
remove(guid: AssetGUID): void;
/**
* Clear all cache
* 清空所有缓存
*/
clear(): void;
/**
* Get cache size
* 获取缓存大小
*/
getSize(): number;
/**
* Get cached asset count
* 获取缓存资产数量
*/
getCount(): number;
/**
* Evict assets based on policy
* 根据策略驱逐资产
*/
evict(targetSize: number): void;
}
/**
* Asset loading queue interface
* 资产加载队列接口
*/
export interface IAssetLoadQueue {
/**
* Add to queue
* 添加到队列
*/
enqueue(
guid: AssetGUID,
priority: number,
options?: IAssetLoadOptions
): void;
/**
* Remove from queue
* 从队列移除
*/
dequeue(): {
guid: AssetGUID;
options?: IAssetLoadOptions;
} | null;
/**
* Check if queue is empty
* 检查队列是否为空
*/
isEmpty(): boolean;
/**
* Get queue size
* 获取队列大小
*/
getSize(): number;
/**
* Clear queue
* 清空队列
*/
clear(): void;
/**
* Reprioritize item
* 重新设置优先级
*/
reprioritize(guid: AssetGUID, newPriority: number): void;
}
/**
* Asset dependency resolver interface
* 资产依赖解析器接口
*/
export interface IAssetDependencyResolver {
/**
* Resolve dependencies for asset
* 解析资产的依赖
*/
resolveDependencies(guid: AssetGUID): Promise<AssetGUID[]>;
/**
* Get direct dependencies
* 获取直接依赖
*/
getDirectDependencies(guid: AssetGUID): AssetGUID[];
/**
* Get all dependencies recursively
* 递归获取所有依赖
*/
getAllDependencies(guid: AssetGUID): AssetGUID[];
/**
* Check for circular dependencies
* 检查循环依赖
*/
hasCircularDependency(guid: AssetGUID): boolean;
/**
* Build dependency graph
* 构建依赖图
*/
buildDependencyGraph(guids: AssetGUID[]): Map<AssetGUID, AssetGUID[]>;
}
/**
* Asset streaming interface
* 资产流式加载接口
*/
export interface IAssetStreaming {
/**
* Start streaming assets
* 开始流式加载资产
*/
startStreaming(guids: AssetGUID[]): void;
/**
* Stop streaming
* 停止流式加载
*/
stopStreaming(): void;
/**
* Pause streaming
* 暂停流式加载
*/
pauseStreaming(): void;
/**
* Resume streaming
* 恢复流式加载
*/
resumeStreaming(): void;
/**
* Set streaming budget per frame
* 设置每帧流式加载预算
*/
setFrameBudget(milliseconds: number): void;
/**
* Get streaming progress
* 获取流式加载进度
*/
getProgress(): IAssetLoadProgress;
}

View File

@@ -0,0 +1,90 @@
/**
* Asset Reader Interface
* 资产读取器接口
*
* Provides unified file reading abstraction across different platforms.
* 提供跨平台的统一文件读取抽象。
*/
/**
* Asset content types.
* 资产内容类型。
*/
export type AssetContentType = 'text' | 'binary' | 'image' | 'audio';
/**
* Asset content result.
* 资产内容结果。
*/
export interface IAssetContent {
/** Content type. | 内容类型。 */
type: AssetContentType;
/** Text content (for text/json files). | 文本内容。 */
text?: string;
/** Binary content. | 二进制内容。 */
binary?: ArrayBuffer;
/** Image element (for textures). | 图片元素。 */
image?: HTMLImageElement;
/** Audio buffer (for audio files). | 音频缓冲区。 */
audioBuffer?: AudioBuffer;
}
/**
* Asset reader interface.
* 资产读取器接口。
*
* Abstracts platform-specific file reading operations.
* 抽象平台特定的文件读取操作。
*/
export interface IAssetReader {
/**
* Read file as text.
* 读取文件为文本。
*
* @param absolutePath - Absolute file path. | 绝对文件路径。
* @returns Text content. | 文本内容。
*/
readText(absolutePath: string): Promise<string>;
/**
* Read file as binary.
* 读取文件为二进制。
*
* @param absolutePath - Absolute file path. | 绝对文件路径。
* @returns Binary content. | 二进制内容。
*/
readBinary(absolutePath: string): Promise<ArrayBuffer>;
/**
* Load image from file.
* 从文件加载图片。
*
* @param absolutePath - Absolute file path. | 绝对文件路径。
* @returns Image element. | 图片元素。
*/
loadImage(absolutePath: string): Promise<HTMLImageElement>;
/**
* Load audio from file.
* 从文件加载音频。
*
* @param absolutePath - Absolute file path. | 绝对文件路径。
* @returns Audio buffer. | 音频缓冲区。
*/
loadAudio(absolutePath: string): Promise<AudioBuffer>;
/**
* Check if file exists.
* 检查文件是否存在。
*
* @param absolutePath - Absolute file path. | 绝对文件路径。
* @returns True if exists. | 是否存在。
*/
exists(absolutePath: string): Promise<boolean>;
}
/**
* Service identifier for IAssetReader.
* IAssetReader 的服务标识符。
*/
export const IAssetReaderService = Symbol.for('IAssetReaderService');

View File

@@ -0,0 +1,62 @@
/**
* 资源组件接口 - 用于依赖运行时资源的组件(纹理、音频等)
* Interface for components that depend on runtime resources (textures, audio, etc.)
*
* 实现此接口的组件可以参与 SceneResourceManager 管理的集中式资源加载
* Components implementing this interface can participate in centralized resource loading managed by SceneResourceManager
*/
/**
* 资源引用 - 包含路径和运行时 ID
* Resource reference with path and runtime ID
*/
export interface ResourceReference {
/** 资源路径(例如 "assets/sprites/player.png"/ Asset path (e.g., "assets/sprites/player.png") */
path: string;
/** 引擎分配的运行时资源 ID例如 GPU 上的纹理 ID/ Runtime resource ID assigned by engine (e.g., texture ID on GPU) */
runtimeId?: number;
/** 资源类型标识符 / Resource type identifier */
type: 'texture' | 'audio' | 'font' | 'data';
}
/**
* 资源组件接口
* Resource component interface
*
* 实现此接口的组件可以在场景启动前由 SceneResourceManager 集中加载资源
* Components implementing this interface can have their resources loaded centrally by SceneResourceManager before the scene starts
*/
export interface IResourceComponent {
/**
* 获取此组件需要的所有资源引用
* Get all resource references needed by this component
*
* 在场景加载期间调用以收集资源路径
* Called during scene loading to collect resource paths
*/
getResourceReferences(): ResourceReference[];
/**
* 设置已加载资源的运行时 ID
* Set runtime IDs for loaded resources
*
* 在 SceneResourceManager 加载资源后调用
* Called after resources are loaded by SceneResourceManager
*
* @param pathToId 资源路径到运行时 ID 的映射 / Map of resource paths to runtime IDs
*/
setResourceIds(pathToId: Map<string, number>): void;
}
/**
* 类型守卫 - 检查组件是否实现了 IResourceComponent
* Type guard to check if a component implements IResourceComponent
*/
export function isResourceComponent(component: any): component is IResourceComponent {
return (
component !== null &&
typeof component === 'object' &&
typeof component.getResourceReferences === 'function' &&
typeof component.setResourceIds === 'function'
);
}

View File

@@ -0,0 +1,141 @@
/**
* Asset loader factory implementation
* 资产加载器工厂实现
*/
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
import { TextureLoader } from './TextureLoader';
import { JsonLoader } from './JsonLoader';
import { TextLoader } from './TextLoader';
import { BinaryLoader } from './BinaryLoader';
/**
* Asset loader factory
* 资产加载器工厂
*/
export class AssetLoaderFactory implements IAssetLoaderFactory {
private readonly _loaders = new Map<AssetType, IAssetLoader>();
constructor() {
// 注册默认加载器 / Register default loaders
this.registerDefaultLoaders();
}
/**
* Register default loaders
* 注册默认加载器
*/
private registerDefaultLoaders(): void {
// 纹理加载器 / Texture loader
this._loaders.set(AssetType.Texture, new TextureLoader());
// JSON加载器 / JSON loader
this._loaders.set(AssetType.Json, new JsonLoader());
// 文本加载器 / Text loader
this._loaders.set(AssetType.Text, new TextLoader());
// 二进制加载器 / Binary loader
this._loaders.set(AssetType.Binary, new BinaryLoader());
}
/**
* Create loader for specific asset type
* 为特定资产类型创建加载器
*/
createLoader(type: AssetType): IAssetLoader | null {
return this._loaders.get(type) || null;
}
/**
* Register custom loader
* 注册自定义加载器
*/
registerLoader(type: AssetType, loader: IAssetLoader): void {
this._loaders.set(type, loader);
}
/**
* Unregister loader
* 注销加载器
*/
unregisterLoader(type: AssetType): void {
this._loaders.delete(type);
}
/**
* Check if loader exists for type
* 检查类型是否有加载器
*/
hasLoader(type: AssetType): boolean {
return this._loaders.has(type);
}
/**
* Get asset type by file extension
* 根据文件扩展名获取资产类型
*
* @param extension - File extension including dot (e.g., '.btree', '.png')
* @returns Asset type if a loader supports this extension, null otherwise
*/
getAssetTypeByExtension(extension: string): AssetType | null {
const ext = extension.toLowerCase();
for (const [type, loader] of this._loaders) {
if (loader.supportedExtensions.some(e => e.toLowerCase() === ext)) {
return type;
}
}
return null;
}
/**
* Get asset type by file path
* 根据文件路径获取资产类型
*
* Checks for compound extensions (like .tilemap.json) first, then simple extensions
*
* @param path - File path
* @returns Asset type if a loader supports this file, null otherwise
*/
getAssetTypeByPath(path: string): AssetType | null {
const lowerPath = path.toLowerCase();
// First check compound extensions (e.g., .tilemap.json)
for (const [type, loader] of this._loaders) {
for (const ext of loader.supportedExtensions) {
if (ext.includes('.') && ext.split('.').length > 2) {
// This is a compound extension like .tilemap.json
if (lowerPath.endsWith(ext.toLowerCase())) {
return type;
}
}
}
}
// Then check simple extensions
const lastDot = path.lastIndexOf('.');
if (lastDot !== -1) {
const ext = path.substring(lastDot).toLowerCase();
return this.getAssetTypeByExtension(ext);
}
return null;
}
/**
* Get all registered loaders
* 获取所有注册的加载器
*/
getRegisteredTypes(): AssetType[] {
return Array.from(this._loaders.keys());
}
/**
* Clear all loaders
* 清空所有加载器
*/
clear(): void {
this._loaders.clear();
}
}

View File

@@ -0,0 +1,43 @@
/**
* Binary asset loader
* 二进制资产加载器
*/
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, IBinaryAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Binary loader implementation
* 二进制加载器实现
*/
export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
readonly supportedType = AssetType.Binary;
readonly supportedExtensions = [
'.bin', '.dat', '.raw', '.bytes',
'.wasm', '.so', '.dll', '.dylib'
];
readonly contentType: AssetContentType = 'binary';
/**
* Parse binary from content.
* 从内容解析二进制。
*/
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IBinaryAsset> {
if (!content.binary) {
throw new Error('Binary content is empty');
}
return {
data: content.binary
};
}
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: IBinaryAsset): void {
(asset as any).data = null;
}
}

View File

@@ -0,0 +1,40 @@
/**
* JSON asset loader
* JSON资产加载器
*/
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, IJsonAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* JSON loader implementation
* JSON加载器实现
*/
export class JsonLoader implements IAssetLoader<IJsonAsset> {
readonly supportedType = AssetType.Json;
readonly supportedExtensions = ['.json', '.jsonc'];
readonly contentType: AssetContentType = 'text';
/**
* Parse JSON from text content.
* 从文本内容解析JSON。
*/
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<IJsonAsset> {
if (!content.text) {
throw new Error('JSON content is empty');
}
return {
data: JSON.parse(content.text)
};
}
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: IJsonAsset): void {
(asset as any).data = null;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Text asset loader
* 文本资产加载器
*/
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, ITextAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Text loader implementation
* 文本加载器实现
*/
export class TextLoader implements IAssetLoader<ITextAsset> {
readonly supportedType = AssetType.Text;
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
readonly contentType: AssetContentType = 'text';
/**
* Parse text from content.
* 从内容解析文本。
*/
async parse(content: IAssetContent, _context: IAssetParseContext): Promise<ITextAsset> {
if (!content.text) {
throw new Error('Text content is empty');
}
return {
content: content.text,
encoding: this.detectEncoding(content.text)
};
}
/**
* Detect text encoding
* 检测文本编码
*/
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
for (let i = 0; i < content.length; i++) {
const charCode = content.charCodeAt(i);
if (charCode > 127) {
return charCode > 255 ? 'utf16' : 'utf8';
}
}
return 'ascii';
}
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: ITextAsset): void {
(asset as any).content = '';
}
}

View File

@@ -0,0 +1,78 @@
/**
* Texture asset loader
* 纹理资产加载器
*/
import { AssetType } from '../types/AssetTypes';
import { IAssetLoader, ITextureAsset, IAssetParseContext } from '../interfaces/IAssetLoader';
import { IAssetContent, AssetContentType } from '../interfaces/IAssetReader';
/**
* Texture loader implementation
* 纹理加载器实现
*/
export class TextureLoader implements IAssetLoader<ITextureAsset> {
readonly supportedType = AssetType.Texture;
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
readonly contentType: AssetContentType = 'image';
private static _nextTextureId = 1;
/**
* Parse texture from image content.
* 从图片内容解析纹理。
*/
async parse(content: IAssetContent, context: IAssetParseContext): Promise<ITextureAsset> {
if (!content.image) {
throw new Error('Texture content is empty');
}
const image = content.image;
const textureAsset: ITextureAsset = {
textureId: TextureLoader._nextTextureId++,
width: image.width,
height: image.height,
format: 'rgba',
hasMipmaps: false,
data: image
};
// Upload to GPU if bridge exists.
if (typeof window !== 'undefined' && (window as any).engineBridge) {
await this.uploadToGPU(textureAsset, context.metadata.path);
}
return textureAsset;
}
/**
* Upload texture to GPU
* 上传纹理到GPU
*/
private async uploadToGPU(textureAsset: ITextureAsset, path: string): Promise<void> {
const bridge = (window as any).engineBridge;
if (bridge && bridge.loadTexture) {
await bridge.loadTexture(textureAsset.textureId, path);
}
}
/**
* Dispose loaded asset
* 释放已加载的资产
*/
dispose(asset: ITextureAsset): void {
// Release GPU resources.
if (typeof window !== 'undefined' && (window as any).engineBridge) {
const bridge = (window as any).engineBridge;
if (bridge.unloadTexture) {
bridge.unloadTexture(asset.textureId);
}
}
// Clean up image data.
if (asset.data instanceof HTMLImageElement) {
asset.data.src = '';
}
}
}

View File

@@ -0,0 +1,275 @@
/**
* Runtime Catalog for Asset Resolution
* 资产解析的运行时目录
*
* Provides GUID-based asset lookup at runtime.
* 提供运行时基于 GUID 的资产查找。
*/
import { AssetGUID, AssetType } from '../types/AssetTypes';
import {
IRuntimeCatalog,
IRuntimeAssetLocation,
IRuntimeBundleInfo
} from '../bundle/BundleFormat';
/**
* Runtime Catalog Manager
* 运行时目录管理器
*
* Loads and manages the asset catalog for runtime GUID resolution.
*/
export class RuntimeCatalog {
private _catalog: IRuntimeCatalog | null = null;
private _loadedBundles = new Map<string, ArrayBuffer>();
private _loadingBundles = new Map<string, Promise<ArrayBuffer>>();
private _baseUrl: string = './';
/**
* Set base URL for loading catalog and bundles
* 设置加载目录和包的基础 URL
*/
setBaseUrl(url: string): void {
this._baseUrl = url.endsWith('/') ? url : `${url}/`;
}
/**
* Load catalog from URL
* 从 URL 加载目录
*/
async loadCatalog(catalogUrl?: string): Promise<void> {
const url = catalogUrl || `${this._baseUrl}asset-catalog.json`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load catalog: ${response.status}`);
}
const data = await response.json();
this._catalog = this._parseCatalog(data);
console.log(`[RuntimeCatalog] Loaded catalog with ${Object.keys(this._catalog.assets).length} assets`);
} catch (error) {
console.error('[RuntimeCatalog] Failed to load catalog:', error);
throw error;
}
}
/**
* Initialize with pre-loaded catalog data
* 使用预加载的目录数据初始化
*/
initWithData(catalogData: IRuntimeCatalog): void {
this._catalog = catalogData;
}
/**
* Check if catalog is loaded
* 检查目录是否已加载
*/
isLoaded(): boolean {
return this._catalog !== null;
}
/**
* Get asset location by GUID
* 根据 GUID 获取资产位置
*/
getAssetLocation(guid: AssetGUID): IRuntimeAssetLocation | null {
if (!this._catalog) {
console.warn('[RuntimeCatalog] Catalog not loaded');
return null;
}
return this._catalog.assets[guid] || null;
}
/**
* Check if asset exists in catalog
* 检查资产是否存在于目录中
*/
hasAsset(guid: AssetGUID): boolean {
return this._catalog?.assets[guid] !== undefined;
}
/**
* Get all assets of a specific type
* 获取特定类型的所有资产
*/
getAssetsByType(type: AssetType): AssetGUID[] {
if (!this._catalog) return [];
return Object.entries(this._catalog.assets)
.filter(([_, loc]) => loc.type === type)
.map(([guid]) => guid);
}
/**
* Get bundle info
* 获取包信息
*/
getBundleInfo(bundleName: string): IRuntimeBundleInfo | null {
return this._catalog?.bundles[bundleName] || null;
}
/**
* Load a bundle
* 加载包
*/
async loadBundle(bundleName: string): Promise<ArrayBuffer> {
// Return cached bundle
const cached = this._loadedBundles.get(bundleName);
if (cached) {
return cached;
}
// Return pending load
const pending = this._loadingBundles.get(bundleName);
if (pending) {
return pending;
}
// Start new load
const bundleInfo = this.getBundleInfo(bundleName);
if (!bundleInfo) {
throw new Error(`Bundle not found in catalog: ${bundleName}`);
}
const loadPromise = this._fetchBundle(bundleInfo);
this._loadingBundles.set(bundleName, loadPromise);
try {
const data = await loadPromise;
this._loadedBundles.set(bundleName, data);
return data;
} finally {
this._loadingBundles.delete(bundleName);
}
}
/**
* Load asset data by GUID
* 根据 GUID 加载资产数据
*/
async loadAssetData(guid: AssetGUID): Promise<ArrayBuffer> {
const location = this.getAssetLocation(guid);
if (!location) {
throw new Error(`Asset not found in catalog: ${guid}`);
}
// Load the bundle containing this asset
const bundleData = await this.loadBundle(location.bundle);
// Extract asset data from bundle
return bundleData.slice(location.offset, location.offset + location.size);
}
/**
* Preload bundles marked for preloading
* 预加载标记为预加载的包
*/
async preloadBundles(): Promise<void> {
if (!this._catalog) return;
const preloadPromises: Promise<void>[] = [];
for (const [name, info] of Object.entries(this._catalog.bundles)) {
if (info.preload) {
preloadPromises.push(
this.loadBundle(name).then(() => {
console.log(`[RuntimeCatalog] Preloaded bundle: ${name}`);
})
);
}
}
await Promise.all(preloadPromises);
}
/**
* Unload a bundle from memory
* 从内存卸载包
*/
unloadBundle(bundleName: string): void {
this._loadedBundles.delete(bundleName);
}
/**
* Clear all loaded bundles
* 清除所有已加载的包
*/
clearBundles(): void {
this._loadedBundles.clear();
}
/**
* Get catalog statistics
* 获取目录统计信息
*/
getStatistics(): {
totalAssets: number;
totalBundles: number;
loadedBundles: number;
assetsByType: Record<string, number>;
} {
if (!this._catalog) {
return {
totalAssets: 0,
totalBundles: 0,
loadedBundles: 0,
assetsByType: {}
};
}
const assetsByType: Record<string, number> = {};
for (const loc of Object.values(this._catalog.assets)) {
assetsByType[loc.type] = (assetsByType[loc.type] || 0) + 1;
}
return {
totalAssets: Object.keys(this._catalog.assets).length,
totalBundles: Object.keys(this._catalog.bundles).length,
loadedBundles: this._loadedBundles.size,
assetsByType
};
}
/**
* Parse catalog JSON to typed structure
* 将目录 JSON 解析为类型化结构
*/
private _parseCatalog(data: unknown): IRuntimeCatalog {
const raw = data as Record<string, unknown>;
return {
version: (raw.version as string) || '1.0',
createdAt: (raw.createdAt as number) || Date.now(),
bundles: (raw.bundles as Record<string, IRuntimeBundleInfo>) || {},
assets: (raw.assets as Record<AssetGUID, IRuntimeAssetLocation>) || {}
};
}
/**
* Fetch bundle data
* 获取包数据
*/
private async _fetchBundle(info: IRuntimeBundleInfo): Promise<ArrayBuffer> {
const url = info.url.startsWith('http')
? info.url
: `${this._baseUrl}${info.url}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load bundle: ${url} (${response.status})`);
}
return response.arrayBuffer();
}
}
/**
* Global runtime catalog instance
* 全局运行时目录实例
*/
export const runtimeCatalog = new RuntimeCatalog();

View File

@@ -0,0 +1,155 @@
/**
* 场景资源管理器 - 集中式场景资源加载
* SceneResourceManager - Centralized resource loading for scenes
*
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
*/
import type { Scene } from '@esengine/ecs-framework';
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
/**
* 资源加载器接口
* Resource loader interface
*/
export interface IResourceLoader {
/**
* 批量加载资源并返回路径到 ID 的映射
* Load a batch of resources and return path-to-ID mapping
* @param paths 资源路径数组 / Array of resource paths
* @param type 资源类型 / Resource type
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
*/
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
}
export class SceneResourceManager {
private resourceLoader: IResourceLoader | null = null;
/**
* 设置资源加载器实现
* Set the resource loader implementation
*
* 应由引擎集成层调用
* This should be called by the engine integration layer
*
* @param loader 资源加载器实例 / Resource loader instance
*/
setResourceLoader(loader: IResourceLoader): void {
this.resourceLoader = loader;
}
/**
* 加载场景所需的所有资源
* Load all resources required by a scene
*
* 流程 / Process:
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
* Scan all entities and collect resource references from IResourceComponent implementations
* 2. 按类型分组资源(纹理、音频等)
* Group resources by type (texture, audio, etc.)
* 3. 批量加载每种资源类型
* Batch load each resource type
* 4. 将运行时 ID 分配回组件
* Assign runtime IDs back to components
*
* @param scene 要加载资源的场景 / The scene to load resources for
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
*/
async loadSceneResources(scene: Scene): Promise<void> {
if (!this.resourceLoader) {
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
return;
}
// 从组件收集所有资源引用 / Collect all resource references from components
const resourceRefs = this.collectResourceReferences(scene);
if (resourceRefs.length === 0) {
return;
}
// 按资源类型分组 / Group by resource type
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
for (const ref of resourceRefs) {
if (!resourcesByType.has(ref.type)) {
resourcesByType.set(ref.type, new Set());
}
resourcesByType.get(ref.type)!.add(ref.path);
}
// 批量加载每种资源类型 / Load each resource type in batch
const allResourceIds = new Map<string, number>();
for (const [type, paths] of resourcesByType) {
const pathsArray = Array.from(paths);
try {
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
// 合并到总映射表 / Merge into combined map
for (const [path, id] of resourceIds) {
allResourceIds.set(path, id);
}
} catch (error) {
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
}
}
// 将资源 ID 分配回组件 / Assign resource IDs back to components
this.assignResourceIds(scene, allResourceIds);
}
/**
* 从场景实体收集所有资源引用
* Collect all resource references from scene entities
*/
private collectResourceReferences(scene: Scene): ResourceReference[] {
const refs: ResourceReference[] = [];
for (const entity of scene.entities.buffer) {
for (const component of entity.components) {
if (isResourceComponent(component)) {
const componentRefs = component.getResourceReferences();
refs.push(...componentRefs);
}
}
}
return refs;
}
/**
* 将已加载的资源 ID 分配回组件
* Assign loaded resource IDs back to components
*
* @param scene 场景 / Scene
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
*/
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
for (const entity of scene.entities.buffer) {
for (const component of entity.components) {
if (isResourceComponent(component)) {
component.setResourceIds(pathToId);
}
}
}
}
/**
* 卸载场景使用的所有资源
* Unload all resources used by a scene
*
* 在场景销毁时调用
* Called when a scene is being destroyed
*
* @param scene 要卸载资源的场景 / The scene to unload resources for
*/
async unloadSceneResources(_scene: Scene): Promise<void> {
// TODO: 实现资源卸载 / Implement resource unloading
// 需要跟踪资源引用计数,仅在不再使用时卸载
// Need to track resource reference counts and only unload when no longer used
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
}
}

View File

@@ -0,0 +1,413 @@
/**
* Core asset system types and enums
* 核心资产系统类型和枚举
*/
/**
* Unique identifier for assets across the project
* 项目中资产的唯一标识符
*/
export type AssetGUID = string;
/**
* Runtime asset handle for efficient access
* 运行时资产句柄,用于高效访问
*/
export type AssetHandle = number;
/**
* Asset loading state
* 资产加载状态
*/
export enum AssetState {
/** 未加载 */
Unloaded = 'unloaded',
/** 加载中 */
Loading = 'loading',
/** 已加载 */
Loaded = 'loaded',
/** 加载失败 */
Failed = 'failed',
/** 释放中 */
Disposing = 'disposing'
}
/**
* Asset type - string based for extensibility
* 资产类型 - 使用字符串以支持插件扩展
*
* Plugins can define their own asset types by using custom strings.
* Built-in types are provided as constants below.
* 插件可以通过使用自定义字符串定义自己的资产类型。
* 内置类型作为常量提供如下。
*/
export type AssetType = string;
/**
* Built-in asset types provided by asset-system
* asset-system 提供的内置资产类型
*/
export const AssetType = {
/** 纹理 */
Texture: 'texture',
/** 网格 */
Mesh: 'mesh',
/** 材质 */
Material: 'material',
/** 着色器 */
Shader: 'shader',
/** 音频 */
Audio: 'audio',
/** 字体 */
Font: 'font',
/** 预制体 */
Prefab: 'prefab',
/** 场景 */
Scene: 'scene',
/** 脚本 */
Script: 'script',
/** 动画片段 */
AnimationClip: 'animation',
/** JSON数据 */
Json: 'json',
/** 文本 */
Text: 'text',
/** 二进制 */
Binary: 'binary',
/** 自定义 */
Custom: 'custom'
} as const;
/**
* Platform variants for assets
* 资产的平台变体
*/
export enum AssetPlatform {
/** H5平台浏览器 */
H5 = 'h5',
/** 微信小游戏 */
WeChat = 'wechat',
/** 试玩广告Playable Ads */
Playable = 'playable',
/** Android平台 */
Android = 'android',
/** iOS平台 */
iOS = 'ios',
/** 编辑器Tauri桌面 */
Editor = 'editor'
}
/**
* Quality levels for asset variants
* 资产变体的质量级别
*/
export enum AssetQuality {
/** 低质量 */
Low = 'low',
/** 中等质量 */
Medium = 'medium',
/** 高质量 */
High = 'high',
/** 超高质量 */
Ultra = 'ultra'
}
/**
* Asset metadata stored in the database
* 存储在数据库中的资产元数据
*/
export interface IAssetMetadata {
/** 全局唯一标识符 */
guid: AssetGUID;
/** 资产路径 */
path: string;
/** 资产类型 */
type: AssetType;
/** 资产名称 */
name: string;
/** 文件大小(字节) / File size in bytes */
size: number;
/** 内容哈希值 / Content hash for versioning */
hash: string;
/** 依赖的其他资产 / Dependencies on other assets */
dependencies: AssetGUID[];
/** 资产标签 / User-defined labels for categorization */
labels: string[];
/** 自定义标签 / Custom metadata tags */
tags: Map<string, string>;
/** 导入设置 / Import-time settings */
importSettings?: Record<string, unknown>;
/** 最后修改时间 / Unix timestamp of last modification */
lastModified: number;
/** 版本号 / Asset version number */
version: number;
}
/**
* Asset variant descriptor
* 资产变体描述符
*/
export interface IAssetVariant {
/** 目标平台 */
platform: AssetPlatform;
/** 质量级别 */
quality: AssetQuality;
/** 本地化语言 / Language code for localized assets */
locale?: string;
/** 主题变体 / Theme identifier (e.g., 'dark', 'light') */
theme?: string;
}
/**
* Asset load options
* 资产加载选项
*/
export interface IAssetLoadOptions {
/** 加载优先级0-100越高越优先 / Priority level 0-100, higher loads first */
priority?: number;
/** 是否异步加载 / Use async loading */
async?: boolean;
/** 指定加载的变体 / Specific variant to load */
variant?: IAssetVariant;
/** 强制重新加载 / Force reload even if cached */
forceReload?: boolean;
/** 超时时间(毫秒) / Timeout in milliseconds */
timeout?: number;
/** 进度回调 / Progress callback (0-1) */
onProgress?: (progress: number) => void;
}
/**
* Asset bundle manifest
* 资产包清单
*/
export interface IAssetBundleManifest {
/** 包名称 */
name: string;
/** 版本号 */
version: string;
/** 内容哈希 / Content hash for integrity check */
hash: string;
/** 压缩类型 */
compression?: 'none' | 'gzip' | 'brotli';
/** 包含的资产列表 / Assets contained in this bundle */
assets: AssetGUID[];
/** 依赖的其他包 / Other bundles this depends on */
dependencies: string[];
/** 包大小(字节) / Bundle size in bytes */
size: number;
/** 创建时间戳 / Creation timestamp */
createdAt: number;
}
/**
* Asset loading result
* 资产加载结果
*/
export interface IAssetLoadResult<T = unknown> {
/** 加载的资产实例 */
asset: T;
/** 资产句柄 */
handle: AssetHandle;
/** 资产元数据 */
metadata: IAssetMetadata;
/** 加载耗时(毫秒) / Load time in milliseconds */
loadTime: number;
}
/**
* Asset loading error
* 资产加载错误
*/
export class AssetLoadError extends Error {
constructor(
message: string,
public readonly guid: AssetGUID,
public readonly type: AssetType,
public readonly cause?: Error
) {
super(message);
this.name = 'AssetLoadError';
Object.setPrototypeOf(this, new.target.prototype);
}
/**
* Factory method for file not found error
* 文件未找到错误的工厂方法
*/
static fileNotFound(guid: AssetGUID, path: string): AssetLoadError {
return new AssetLoadError(`Asset file not found: ${path}`, guid, AssetType.Custom);
}
/**
* Factory method for unsupported type error
* 不支持的类型错误的工厂方法
*/
static unsupportedType(guid: AssetGUID, type: AssetType): AssetLoadError {
return new AssetLoadError(`Unsupported asset type: ${type}`, guid, type);
}
/**
* Factory method for load timeout error
* 加载超时错误的工厂方法
*/
static loadTimeout(guid: AssetGUID, type: AssetType, timeout: number): AssetLoadError {
return new AssetLoadError(`Asset load timeout after ${timeout}ms`, guid, type);
}
/**
* Factory method for dependency failed error
* 依赖加载失败错误的工厂方法
*/
static dependencyFailed(guid: AssetGUID, type: AssetType, depGuid: AssetGUID): AssetLoadError {
return new AssetLoadError(`Dependency failed to load: ${depGuid}`, guid, type);
}
}
/**
* Asset reference counting info
* 资产引用计数信息
*/
export interface IAssetReferenceInfo {
/** 资产GUID */
guid: AssetGUID;
/** 资产句柄 */
handle: AssetHandle;
/** 引用计数 */
referenceCount: number;
/** 最后访问时间 / Unix timestamp of last access */
lastAccessTime: number;
/** 当前状态 */
state: AssetState;
}
/**
* Asset import options
* 资产导入选项
*/
export interface IAssetImportOptions {
/** 资产类型 */
type: AssetType;
/** 生成Mipmap / Generate mipmaps for textures */
generateMipmaps?: boolean;
/** 纹理压缩格式 / Texture compression format */
compression?: 'none' | 'dxt' | 'etc2' | 'astc';
/** 最大纹理尺寸 / Maximum texture dimension */
maxTextureSize?: number;
/** 生成LOD / Generate LODs for meshes */
generateLODs?: boolean;
/** 优化网格 / Optimize mesh geometry */
optimizeMesh?: boolean;
/** 音频格式 / Audio encoding format */
audioFormat?: 'mp3' | 'ogg' | 'wav';
/** 自定义处理器 / Custom processor plugin name */
customProcessor?: string;
}
/**
* Asset usage statistics
* 资产使用统计
*/
export interface IAssetUsageStats {
/** 资产GUID */
guid: AssetGUID;
/** 加载次数 */
loadCount: number;
/** 总加载时间(毫秒) / Total time spent loading in ms */
totalLoadTime: number;
/** 平均加载时间(毫秒) / Average load time in ms */
averageLoadTime: number;
/** 最后使用时间 / Unix timestamp of last use */
lastUsedTime: number;
/** 被引用的资产列表 / Assets that reference this one */
referencedBy: AssetGUID[];
}
/**
* Asset preload group
* 资产预加载组
*/
export interface IAssetPreloadGroup {
/** 组名称 */
name: string;
/** 包含的资产 */
assets: AssetGUID[];
/** 加载优先级 / Load priority 0-100 */
priority: number;
/** 是否必需 / Must be loaded before scene start */
required: boolean;
}
/**
* Asset loading progress info
* 资产加载进度信息
*/
export interface IAssetLoadProgress {
/** 当前加载的资产 */
currentAsset: string;
/** 已加载数量 */
loadedCount: number;
/** 总数量 */
totalCount: number;
/** 已加载字节数 */
loadedBytes: number;
/** 总字节数 */
totalBytes: number;
/** 进度百分比0-1 / Progress value 0-1 */
progress: number;
}
/**
* Asset catalog entry for runtime lookups
* 运行时查找的资产目录条目
*/
export interface IAssetCatalogEntry {
/** 资产GUID */
guid: AssetGUID;
/** 资产路径 */
path: string;
/** 资产类型 */
type: AssetType;
/** 所在包名称 / Bundle containing this asset */
bundleName?: string;
/** 可用变体 / Available variants */
variants?: IAssetVariant[];
/** 大小(字节) / Size in bytes */
size: number;
/** 内容哈希 / Content hash */
hash: string;
}
/**
* Runtime asset catalog
* 运行时资产目录
*/
export interface IAssetCatalog {
/** 版本号 */
version: string;
/** 创建时间戳 / Creation timestamp */
createdAt: number;
/** 所有目录条目 / All catalog entries */
entries: Map<AssetGUID, IAssetCatalogEntry>;
/** 此目录中的包 / Bundles in this catalog */
bundles: Map<string, IAssetBundleManifest>;
}
/**
* Asset hot-reload event
* 资产热重载事件
*/
export interface IAssetHotReloadEvent {
/** 资产GUID */
guid: AssetGUID;
/** 资产路径 */
path: string;
/** 资产类型 */
type: AssetType;
/** 旧版本哈希 / Previous version hash */
oldHash: string;
/** 新版本哈希 / New version hash */
newHash: string;
/** 时间戳 */
timestamp: number;
}

View File

@@ -0,0 +1,227 @@
/**
* Path Validator
* 路径验证器
*
* Validates and sanitizes asset paths for security
* 验证并清理资产路径以确保安全
*/
/**
* Path validation options.
* 路径验证选项。
*/
export interface PathValidationOptions {
/** Allow absolute paths (for editor environment). | 允许绝对路径(用于编辑器环境)。 */
allowAbsolutePaths?: boolean;
/** Allow URLs (http://, https://, asset://). | 允许 URL。 */
allowUrls?: boolean;
}
export class PathValidator {
// Dangerous path patterns (without absolute path checks)
private static readonly DANGEROUS_PATTERNS_STRICT = [
/\.\.[/\\]/g, // Path traversal attempts (..)
/^[/\\]/, // Absolute paths on Unix
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
/\0/, // Null bytes
/%00/, // URL encoded null bytes
/\.\.%2[fF]/ // URL encoded path traversal
];
// Dangerous path patterns (allowing absolute paths)
private static readonly DANGEROUS_PATTERNS_RELAXED = [
/\.\.[/\\]/g, // Path traversal attempts (..)
/\0/, // Null bytes
/%00/, // URL encoded null bytes
/\.\.%2[fF]/ // URL encoded path traversal
];
// Valid path characters for relative paths (alphanumeric, dash, underscore, dot, slash)
private static readonly VALID_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@]+$/;
// Valid path characters for absolute paths (includes colon for Windows drives)
private static readonly VALID_ABSOLUTE_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@:]+$/;
// URL pattern
private static readonly URL_REGEX = /^(https?|asset|blob|data):\/\//;
// Maximum path length
private static readonly MAX_PATH_LENGTH = 1024;
/** Global options for path validation. | 路径验证的全局选项。 */
private static _globalOptions: PathValidationOptions = {
allowAbsolutePaths: false,
allowUrls: true
};
/**
* Set global validation options.
* 设置全局验证选项。
*/
static setGlobalOptions(options: PathValidationOptions): void {
this._globalOptions = { ...this._globalOptions, ...options };
}
/**
* Get current global options.
* 获取当前全局选项。
*/
static getGlobalOptions(): PathValidationOptions {
return { ...this._globalOptions };
}
/**
* Validate if a path is safe
* 验证路径是否安全
*/
static validate(path: string, options?: PathValidationOptions): { valid: boolean; reason?: string } {
const opts = { ...this._globalOptions, ...options };
// Check for null/undefined/empty
if (!path || typeof path !== 'string') {
return { valid: false, reason: 'Path is empty or invalid' };
}
// Check length
if (path.length > this.MAX_PATH_LENGTH) {
return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` };
}
// Allow URLs if enabled
if (opts.allowUrls && this.URL_REGEX.test(path)) {
return { valid: true };
}
// Choose patterns based on options
const patterns = opts.allowAbsolutePaths
? this.DANGEROUS_PATTERNS_RELAXED
: this.DANGEROUS_PATTERNS_STRICT;
// Check for dangerous patterns
for (const pattern of patterns) {
if (pattern.test(path)) {
return { valid: false, reason: 'Path contains dangerous pattern' };
}
}
// Check for valid characters
const validCharsRegex = opts.allowAbsolutePaths
? this.VALID_ABSOLUTE_PATH_REGEX
: this.VALID_PATH_REGEX;
if (!validCharsRegex.test(path)) {
return { valid: false, reason: 'Path contains invalid characters' };
}
return { valid: true };
}
/**
* Sanitize a path
* 清理路径
*/
static sanitize(path: string): string {
if (!path || typeof path !== 'string') {
return '';
}
// Remove dangerous patterns
let sanitized = path;
// Remove path traversal (apply repeatedly until fully removed)
let prev;
do {
prev = sanitized;
sanitized = sanitized.replace(/\.\.[/\\]/g, '');
} while (sanitized !== prev);
// Remove leading slashes
sanitized = sanitized.replace(/^[/\\]+/, '');
// Remove null bytes
sanitized = sanitized.replace(/\0/g, '');
sanitized = sanitized.replace(/%00/g, '');
// Remove invalid Windows characters
sanitized = sanitized.replace(/[<>:"|?*]/g, '_');
// Normalize slashes
sanitized = sanitized.replace(/\\/g, '/');
// Remove double slashes
sanitized = sanitized.replace(/\/+/g, '/');
// Trim whitespace
sanitized = sanitized.trim();
// Truncate if too long
if (sanitized.length > this.MAX_PATH_LENGTH) {
sanitized = sanitized.substring(0, this.MAX_PATH_LENGTH);
}
return sanitized;
}
/**
* Check if path is trying to escape the base directory
* 检查路径是否试图逃离基础目录
*/
static isPathTraversal(path: string): boolean {
const normalized = path.replace(/\\/g, '/');
return normalized.includes('../') || normalized.includes('..\\');
}
/**
* Normalize a path for consistent handling
* 规范化路径以便一致处理
*/
static normalize(path: string): string {
if (!path) return '';
// Sanitize first
let normalized = this.sanitize(path);
// Convert backslashes to forward slashes
normalized = normalized.replace(/\\/g, '/');
// Remove trailing slash (except for root)
if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
/**
* Join path segments safely
* 安全地连接路径段
*/
static join(...segments: string[]): string {
const validSegments = segments
.filter((s) => s && typeof s === 'string')
.map((s) => this.sanitize(s))
.filter((s) => s.length > 0);
if (validSegments.length === 0) {
return '';
}
return this.normalize(validSegments.join('/'));
}
/**
* Get file extension safely
* 安全地获取文件扩展名
*/
static getExtension(path: string): string {
const sanitized = this.sanitize(path);
const lastDot = sanitized.lastIndexOf('.');
const lastSlash = sanitized.lastIndexOf('/');
if (lastDot > lastSlash && lastDot > 0) {
return sanitized.substring(lastDot + 1).toLowerCase();
}
return '';
}
}

View File

@@ -0,0 +1,81 @@
/**
* UV Coordinate Helper
* UV 坐标辅助工具
*
* 引擎使用图像坐标系:
* Engine uses image coordinate system:
* - 原点 (0, 0) 在左上角 | Origin at top-left
* - V 轴向下增长 | V-axis increases downward
* - UV 格式:[u0, v0, u1, v1] 其中 v0 < v1
*/
export class UVHelper {
/**
* Calculate UV coordinates for a texture region
* 计算纹理区域的 UV 坐标
*/
static calculateUV(
imageRect: { x: number; y: number; width: number; height: number },
textureSize: { width: number; height: number }
): [number, number, number, number] {
const { x, y, width, height } = imageRect;
const { width: tw, height: th } = textureSize;
return [
x / tw, // u0
y / th, // v0
(x + width) / tw, // u1
(y + height) / th // v1
];
}
/**
* Calculate UV coordinates for a tile in a tileset
* 计算 tileset 中某个 tile 的 UV 坐标
*/
static calculateTileUV(
tileIndex: number,
tilesetInfo: {
columns: number;
tileWidth: number;
tileHeight: number;
imageWidth: number;
imageHeight: number;
margin?: number;
spacing?: number;
}
): [number, number, number, number] | null {
if (tileIndex < 0) return null;
const {
columns,
tileWidth,
tileHeight,
imageWidth,
imageHeight,
margin = 0,
spacing = 0
} = tilesetInfo;
const col = tileIndex % columns;
const row = Math.floor(tileIndex / columns);
const x = margin + col * (tileWidth + spacing);
const y = margin + row * (tileHeight + spacing);
return this.calculateUV(
{ x, y, width: tileWidth, height: tileHeight },
{ width: imageWidth, height: imageHeight }
);
}
static validateUV(uv: [number, number, number, number]): boolean {
const [u0, v0, u1, v1] = uv;
return u0 >= 0 && u0 <= 1 && u1 >= 0 && u1 <= 1 &&
v0 >= 0 && v0 <= 1 && v1 >= 0 && v1 <= 1 &&
u0 < u1 && v0 < v1;
}
static debugPrint(uv: [number, number, number, number], label?: string): void {
const prefix = label ? `[${label}] ` : '';
console.log(`${prefix}UV: [${uv.map(n => n.toFixed(4)).join(', ')}]`);
}
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsup';
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
export default defineConfig({
...runtimeOnlyPreset(),
tsconfig: 'tsconfig.build.json'
});

View File

@@ -0,0 +1,43 @@
{
"id": "audio",
"name": "@esengine/audio",
"displayName": "Audio",
"description": "Audio playback and sound effects | 音频播放和音效",
"version": "1.0.0",
"category": "Audio",
"icon": "Volume2",
"tags": [
"audio",
"sound",
"music"
],
"isCore": false,
"defaultEnabled": false,
"isEngineModule": true,
"canContainContent": true,
"platforms": [
"web",
"desktop",
"mobile"
],
"dependencies": [
"core",
"asset-system"
],
"exports": {
"components": [
"AudioSourceComponent",
"AudioListenerComponent"
],
"systems": [
"AudioSystem"
],
"other": [
"AudioClip",
"AudioMixer"
]
},
"requiresWasm": false,
"outputPath": "dist/index.js",
"pluginExport": "AudioPlugin"
}

View File

@@ -0,0 +1,46 @@
{
"name": "@esengine/audio",
"version": "1.0.0",
"description": "ECS-based audio system",
"esengine": {
"plugin": true,
"pluginExport": "AudioPlugin",
"category": "audio",
"isEnginePlugin": true
},
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/build-config": "workspace:*",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.3.3"
},
"keywords": [
"ecs",
"audio",
"sound",
"music"
],
"author": "yhh",
"license": "MIT"
}

View File

@@ -0,0 +1,28 @@
import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework';
import type { IRuntimeModule, IPlugin, ModuleManifest } from '@esengine/engine-core';
import { AudioSourceComponent } from './AudioSourceComponent';
class AudioRuntimeModule implements IRuntimeModule {
registerComponents(registry: typeof ComponentRegistryType): void {
registry.register(AudioSourceComponent);
}
}
const manifest: ModuleManifest = {
id: 'audio',
name: '@esengine/audio',
displayName: 'Audio',
version: '1.0.0',
description: '音频组件',
category: 'Audio',
isCore: false,
defaultEnabled: true,
isEngineModule: true,
dependencies: ['core', 'asset-system'],
exports: { components: ['AudioSourceComponent'] }
};
export const AudioPlugin: IPlugin = {
manifest,
runtimeModule: new AudioRuntimeModule()
};

View File

@@ -0,0 +1,43 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
@ECSComponent('AudioSource')
@Serializable({ version: 1, typeId: 'AudioSource' })
export class AudioSourceComponent extends Component {
@Serialize()
@Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' })
clip: string = '';
/** 范围 [0, 1] */
@Serialize()
@Property({ type: 'number', label: 'Volume', min: 0, max: 1, step: 0.01 })
volume: number = 1;
@Serialize()
@Property({ type: 'number', label: 'Pitch', min: 0.1, max: 3, step: 0.1 })
pitch: number = 1;
@Serialize()
@Property({ type: 'boolean', label: 'Loop' })
loop: boolean = false;
@Serialize()
@Property({ type: 'boolean', label: 'Play On Awake' })
playOnAwake: boolean = false;
@Serialize()
@Property({ type: 'boolean', label: 'Mute' })
mute: boolean = false;
/** 0 = 2D, 1 = 3D */
@Serialize()
@Property({ type: 'number', label: 'Spatial Blend', min: 0, max: 1, step: 0.1 })
spatialBlend: number = 0;
@Serialize()
@Property({ type: 'number', label: 'Min Distance' })
minDistance: number = 1;
@Serialize()
@Property({ type: 'number', label: 'Max Distance' })
maxDistance: number = 500;
}

View File

@@ -0,0 +1,2 @@
export { AudioSourceComponent } from './AudioSourceComponent';
export { AudioPlugin } from './AudioPlugin';

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../core" }
]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsup';
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
export default defineConfig({
...runtimeOnlyPreset(),
tsconfig: 'tsconfig.build.json'
});

View File

@@ -0,0 +1,49 @@
{
"name": "@esengine/behavior-tree-editor",
"version": "1.0.0",
"description": "Editor support for @esengine/behavior-tree - visual editor, inspectors, and tools",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/behavior-tree": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/editor-core": "workspace:*",
"@esengine/editor-runtime": "workspace:*",
"@esengine/node-editor": "workspace:*",
"@esengine/build-config": "workspace:*",
"lucide-react": "^0.545.0",
"react": "^18.3.1",
"zustand": "^5.0.8",
"@types/react": "^18.3.12",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.3.3"
},
"keywords": [
"ecs",
"behavior-tree",
"editor"
],
"author": "",
"license": "MIT"
}

View File

@@ -0,0 +1,29 @@
/**
* Behavior Tree Plugin Manifest
* 行为树插件清单
*/
import type { ModuleManifest } from '@esengine/editor-runtime';
/**
* 插件清单
*/
export const manifest: ModuleManifest = {
id: '@esengine/behavior-tree',
name: '@esengine/behavior-tree',
displayName: 'Behavior Tree System',
version: '1.0.0',
description: 'AI 行为树系统,支持可视化编辑和运行时执行',
category: 'AI',
icon: 'GitBranch',
isCore: false,
defaultEnabled: true,
isEngineModule: false,
canContainContent: false,
dependencies: ['engine-core'],
exports: {
components: ['BehaviorTreeRuntimeComponent'],
systems: ['BehaviorTreeExecutionSystem'],
loaders: ['BehaviorTreeLoader']
}
};

View File

@@ -0,0 +1,26 @@
import type { ServiceContainer } from '@esengine/editor-runtime';
/**
* 插件上下文
* 存储插件安装时传入的服务容器引用
*/
class PluginContextClass {
private _services: ServiceContainer | null = null;
setServices(services: ServiceContainer): void {
this._services = services;
}
getServices(): ServiceContainer {
if (!this._services) {
throw new Error('PluginContext not initialized. Make sure the plugin is properly installed.');
}
return this._services;
}
clear(): void {
this._services = null;
}
}
export const PluginContext = new PluginContextClass();

View File

@@ -0,0 +1,203 @@
import { ICommand } from './ICommand';
/**
* 命令历史记录配置
*/
export interface CommandManagerConfig {
/**
* 最大历史记录数量
*/
maxHistorySize?: number;
/**
* 是否自动合并相似命令
*/
autoMerge?: boolean;
}
/**
* 命令管理器
* 管理命令的执行、撤销、重做以及历史记录
*/
export class CommandManager {
private undoStack: ICommand[] = [];
private redoStack: ICommand[] = [];
private readonly config: Required<CommandManagerConfig>;
private isExecuting = false;
constructor(config: CommandManagerConfig = {}) {
this.config = {
maxHistorySize: config.maxHistorySize ?? 100,
autoMerge: config.autoMerge ?? true
};
}
/**
* 执行命令
*/
execute(command: ICommand): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中执行新命令');
}
this.isExecuting = true;
try {
command.execute();
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
} finally {
this.isExecuting = false;
}
}
/**
* 撤销上一个命令
*/
undo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中撤销');
}
const command = this.undoStack.pop();
if (!command) {
return;
}
this.isExecuting = true;
try {
command.undo();
this.redoStack.push(command);
} catch (error) {
this.undoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
}
}
/**
* 重做上一个被撤销的命令
*/
redo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中重做');
}
const command = this.redoStack.pop();
if (!command) {
return;
}
this.isExecuting = true;
try {
command.execute();
this.undoStack.push(command);
} catch (error) {
this.redoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
}
}
/**
* 检查是否可以撤销
*/
canUndo(): boolean {
return this.undoStack.length > 0;
}
/**
* 检查是否可以重做
*/
canRedo(): boolean {
return this.redoStack.length > 0;
}
/**
* 获取撤销栈的描述列表
*/
getUndoHistory(): string[] {
return this.undoStack.map((cmd) => cmd.getDescription());
}
/**
* 获取重做栈的描述列表
*/
getRedoHistory(): string[] {
return this.redoStack.map((cmd) => cmd.getDescription());
}
/**
* 清空所有历史记录
*/
clear(): void {
this.undoStack = [];
this.redoStack = [];
}
/**
* 批量执行命令(作为单一操作,可以一次撤销)
*/
executeBatch(commands: ICommand[]): void {
if (commands.length === 0) {
return;
}
const batchCommand = new BatchCommand(commands);
this.execute(batchCommand);
}
}
/**
* 批量命令
* 将多个命令组合为一个命令
*/
class BatchCommand implements ICommand {
constructor(private readonly commands: ICommand[]) {}
execute(): void {
for (const command of this.commands) {
command.execute();
}
}
undo(): void {
for (let i = this.commands.length - 1; i >= 0; i--) {
const command = this.commands[i];
if (command) {
command.undo();
}
}
}
getDescription(): string {
return `批量操作 (${this.commands.length} 个命令)`;
}
canMergeWith(): boolean {
return false;
}
mergeWith(): ICommand {
throw new Error('批量命令不支持合并');
}
}

View File

@@ -0,0 +1,31 @@
/**
* 命令接口
* 实现命令模式,支持撤销/重做功能
*/
export interface ICommand {
/**
* 执行命令
*/
execute(): void;
/**
* 撤销命令
*/
undo(): void;
/**
* 获取命令描述(用于显示历史记录)
*/
getDescription(): string;
/**
* 检查命令是否可以合并
* 用于优化撤销/重做历史,例如连续的移动操作可以合并为一个
*/
canMergeWith(other: ICommand): boolean;
/**
* 与另一个命令合并
*/
mergeWith(other: ICommand): ICommand;
}

View File

@@ -0,0 +1,17 @@
import { BehaviorTree } from '../../domain/models/BehaviorTree';
/**
* 行为树状态接口
* 命令通过此接口操作状态
*/
export interface ITreeState {
/**
* 获取当前行为树
*/
getTree(): BehaviorTree;
/**
* 设置行为树
*/
setTree(tree: BehaviorTree): void;
}

View File

@@ -0,0 +1,36 @@
import { Connection } from '../../../domain/models/Connection';
import { BaseCommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState';
/**
* 添加连接命令
*/
export class AddConnectionCommand extends BaseCommand {
constructor(
private readonly state: ITreeState,
private readonly connection: Connection
) {
super();
}
execute(): void {
const tree = this.state.getTree();
const newTree = tree.addConnection(this.connection);
this.state.setTree(newTree);
}
undo(): void {
const tree = this.state.getTree();
const newTree = tree.removeConnection(
this.connection.from,
this.connection.to,
this.connection.fromProperty,
this.connection.toProperty
);
this.state.setTree(newTree);
}
getDescription(): string {
return `添加连接: ${this.connection.from} -> ${this.connection.to}`;
}
}

View File

@@ -0,0 +1,34 @@
import { Node } from '../../../domain/models/Node';
import { BaseCommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState';
/**
* 创建节点命令
*/
export class CreateNodeCommand extends BaseCommand {
private createdNodeId: string;
constructor(
private readonly state: ITreeState,
private readonly node: Node
) {
super();
this.createdNodeId = node.id;
}
execute(): void {
const tree = this.state.getTree();
const newTree = tree.addNode(this.node);
this.state.setTree(newTree);
}
undo(): void {
const tree = this.state.getTree();
const newTree = tree.removeNode(this.createdNodeId);
this.state.setTree(newTree);
}
getDescription(): string {
return `创建节点: ${this.node.template.displayName}`;
}
}

View File

@@ -0,0 +1,38 @@
import { Node } from '../../../domain/models/Node';
import { BaseCommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState';
/**
* 删除节点命令
*/
export class DeleteNodeCommand extends BaseCommand {
private deletedNode: Node | null = null;
constructor(
private readonly state: ITreeState,
private readonly nodeId: string
) {
super();
}
execute(): void {
const tree = this.state.getTree();
this.deletedNode = tree.getNode(this.nodeId);
const newTree = tree.removeNode(this.nodeId);
this.state.setTree(newTree);
}
undo(): void {
if (!this.deletedNode) {
throw new Error('无法撤销:未保存已删除的节点');
}
const tree = this.state.getTree();
const newTree = tree.addNode(this.deletedNode);
this.state.setTree(newTree);
}
getDescription(): string {
return `删除节点: ${this.deletedNode?.template.displayName ?? this.nodeId}`;
}
}

View File

@@ -0,0 +1,74 @@
import { Position } from '../../../domain/value-objects/Position';
import { BaseCommand, ICommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState';
/**
* 移动节点命令
* 支持合并连续的移动操作
*/
export class MoveNodeCommand extends BaseCommand {
private oldPosition: Position;
constructor(
private readonly state: ITreeState,
private readonly nodeId: string,
private readonly newPosition: Position
) {
super();
const tree = this.state.getTree();
const node = tree.getNode(nodeId);
this.oldPosition = node.position;
}
execute(): void {
const tree = this.state.getTree();
const newTree = tree.updateNode(this.nodeId, (node) =>
node.moveToPosition(this.newPosition)
);
this.state.setTree(newTree);
}
undo(): void {
const tree = this.state.getTree();
const newTree = tree.updateNode(this.nodeId, (node) =>
node.moveToPosition(this.oldPosition)
);
this.state.setTree(newTree);
}
getDescription(): string {
return `移动节点: ${this.nodeId}`;
}
/**
* 移动命令可以合并
*/
canMergeWith(other: ICommand): boolean {
if (!(other instanceof MoveNodeCommand)) {
return false;
}
return this.nodeId === other.nodeId;
}
/**
* 合并移动命令
* 保留初始位置,更新最终位置
*/
mergeWith(other: ICommand): ICommand {
if (!(other instanceof MoveNodeCommand)) {
throw new Error('只能与 MoveNodeCommand 合并');
}
if (this.nodeId !== other.nodeId) {
throw new Error('只能合并同一节点的移动命令');
}
const merged = new MoveNodeCommand(
this.state,
this.nodeId,
other.newPosition
);
merged.oldPosition = this.oldPosition;
return merged;
}
}

View File

@@ -0,0 +1,50 @@
import { Connection } from '../../../domain/models/Connection';
import { BaseCommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState';
/**
* 移除连接命令
*/
export class RemoveConnectionCommand extends BaseCommand {
private removedConnection: Connection | null = null;
constructor(
private readonly state: ITreeState,
private readonly from: string,
private readonly to: string,
private readonly fromProperty?: string,
private readonly toProperty?: string
) {
super();
}
execute(): void {
const tree = this.state.getTree();
const connection = tree.connections.find((c) =>
c.matches(this.from, this.to, this.fromProperty, this.toProperty)
);
if (!connection) {
throw new Error(`连接不存在: ${this.from} -> ${this.to}`);
}
this.removedConnection = connection;
const newTree = tree.removeConnection(this.from, this.to, this.fromProperty, this.toProperty);
this.state.setTree(newTree);
}
undo(): void {
if (!this.removedConnection) {
throw new Error('无法撤销:未保存已删除的连接');
}
const tree = this.state.getTree();
const newTree = tree.addConnection(this.removedConnection);
this.state.setTree(newTree);
}
getDescription(): string {
return `移除连接: ${this.from} -> ${this.to}`;
}
}

View File

@@ -0,0 +1,40 @@
import { BaseCommand } from '@esengine/editor-runtime';
import { ITreeState } from '../ITreeState';
/**
* 更新节点数据命令
*/
export class UpdateNodeDataCommand extends BaseCommand {
private oldData: Record<string, unknown>;
constructor(
private readonly state: ITreeState,
private readonly nodeId: string,
private readonly newData: Record<string, unknown>
) {
super();
const tree = this.state.getTree();
const node = tree.getNode(nodeId);
this.oldData = node.data;
}
execute(): void {
const tree = this.state.getTree();
const newTree = tree.updateNode(this.nodeId, (node) =>
node.updateData(this.newData)
);
this.state.setTree(newTree);
}
undo(): void {
const tree = this.state.getTree();
const newTree = tree.updateNode(this.nodeId, (node) =>
node.updateData(this.oldData)
);
this.state.setTree(newTree);
}
getDescription(): string {
return `更新节点数据: ${this.nodeId}`;
}
}

View File

@@ -0,0 +1,6 @@
export { CreateNodeCommand } from './CreateNodeCommand';
export { DeleteNodeCommand } from './DeleteNodeCommand';
export { AddConnectionCommand } from './AddConnectionCommand';
export { RemoveConnectionCommand } from './RemoveConnectionCommand';
export { MoveNodeCommand } from './MoveNodeCommand';
export { UpdateNodeDataCommand } from './UpdateNodeDataCommand';

View File

@@ -0,0 +1,253 @@
import { Node as BehaviorTreeNode } from '../../domain/models/Node';
import { Connection } from '../../domain/models/Connection';
import { ExecutionLog } from '../../utils/BehaviorTreeExecutor';
import { BlackboardValue } from '../../domain/models/Blackboard';
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('ExecutionHooks');
type BlackboardVariables = Record<string, BlackboardValue>;
type NodeExecutionStatus = 'idle' | 'running' | 'success' | 'failure';
export interface ExecutionContext {
nodes: BehaviorTreeNode[];
connections: Connection[];
blackboardVariables: BlackboardVariables;
rootNodeId: string;
tickCount: number;
}
export interface NodeStatusChangeEvent {
nodeId: string;
status: NodeExecutionStatus;
previousStatus?: NodeExecutionStatus;
timestamp: number;
}
export interface IExecutionHooks {
beforePlay?(context: ExecutionContext): void | Promise<void>;
afterPlay?(context: ExecutionContext): void | Promise<void>;
beforePause?(): void | Promise<void>;
afterPause?(): void | Promise<void>;
beforeResume?(): void | Promise<void>;
afterResume?(): void | Promise<void>;
beforeStop?(): void | Promise<void>;
afterStop?(): void | Promise<void>;
beforeStep?(deltaTime: number): void | Promise<void>;
afterStep?(deltaTime: number): void | Promise<void>;
onTick?(tickCount: number, deltaTime: number): void | Promise<void>;
onNodeStatusChange?(event: NodeStatusChangeEvent): void | Promise<void>;
onExecutionComplete?(logs: ExecutionLog[]): void | Promise<void>;
onBlackboardUpdate?(variables: BlackboardVariables): void | Promise<void>;
onError?(error: Error, context?: string): void | Promise<void>;
}
export class ExecutionHooksManager {
private hooks: Set<IExecutionHooks> = new Set();
register(hook: IExecutionHooks): void {
this.hooks.add(hook);
}
unregister(hook: IExecutionHooks): void {
this.hooks.delete(hook);
}
clear(): void {
this.hooks.clear();
}
async triggerBeforePlay(context: ExecutionContext): Promise<void> {
for (const hook of this.hooks) {
if (hook.beforePlay) {
try {
await hook.beforePlay(context);
} catch (error) {
logger.error('Error in beforePlay hook:', error);
}
}
}
}
async triggerAfterPlay(context: ExecutionContext): Promise<void> {
for (const hook of this.hooks) {
if (hook.afterPlay) {
try {
await hook.afterPlay(context);
} catch (error) {
logger.error('Error in afterPlay hook:', error);
}
}
}
}
async triggerBeforePause(): Promise<void> {
for (const hook of this.hooks) {
if (hook.beforePause) {
try {
await hook.beforePause();
} catch (error) {
logger.error('Error in beforePause hook:', error);
}
}
}
}
async triggerAfterPause(): Promise<void> {
for (const hook of this.hooks) {
if (hook.afterPause) {
try {
await hook.afterPause();
} catch (error) {
logger.error('Error in afterPause hook:', error);
}
}
}
}
async triggerBeforeResume(): Promise<void> {
for (const hook of this.hooks) {
if (hook.beforeResume) {
try {
await hook.beforeResume();
} catch (error) {
logger.error('Error in beforeResume hook:', error);
}
}
}
}
async triggerAfterResume(): Promise<void> {
for (const hook of this.hooks) {
if (hook.afterResume) {
try {
await hook.afterResume();
} catch (error) {
logger.error('Error in afterResume hook:', error);
}
}
}
}
async triggerBeforeStop(): Promise<void> {
for (const hook of this.hooks) {
if (hook.beforeStop) {
try {
await hook.beforeStop();
} catch (error) {
logger.error('Error in beforeStop hook:', error);
}
}
}
}
async triggerAfterStop(): Promise<void> {
for (const hook of this.hooks) {
if (hook.afterStop) {
try {
await hook.afterStop();
} catch (error) {
logger.error('Error in afterStop hook:', error);
}
}
}
}
async triggerBeforeStep(deltaTime: number): Promise<void> {
for (const hook of this.hooks) {
if (hook.beforeStep) {
try {
await hook.beforeStep(deltaTime);
} catch (error) {
logger.error('Error in beforeStep hook:', error);
}
}
}
}
async triggerAfterStep(deltaTime: number): Promise<void> {
for (const hook of this.hooks) {
if (hook.afterStep) {
try {
await hook.afterStep(deltaTime);
} catch (error) {
logger.error('Error in afterStep hook:', error);
}
}
}
}
async triggerOnTick(tickCount: number, deltaTime: number): Promise<void> {
for (const hook of this.hooks) {
if (hook.onTick) {
try {
await hook.onTick(tickCount, deltaTime);
} catch (error) {
logger.error('Error in onTick hook:', error);
}
}
}
}
async triggerOnNodeStatusChange(event: NodeStatusChangeEvent): Promise<void> {
for (const hook of this.hooks) {
if (hook.onNodeStatusChange) {
try {
await hook.onNodeStatusChange(event);
} catch (error) {
logger.error('Error in onNodeStatusChange hook:', error);
}
}
}
}
async triggerOnExecutionComplete(logs: ExecutionLog[]): Promise<void> {
for (const hook of this.hooks) {
if (hook.onExecutionComplete) {
try {
await hook.onExecutionComplete(logs);
} catch (error) {
logger.error('Error in onExecutionComplete hook:', error);
}
}
}
}
async triggerOnBlackboardUpdate(variables: BlackboardVariables): Promise<void> {
for (const hook of this.hooks) {
if (hook.onBlackboardUpdate) {
try {
await hook.onBlackboardUpdate(variables);
} catch (error) {
logger.error('Error in onBlackboardUpdate hook:', error);
}
}
}
}
async triggerOnError(error: Error, context?: string): Promise<void> {
for (const hook of this.hooks) {
if (hook.onError) {
try {
await hook.onError(error, context);
} catch (err) {
logger.error('Error in onError hook:', err);
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
import { BlackboardValue } from '../../domain/models/Blackboard';
type BlackboardVariables = Record<string, BlackboardValue>;
export class BlackboardManager {
private initialVariables: BlackboardVariables = {};
private currentVariables: BlackboardVariables = {};
setInitialVariables(variables: BlackboardVariables): void {
this.initialVariables = JSON.parse(JSON.stringify(variables)) as BlackboardVariables;
}
getInitialVariables(): BlackboardVariables {
return { ...this.initialVariables };
}
setCurrentVariables(variables: BlackboardVariables): void {
this.currentVariables = { ...variables };
}
getCurrentVariables(): BlackboardVariables {
return { ...this.currentVariables };
}
updateVariable(key: string, value: BlackboardValue): void {
this.currentVariables[key] = value;
}
restoreInitialVariables(): BlackboardVariables {
this.currentVariables = { ...this.initialVariables };
return this.getInitialVariables();
}
hasChanges(): boolean {
return JSON.stringify(this.currentVariables) !== JSON.stringify(this.initialVariables);
}
clear(): void {
this.initialVariables = {};
this.currentVariables = {};
}
}

View File

@@ -0,0 +1,552 @@
import { BehaviorTreeExecutor, ExecutionStatus, ExecutionLog } from '../../utils/BehaviorTreeExecutor';
import { BehaviorTreeNode, Connection } from '../../stores';
import type { NodeExecutionStatus } from '../../stores';
import { BlackboardValue } from '../../domain/models/Blackboard';
import { DOMCache } from '../../utils/DOMCache';
import { EditorEventBus, EditorEvent } from '../../infrastructure/events/EditorEventBus';
import { ExecutionHooksManager } from '../interfaces/IExecutionHooks';
import type { Breakpoint } from '../../types/Breakpoint';
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('ExecutionController');
export type ExecutionMode = 'idle' | 'running' | 'paused';
type BlackboardVariables = Record<string, BlackboardValue>;
interface ExecutionControllerConfig {
rootNodeId: string;
projectPath: string | null;
onLogsUpdate: (logs: ExecutionLog[]) => void;
onBlackboardUpdate: (variables: BlackboardVariables) => void;
onTickCountUpdate: (count: number) => void;
onExecutionStatusUpdate: (statuses: Map<string, NodeExecutionStatus>, orders: Map<string, number>) => void;
onBreakpointHit?: (nodeId: string, nodeName: string) => void;
eventBus?: EditorEventBus;
hooksManager?: ExecutionHooksManager;
}
export class ExecutionController {
private executor: BehaviorTreeExecutor | null = null;
private mode: ExecutionMode = 'idle';
private animationFrameId: number | null = null;
private lastTickTime: number = 0;
private speed: number = 1.0;
private tickCount: number = 0;
private domCache: DOMCache = new DOMCache();
private eventBus?: EditorEventBus;
private hooksManager?: ExecutionHooksManager;
private config: ExecutionControllerConfig;
private currentNodes: BehaviorTreeNode[] = [];
private currentConnections: Connection[] = [];
private currentBlackboard: BlackboardVariables = {};
private stepByStepMode: boolean = true;
private pendingStatusUpdates: ExecutionStatus[] = [];
private currentlyDisplayedIndex: number = 0;
private lastStepTime: number = 0;
private stepInterval: number = 200;
// 存储断点回调的引用
private breakpointCallback: ((nodeId: string, nodeName: string) => void) | null = null;
constructor(config: ExecutionControllerConfig) {
this.config = config;
this.executor = new BehaviorTreeExecutor();
this.eventBus = config.eventBus;
this.hooksManager = config.hooksManager;
}
getMode(): ExecutionMode {
return this.mode;
}
getTickCount(): number {
return this.tickCount;
}
getSpeed(): number {
return this.speed;
}
setSpeed(speed: number): void {
this.speed = speed;
this.lastTickTime = 0;
}
async play(
nodes: BehaviorTreeNode[],
blackboardVariables: BlackboardVariables,
connections: Connection[]
): Promise<void> {
if (this.mode === 'running') return;
this.currentNodes = nodes;
this.currentConnections = connections;
this.currentBlackboard = blackboardVariables;
const context = {
nodes,
connections,
blackboardVariables,
rootNodeId: this.config.rootNodeId,
tickCount: 0
};
try {
await this.hooksManager?.triggerBeforePlay(context);
this.mode = 'running';
this.tickCount = 0;
this.lastTickTime = 0;
if (!this.executor) {
this.executor = new BehaviorTreeExecutor();
}
this.executor.buildTree(
nodes,
this.config.rootNodeId,
blackboardVariables,
connections,
this.handleExecutionStatusUpdate.bind(this)
);
// 设置断点触发回调(使用存储的回调)
if (this.breakpointCallback) {
this.executor.setBreakpointCallback(this.breakpointCallback);
}
this.executor.start();
this.animationFrameId = requestAnimationFrame(this.tickLoop.bind(this));
this.eventBus?.emit(EditorEvent.EXECUTION_STARTED, context);
await this.hooksManager?.triggerAfterPlay(context);
} catch (error) {
console.error('Error in play:', error);
await this.hooksManager?.triggerOnError(error as Error, 'play');
throw error;
}
}
async pause(): Promise<void> {
try {
if (this.mode === 'running') {
await this.hooksManager?.triggerBeforePause();
this.mode = 'paused';
if (this.executor) {
this.executor.pause();
}
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
this.eventBus?.emit(EditorEvent.EXECUTION_PAUSED);
await this.hooksManager?.triggerAfterPause();
} else if (this.mode === 'paused') {
await this.hooksManager?.triggerBeforeResume();
this.mode = 'running';
this.lastTickTime = 0;
if (this.executor) {
this.executor.resume();
}
this.animationFrameId = requestAnimationFrame(this.tickLoop.bind(this));
this.eventBus?.emit(EditorEvent.EXECUTION_RESUMED);
await this.hooksManager?.triggerAfterResume();
}
} catch (error) {
console.error('Error in pause/resume:', error);
await this.hooksManager?.triggerOnError(error as Error, 'pause');
throw error;
}
}
async stop(): Promise<void> {
try {
await this.hooksManager?.triggerBeforeStop();
this.mode = 'idle';
this.tickCount = 0;
this.lastTickTime = 0;
this.lastStepTime = 0;
this.pendingStatusUpdates = [];
this.currentlyDisplayedIndex = 0;
this.domCache.clearAllStatusTimers();
this.domCache.clearStatusCache();
this.config.onExecutionStatusUpdate(new Map(), new Map());
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
if (this.executor) {
this.executor.stop();
}
this.eventBus?.emit(EditorEvent.EXECUTION_STOPPED);
await this.hooksManager?.triggerAfterStop();
} catch (error) {
console.error('Error in stop:', error);
await this.hooksManager?.triggerOnError(error as Error, 'stop');
throw error;
}
}
async reset(): Promise<void> {
await this.stop();
if (this.executor) {
this.executor.cleanup();
}
}
async step(): Promise<void> {
if (this.mode === 'running') {
await this.pause();
}
if (this.mode === 'idle') {
if (!this.currentNodes.length) {
logger.warn('No tree loaded for step execution');
return;
}
if (!this.executor) {
this.executor = new BehaviorTreeExecutor();
}
this.executor.buildTree(
this.currentNodes,
this.config.rootNodeId,
this.currentBlackboard,
this.currentConnections,
this.handleExecutionStatusUpdate.bind(this)
);
if (this.breakpointCallback) {
this.executor.setBreakpointCallback(this.breakpointCallback);
}
this.executor.start();
}
try {
await this.hooksManager?.triggerBeforeStep?.(0);
if (this.stepByStepMode && this.pendingStatusUpdates.length > 0) {
if (this.currentlyDisplayedIndex < this.pendingStatusUpdates.length) {
this.displayNextNode();
} else {
this.executeSingleTick();
}
} else {
this.executeSingleTick();
}
this.eventBus?.emit(EditorEvent.EXECUTION_STEPPED, { tickCount: this.tickCount });
await this.hooksManager?.triggerAfterStep?.(0);
} catch (error) {
console.error('Error in step:', error);
await this.hooksManager?.triggerOnError(error as Error, 'step');
}
this.mode = 'paused';
}
private executeSingleTick(): void {
if (!this.executor) return;
const deltaTime = 16.67 / 1000;
this.executor.tick(deltaTime);
this.tickCount = this.executor.getTickCount();
this.config.onTickCountUpdate(this.tickCount);
}
updateBlackboardVariable(key: string, value: BlackboardValue): void {
if (this.executor && this.mode !== 'idle') {
this.executor.updateBlackboardVariable(key, value);
}
}
getBlackboardVariables(): BlackboardVariables {
if (this.executor) {
return this.executor.getBlackboardVariables();
}
return {};
}
updateNodes(nodes: BehaviorTreeNode[]): void {
if (this.mode === 'idle' || !this.executor) {
return;
}
this.currentNodes = nodes;
this.executor.buildTree(
nodes,
this.config.rootNodeId,
this.currentBlackboard,
this.currentConnections,
this.handleExecutionStatusUpdate.bind(this)
);
// 设置断点触发回调(使用存储的回调)
if (this.breakpointCallback) {
this.executor.setBreakpointCallback(this.breakpointCallback);
}
this.executor.start();
}
clearDOMCache(): void {
this.domCache.clearAll();
}
destroy(): void {
this.stop();
if (this.executor) {
this.executor.destroy();
this.executor = null;
}
}
private tickLoop(currentTime: number): void {
if (this.mode !== 'running') {
return;
}
if (!this.executor) {
return;
}
if (this.stepByStepMode) {
this.handleStepByStepExecution(currentTime);
} else {
this.handleNormalExecution(currentTime);
}
this.animationFrameId = requestAnimationFrame(this.tickLoop.bind(this));
}
private handleNormalExecution(currentTime: number): void {
const baseTickInterval = 16.67;
const scaledTickInterval = baseTickInterval / this.speed;
if (this.lastTickTime === 0) {
this.lastTickTime = currentTime;
}
const elapsed = currentTime - this.lastTickTime;
if (elapsed >= scaledTickInterval) {
const deltaTime = baseTickInterval / 1000;
this.executor!.tick(deltaTime);
this.tickCount = this.executor!.getTickCount();
this.config.onTickCountUpdate(this.tickCount);
this.lastTickTime = currentTime;
}
}
private handleStepByStepExecution(currentTime: number): void {
if (this.lastStepTime === 0) {
this.lastStepTime = currentTime;
}
const stepElapsed = currentTime - this.lastStepTime;
const actualStepInterval = this.stepInterval / this.speed;
if (stepElapsed >= actualStepInterval) {
if (this.currentlyDisplayedIndex < this.pendingStatusUpdates.length) {
this.displayNextNode();
this.lastStepTime = currentTime;
} else {
if (this.lastTickTime === 0) {
this.lastTickTime = currentTime;
}
const tickElapsed = currentTime - this.lastTickTime;
const baseTickInterval = 16.67;
const scaledTickInterval = baseTickInterval / this.speed;
if (tickElapsed >= scaledTickInterval) {
const deltaTime = baseTickInterval / 1000;
this.executor!.tick(deltaTime);
this.tickCount = this.executor!.getTickCount();
this.config.onTickCountUpdate(this.tickCount);
this.lastTickTime = currentTime;
}
}
}
}
private displayNextNode(): void {
if (this.currentlyDisplayedIndex >= this.pendingStatusUpdates.length) {
return;
}
const statusesToDisplay = this.pendingStatusUpdates.slice(0, this.currentlyDisplayedIndex + 1);
const currentNode = this.pendingStatusUpdates[this.currentlyDisplayedIndex];
if (!currentNode) {
return;
}
const statusMap = new Map<string, NodeExecutionStatus>();
const orderMap = new Map<string, number>();
statusesToDisplay.forEach((s) => {
statusMap.set(s.nodeId, s.status);
if (s.executionOrder !== undefined) {
orderMap.set(s.nodeId, s.executionOrder);
}
});
const nodeName = this.currentNodes.find((n) => n.id === currentNode.nodeId)?.template.displayName || 'Unknown';
logger.info(`[StepByStep] Displaying ${this.currentlyDisplayedIndex + 1}/${this.pendingStatusUpdates.length} | ${nodeName} | Order: ${currentNode.executionOrder} | ID: ${currentNode.nodeId}`);
this.config.onExecutionStatusUpdate(statusMap, orderMap);
this.currentlyDisplayedIndex++;
}
private handleExecutionStatusUpdate(
statuses: ExecutionStatus[],
logs: ExecutionLog[],
runtimeBlackboardVars?: BlackboardVariables
): void {
this.config.onLogsUpdate([...logs]);
if (runtimeBlackboardVars) {
this.config.onBlackboardUpdate(runtimeBlackboardVars);
}
if (this.stepByStepMode) {
const statusesWithOrder = statuses.filter((s) => s.executionOrder !== undefined);
if (statusesWithOrder.length > 0) {
const minOrder = Math.min(...statusesWithOrder.map((s) => s.executionOrder!));
if (minOrder === 1 || this.pendingStatusUpdates.length === 0) {
this.pendingStatusUpdates = statusesWithOrder.sort((a, b) =>
(a.executionOrder || 0) - (b.executionOrder || 0)
);
this.currentlyDisplayedIndex = 0;
this.lastStepTime = 0;
} else {
const maxExistingOrder = this.pendingStatusUpdates.length > 0
? Math.max(...this.pendingStatusUpdates.map((s) => s.executionOrder || 0))
: 0;
const newStatuses = statusesWithOrder.filter((s) =>
(s.executionOrder || 0) > maxExistingOrder
);
if (newStatuses.length > 0) {
logger.info(`[StepByStep] Appending ${newStatuses.length} new nodes, orders:`, newStatuses.map((s) => s.executionOrder));
this.pendingStatusUpdates = [
...this.pendingStatusUpdates,
...newStatuses
].sort((a, b) => (a.executionOrder || 0) - (b.executionOrder || 0));
}
}
}
} else {
const statusMap = new Map<string, NodeExecutionStatus>();
const orderMap = new Map<string, number>();
statuses.forEach((s) => {
statusMap.set(s.nodeId, s.status);
if (s.executionOrder !== undefined) {
orderMap.set(s.nodeId, s.executionOrder);
}
});
this.config.onExecutionStatusUpdate(statusMap, orderMap);
}
}
private updateConnectionStyles(
statusMap: Record<string, NodeExecutionStatus>,
connections?: Connection[]
): void {
if (!connections) return;
connections.forEach((conn) => {
const connKey = `${conn.from}-${conn.to}`;
const pathElement = this.domCache.getConnection(connKey);
if (!pathElement) {
return;
}
const fromStatus = statusMap[conn.from];
const toStatus = statusMap[conn.to];
const isActive = fromStatus === 'running' || toStatus === 'running';
if (conn.connectionType === 'property') {
this.domCache.setConnectionAttribute(connKey, 'stroke', '#9c27b0');
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '2');
} else if (isActive) {
this.domCache.setConnectionAttribute(connKey, 'stroke', '#ffa726');
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '3');
} else {
const isExecuted = this.domCache.hasNodeClass(conn.from, 'executed') &&
this.domCache.hasNodeClass(conn.to, 'executed');
if (isExecuted) {
this.domCache.setConnectionAttribute(connKey, 'stroke', '#4caf50');
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '2.5');
} else {
this.domCache.setConnectionAttribute(connKey, 'stroke', '#0e639c');
this.domCache.setConnectionAttribute(connKey, 'stroke-width', '2');
}
}
});
}
setConnections(connections: Connection[]): void {
if (this.mode !== 'idle') {
const currentStatuses: Record<string, NodeExecutionStatus> = {};
connections.forEach((conn) => {
const fromStatus = this.domCache.getLastStatus(conn.from);
const toStatus = this.domCache.getLastStatus(conn.to);
if (fromStatus) currentStatuses[conn.from] = fromStatus;
if (toStatus) currentStatuses[conn.to] = toStatus;
});
this.updateConnectionStyles(currentStatuses, connections);
}
}
setBreakpoints(breakpoints: Map<string, Breakpoint>): void {
if (this.executor) {
this.executor.setBreakpoints(breakpoints);
}
}
/**
* 设置断点触发回调
*/
setBreakpointCallback(callback: (nodeId: string, nodeName: string) => void): void {
this.breakpointCallback = callback;
// 如果 executor 已存在,立即设置
if (this.executor) {
this.executor.setBreakpointCallback(callback);
}
}
}

Some files were not shown because too many files have changed in this diff Show More