Compare commits

...

210 Commits

Author SHA1 Message Date
github-actions[bot]
c7f8208b6f chore: release packages (#358)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 10:31:43 +08:00
yhh
5131ec3c52 chore: update pnpm-lock.yaml 2025-12-27 10:28:35 +08:00
yhh
7d74623710 fix(core): 配置 publishConfig.directory 确保从 dist 目录发布 2025-12-27 10:27:00 +08:00
github-actions[bot]
044463dd5f chore: release packages (#357)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 09:55:30 +08:00
YHH
ce2db4e48a fix(core): 修复 World cleanup 在打包环境下的兼容性问题 (#356)
- 使用 forEach 替代 spread + for...of 解构模式,避免某些打包工具转换后的兼容性问题
- 重构 World 和 WorldManager 类,提升代码质量
- 提取默认配置为常量
- 统一双语注释格式
2025-12-27 09:51:04 +08:00
github-actions[bot]
0a88c6f2fc chore: release packages (#355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-27 00:20:04 +08:00
yhh
b0b95c60b4 fix: 从 ignore 列表移除 network-server 以支持版本发布 2025-12-27 00:17:44 +08:00
yhh
683ac7a7d4 Merge branch 'master' of https://github.com/esengine/esengine 2025-12-27 00:16:12 +08:00
YHH
1e240e86f2 feat(cli): 增强 Node.js 服务端适配器 (#354)
* docs(network): 添加网络模块文档和 CLI 支持

- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表

* feat(cli): 增强 Node.js 服务端适配器

- 添加 @esengine/network-server 依赖支持
- 生成完整的 ECS 游戏服务器项目结构
- 修复 network-server 包支持 ESM/CJS 双格式
- 添加 ws@8.18.0 解决 Node.js 24 兼容性问题
- 组件使用 @ECSComponent 装饰器注册
- tsconfig 启用 experimentalDecorators
2025-12-27 00:13:58 +08:00
yhh
4d6c2fe7ff Merge branch 'master' of https://github.com/esengine/esengine 2025-12-26 23:21:17 +08:00
github-actions[bot]
67c06720c5 chore: release packages (#353)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 23:17:14 +08:00
YHH
33e98b9a75 fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑 (#352)
* docs(network): 添加网络模块文档和 CLI 支持

- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表

* fix(cli): 修复 Cocos Creator 3.x 项目检测逻辑

- 重构检测代码,提取通用辅助函数
- 优先检查 package.json 中的 creator.version 字段
- 添加 .creator 和 settings 目录检测
- 使用 getMajorVersion 统一版本号解析

* chore: add changeset
2025-12-26 23:14:23 +08:00
yhh
a42f2412d7 Merge branch 'master' of https://github.com/esengine/esengine 2025-12-26 23:04:47 +08:00
yhh
fdb19a33fb docs(network): 添加网络模块文档和 CLI 支持
- 添加中英文网络模块文档
- 将 network、network-protocols、network-server 加入 CLI 模块列表
2025-12-26 23:01:07 +08:00
github-actions[bot]
1e31e9101b chore: release packages (#351)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 22:32:12 +08:00
yhh
d66c18041e fix(spatial): 修复 GridAOI 可见性更新问题
- 修复 addObserver 时现有观察者无法检测到新实体的问题
- 修复实体远距离移动时观察者可见性未正确更新的问题
- 重构 demos 抽取公共测试工具
2025-12-26 22:23:03 +08:00
yhh
881ffad3bc feat(tools): 添加 CLI 模块管理命令和文档验证 demos
- CLI 新增 list/add/remove 命令管理项目模块
- 创建 demos 包验证模块文档正确性
- 包含 Timer/FSM/Pathfinding/Procgen/Spatial 5个模块的完整测试
2025-12-26 22:09:01 +08:00
YHH
4a16e30794 docs(modules): 添加框架模块文档 (#350)
* docs(modules): 添加框架模块文档

添加以下模块的完整文档:
- FSM (状态机): 状态定义、转换条件、优先级、事件监听
- Timer (定时器): 定时器调度、冷却系统、服务令牌
- Spatial (空间索引): GridSpatialIndex、AOI 兴趣区域管理
- Pathfinding (寻路): A* 算法、网格地图、导航网格、路径平滑
- Procgen (程序化生成): 噪声函数、种子随机数、加权随机

所有文档均基于实际源码 API 编写,包含:
- 快速开始示例
- 完整 API 参考
- 实际使用案例
- 蓝图节点说明
- 最佳实践建议

* docs(modules): 添加 Blueprint 模块文档和所有模块英文版

新增中文文档:
- Blueprint (蓝图可视化脚本): VM、自定义节点、组合系统、触发器

新增英文文档 (docs/en/modules/):
- FSM: State machine API, transitions, ECS integration
- Timer: Timers, cooldowns, service tokens
- Spatial: Grid spatial index, AOI management
- Pathfinding: A*, grid map, NavMesh, path smoothing
- Procgen: Noise functions, seeded random, weighted random
- Blueprint: Visual scripting, custom nodes, composition

所有文档均基于实际源码 API 编写。
2025-12-26 20:02:21 +08:00
YHH
76691cc198 docs: 重构文档结构,添加独立模块区域 (#349)
* docs: 重构文档结构,添加独立模块区域

- 新增 /modules/ 目录用于功能模块文档
- 移动 behavior-tree 从 /guide/ 到 /modules/
- 添加模块总览页面
- 更新导航栏添加"模块"入口
- 更新侧边栏:模块区域独立侧边栏
- 更新 i18n 配置支持新模块

* style(docs): 提高文字对比度
2025-12-26 19:15:08 +08:00
github-actions[bot]
27b9e174eb chore: release packages (#348)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 18:16:57 +08:00
YHH
ede440d277 chore: changeset for framework package configs (#347)
* chore: add changeset for framework package configs

* fix(docs): 修复 VitePress 配置中的包路径
2025-12-26 18:13:12 +08:00
YHH
5cb83f0743 fix(framework): 补充包配置 peerDeps/repository/keywords (#346)
* fix(framework): 补充 peerDependencies, repository 和 keywords 配置

- fsm: 添加 peerDeps, repository, keywords
- timer: 添加 peerDeps, repository, keywords
- spatial: 添加 peerDeps, repository, keywords
- procgen: 添加 peerDeps, repository, keywords
- pathfinding: 移除重复的 dependencies,添加 repository, keywords

* chore: update pnpm-lock.yaml
2025-12-26 18:05:37 +08:00
yhh
7cbf92b8c7 fix(docs): 修复 tsconfig.json 中的 references 路径 2025-12-26 17:49:40 +08:00
YHH
a049bbe2f5 fix: add private:true to packages not meant for npm (#345)
* fix(ci): run on all PRs with conditional skip

- Remove paths filter from pull_request trigger
- Add check-changes job to detect code changes
- Skip full CI if no code files changed
- Satisfies branch protection while avoiding unnecessary builds

* fix: add private:true to packages not meant for npm

Prevents changesets from trying to publish internal packages:
- engine/* (8 packages)
- rendering/* (8 packages)
- physics/* (2 packages)
- streaming/* (1 package)
- tools/sdk, tools/worker-generator
2025-12-26 17:45:00 +08:00
github-actions[bot]
ec72df7af5 chore: release packages (#343)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: YHH <359807859@qq.com>
2025-12-26 17:18:01 +08:00
YHH
9327c1cef5 fix(ci): run on all PRs with conditional skip (#344)
- Remove paths filter from pull_request trigger
- Add check-changes job to detect code changes
- Skip full CI if no code files changed
- Satisfies branch protection while avoiding unnecessary builds
2025-12-26 17:15:02 +08:00
YHH
da5bf2116a fix(changesets): remove network-protocols and build-config from ignore (#342)
* fix(changesets): remove network-protocols and build-config from ignore

These packages are dependencies of non-ignored packages:
- @esengine/network depends on @esengine/network-protocols
- Multiple packages depend on @esengine/build-config (private, won't publish)

* fix(ci): add .changeset to CI trigger paths
2025-12-26 17:05:41 +08:00
YHH
67e97f89c6 fix(ci): update typedoc path and add workflow_dispatch (#341)
* fix: update Laya examples, add CLI docs, fix changesets workflow

- Update Laya examples to use Laya 3.x Script pattern (@regClass)
- Add CLI tool quick start section to README (npx @esengine/cli init)
- Fix changesets workflow to only build framework packages
- Remove unnecessary Rust/WASM build steps from changesets workflow
- Remove redundant 'pnpm build' from changeset:publish script

* docs: add CLI documentation and update Laya examples

- Add CLI quick start section to getting-started.md (zh/en)
- Update Laya examples to use Laya 3.x Script pattern

* fix(ci): update typedoc path and add workflow_dispatch

- Fix typedoc entry point: packages/core -> packages/framework/core
- Add workflow_dispatch to release-changesets for manual triggering
- Add deeper package paths to trigger on nested packages
2025-12-26 16:52:29 +08:00
YHH
31fd34b221 fix: update Laya examples, add CLI docs, fix changesets workflow (#340)
* fix: update Laya examples, add CLI docs, fix changesets workflow

- Update Laya examples to use Laya 3.x Script pattern (@regClass)
- Add CLI tool quick start section to README (npx @esengine/cli init)
- Fix changesets workflow to only build framework packages
- Remove unnecessary Rust/WASM build steps from changesets workflow
- Remove redundant 'pnpm build' from changeset:publish script

* docs: add CLI documentation and update Laya examples

- Add CLI quick start section to getting-started.md (zh/en)
- Update Laya examples to use Laya 3.x Script pattern
2025-12-26 16:37:37 +08:00
YHH
c4f7a13b74 feat(cli): add CLI tool for adding ECS framework to existing projects (#339)
* feat(cli): add CLI tool for adding ECS framework to existing projects

- Support Cocos Creator 2.x/3.x, LayaAir 3.x, and Node.js platforms
- Auto-detect project type based on directory structure
- Generate ECSManager with full configuration (debug, remote debug, WebSocket URL)
- Auto-install dependencies with npm/yarn/pnpm detection
- Platform-specific decorators and lifecycle methods

* chore: add changeset for @esengine/cli

* fix(ci): fix YAML syntax error in ai-issue-helper workflow

* fix(cli): resolve file system race conditions (CodeQL)

* chore(ci): remove unused and broken workflows

* fix(ci): fix YAML encoding in release.yml
2025-12-26 16:18:59 +08:00
YHH
155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00
YHH
a84ff902e4 fix: 修复 changesets 验证错误 (#337)
- 简化 ignore 配置,只保留 editor-app
- 设置内部包为 private: true
  - build-config, engine, ecs-engine-bindgen
  - editor-core, editor-runtime
  - 所有 *-editor 插件包

这样 changesets 可以正常工作,private 包不会被发布
2025-12-26 09:06:52 +08:00
YHH
54038e3250 feat: 配置 changesets 版本管理和自动发布 (#336)
- 安装 @changesets/cli 和 @changesets/changelog-github
- 配置 .changeset/config.json
- 添加 npm scripts
- 创建 release-changesets.yml GitHub Action
2025-12-25 23:07:44 +08:00
yhh
5544fca002 docs: update changelog for v2.4.2 2025-12-25 20:42:15 +08:00
yhh
88b5ffc0a7 fix(ci): don't mark npm package releases as latest 2025-12-25 20:37:50 +08:00
yhh
0c0a5f10f7 chore(core): release v2.4.2 2025-12-25 20:31:51 +08:00
YHH
56e322de7f refactor(core): 优化 PlatformWorkerPool 和 IncrementalSerializer 代码规范 (#335)
PlatformWorkerPool:
- 添加 IWorkerPoolStatus 导出接口
- 添加便捷 getter: isDestroyed, workerCount, isReady, hasPendingTasks
- 提取 createTask/completeTask 方法减少重复代码
- 添加 ERROR_POOL_DESTROYED 常量
- 添加代码段分隔符和双语注释

IncrementalSerializer:
- 所有公共 API 添加 @zh/@en 双语注释
- 类型改用 interface 并添加 readonly
- SceneDataChange.value 从 any 改为 unknown
- 导出 SceneSnapshot, IIncrementalStats 接口
- 提取 DEFAULT_OPTIONS 常量
- 优化 getIncrementalStats 从 6 次 filter 改为 2 次循环
2025-12-25 18:39:15 +08:00
YHH
c2ebd387f2 refactor(core): 优化 WorkerEntitySystem 实现 (#334)
重构改进:
- 分离 PlatformWorkerPool 到独立文件
- 使用 Map 管理 Worker 任务状态,替代 (worker as any)._currentTask
- 提取通用批次分割逻辑 splitIntoBatches
- 添加 IWorkerMessageData 接口提高类型安全
- 添加 WorkerState 枚举提高可读性
- 添加规范的双语注释 (@zh/@en)
- 导出新类型 IWorkerSystemConfig 和 ProcessingMode
- 保留 WorkerSystemConfig 类型别名向后兼容

代码组织:
- 按功能分组方法 (初始化/处理/批次/结果应用等)
- 减少 any 类型使用
- 统一命名风格
2025-12-25 18:08:17 +08:00
YHH
e5e647f1a4 feat(pathfinding): 添加寻路系统模块 (#333)
* feat(pathfinding): 添加寻路系统模块

实现完整的寻路系统,支持 A* 算法、网格地图、导航网格和路径平滑:

A* 寻路算法:
- 高效的二叉堆优先队列
- 可配置的启发式权重
- 最大搜索节点限制
- 支持对角移动和穿角避免

网格地图 (GridMap):
- 基于二维数组的网格地图
- 支持 4 方向和 8 方向移动
- 可变移动代价
- 从数组或字符串加载地图

导航网格 (NavMesh):
- 凸多边形导航网格
- 自动检测相邻多边形
- 漏斗算法路径优化
- 适合复杂地形

路径平滑:
- Bresenham 视线检测
- 射线投射视线检测
- 视线简化器 (移除不必要的拐点)
- Catmull-Rom 曲线平滑
- 组合平滑器

启发式函数:
- 曼哈顿距离 (4方向)
- 欧几里得距离 (任意方向)
- 切比雪夫距离 (8方向)
- 八角距离 (8方向,对角线√2)

蓝图节点 (8个):
- FindPath: 基础寻路
- FindPathSmooth: 平滑寻路
- IsWalkable: 检查可通行性
- GetPathLength: 获取路径点数
- GetPathDistance: 获取路径距离
- GetPathPoint: 获取路径点
- MoveAlongPath: 沿路径移动
- HasLineOfSight: 视线检测

* chore: update pnpm-lock.yaml for pathfinding package
2025-12-25 17:24:24 +08:00
YHH
4d501ba448 feat(effect): 添加效果系统模块 (#332)
实现 Buff/Debuff 效果管理和属性修改器系统:

核心功能:
- 效果容器管理效果的应用、移除和更新
- 支持持续时间类型: 永久/计时/条件
- 支持叠加规则: 刷新/叠加/独立/替换/忽略
- 效果标签系统用于分类和查询
- 效果优先级和互斥标签支持
- 效果事件系统 (应用/移除/叠加/刷新/跳动/过期)

修改器系统:
- 属性修改器支持加法/乘法/覆盖/最小值/最大值操作
- 优先级分层计算 (基础/加法/乘法/最终)
- 数值计算器自动按优先级应用修改器

蓝图节点 (12个):
- ApplyEffect: 应用效果
- RemoveEffect/RemoveEffectByTag: 移除效果
- HasEffect/HasEffectTag: 检查效果
- GetEffectStacks/GetEffectRemainingTime/GetEffectCount: 查询效果
- ClearAllEffects: 清除所有效果
- OnEffectApplied/OnEffectRemoved/OnEffectTick: 效果事件
2025-12-25 15:17:06 +08:00
YHH
275124b66c feat(procgen): 添加程序化生成工具包 (#331)
- 添加噪声函数 (Perlin, Simplex, Worley, FBM)
- 添加种子随机数生成器 (SeededRandom)
- 添加加权随机选择和洗牌工具
- 添加蓝图节点 (SampleNoise2D, SeededRandom, WeightedPick 等)
2025-12-25 14:33:19 +08:00
YHH
25936c19e9 feat(network): 添加状态同步和客户端预测模块 (#330)
* feat(network): 添加状态同步和客户端预测模块

- 添加状态快照接口和快照缓冲区实现
- 实现线性插值和赫尔米特插值器
- 实现客户端预测和服务器校正系统
- 添加网络相关蓝图节点 (IsLocalPlayer, IsServer 等)

* chore: update pnpm-lock.yaml
2025-12-25 14:03:12 +08:00
YHH
f43631a1e1 feat(spatial): 添加 AOI 兴趣区域系统 (#329)
添加 AOI (Area of Interest) 兴趣区域管理功能:

核心接口:
- IAOIManager<T> - AOI 管理器接口
- IAOIEvent<T> - AOI 事件类型
- IAOIObserverConfig - 观察者配置

实现:
- GridAOI<T> - 基于网格的 AOI 实现
- 实体进入/离开视野事件
- 视野范围配置
- 订阅者模式

蓝图节点:
- GetEntitiesInView - 获取视野内实体
- GetObserversOf - 获取能看到目标的观察者
- CanSee - 检查是否可见
- EventEntityEnterView - 实体进入视野事件
- EventEntityExitView - 实体离开视野事件

服务令牌:
- AOIManagerToken - AOI 管理器服务令牌
2025-12-25 13:24:26 +08:00
YHH
f8c181836e refactor(blueprint): 使用 ecs-framework 的 Entity 和 IScene 类型 (#328)
- 从 @esengine/ecs-framework 导入 Entity 和 IScene 类型
- 移除 ExecutionContext.ts 中的自定义 IEntity 和 IScene 接口
- 更新 BlueprintComponent.ts、BlueprintVM.ts、BlueprintSystem.ts 使用正确的类型

这使得 blueprint 包与 ecs-framework 的类型保持一致,避免重复定义接口。
2025-12-25 13:07:09 +08:00
YHH
840eb3452e feat(fsm): 添加有限状态机模块 (#327)
- 添加 IStateMachine 接口,支持泛型状态和上下文
- 添加 StateMachine 实现
- 支持状态定义 (onEnter/onExit/onUpdate)
- 支持转换定义和条件
- 支持事件监听 (onChange/onEnter/onExit)
- 添加 8 个蓝图节点:
  - GetCurrentState, TransitionTo, CanTransition
  - IsInState, WasInState, GetStateDuration
  - EvaluateTransitions, ResetStateMachine
2025-12-25 12:51:52 +08:00
YHH
0bf849e193 feat(blueprint): 蓝图组合系统 (#326)
* feat(blueprint): 添加蓝图组合系统

- 添加 IBlueprintFragment 接口和实现,支持可重用蓝图片段
- 添加 IBlueprintComposer 接口和实现,支持片段组合
- 添加 FragmentRegistry 片段注册表
- 支持暴露引脚连接和编译成完整蓝图
- 支持片段资产序列化格式

* fix(blueprint): 移除未使用的 ExposedPin 导入
2025-12-25 12:42:57 +08:00
YHH
ebb984d354 feat(timer): 添加定时器和冷却系统 (#325)
- 添加 ITimerService 接口和 TimerService 实现
- 支持一次性定时器和重复定时器
- 支持冷却系统 (startCooldown/isCooldownReady/getCooldownProgress)
- 添加 8 个蓝图节点:
  - StartCooldown, IsCooldownReady, GetCooldownProgress
  - ResetCooldown, GetCooldownInfo
  - HasTimer, CancelTimer, GetTimerRemaining
2025-12-25 12:29:59 +08:00
YHH
068ca4bf69 feat(spatial): 空间查询和索引系统 (#324)
* feat(spatial): 添加空间查询包

- ISpatialQuery: 空间查询接口
  - findInRadius, findInRect, findNearest, findKNearest
  - raycast, raycastFirst
  - IBounds, IRaycastHit, SpatialFilter 类型

- ISpatialIndex: 空间索引接口
  - insert, remove, update, clear, getAll

- GridSpatialIndex: 网格空间索引实现
  - 基于均匀网格的空间划分
  - 支持所有 ISpatialQuery 操作

- 工具函数
  - createBounds, isPointInBounds, boundsIntersect
  - distanceSquared, distance

* feat(spatial): 添加空间查询蓝图节点

- 添加 FindInRadius/FindInRect/FindNearest/FindKNearest 节点
- 添加 Raycast/RaycastFirst 射线检测节点
- 每个节点包含模板和执行器
- 使用 menuPath: ['Spatial', ...] 组织节点菜单
2025-12-25 12:15:06 +08:00
YHH
4089051731 feat(blueprint): 添加蓝图触发器系统 (#323)
* feat(blueprint): 添加蓝图触发器系统

- TriggerTypes: 定义触发器类型和上下文接口
  - tick/input/collision/message/timer/stateEnter/stateExit/custom
  - 各类型的上下文接口和工厂函数

- TriggerCondition: 触发条件系统
  - ITriggerCondition 接口
  - 复合条件 (CompositeCondition, NotCondition)
  - 通用条件 (AlwaysTrue, AlwaysFalse, TriggerType, EntityId)
  - 特定类型条件 (InputAction, MessageName, StateName, TimerId, CollisionEntity, CustomEvent)
  - ConditionBuilder 链式 API

- BlueprintTrigger: 触发器核心实现
  - IBlueprintTrigger 接口
  - BlueprintTrigger 实现类
  - TriggerRegistry 触发器注册表
  - 各类型触发器工厂函数

- TriggerDispatcher: 触发器调度系统
  - ITriggerDispatcher 调度器接口
  - TriggerDispatcher 实现
  - EntityTriggerManager 实体触发器管理器

* feat(blueprint): 添加触发器相关事件节点

- EventInput: 输入事件节点 (action/value/pressed/released)
- EventCollisionEnter/Exit: 碰撞事件节点
- EventMessage: 消息事件节点
- EventTimer: 定时器事件节点
- EventStateEnter/Exit: 状态机事件节点
2025-12-25 11:35:29 +08:00
YHH
6b8b65ae16 feat(script-runtime): 服务器端蓝图执行模块 (#322)
* feat(script-runtime): 添加服务器端蓝图执行模块

- ServerBlueprintVM: 服务器端蓝图虚拟机
- CPULimiter: CPU 时间和步数限制
- IntentCollector: 意图收集系统
- FileMemoryStore: 文件系统持久化
- ServerExecutionContext: 服务器执行上下文

* refactor(script-runtime): 分离引擎接口与游戏逻辑

- 重构 IntentTypes.ts 只保留基础 IIntent 接口和通用常量
- IntentCollector 改为泛型类,支持任意意图类型
- ServerExecutionContext 改为泛型类,支持任意游戏状态类型
- ServerBlueprintVM 改为泛型类,使用 TGameState 和 TIntent 类型参数
- 移除游戏特定类型(IUnitState, ISpawnerState 等),由游戏项目定义
- 添加 IntentKeyExtractor 机制用于防止重复意图

* feat(script-runtime): 添加服务器端游戏循环框架

- PlayerSession: 封装单个玩家的 VM、蓝图和 Memory 状态
- TickScheduler: 管理所有玩家会话,调度每 tick 的蓝图执行
- IIntentProcessor: 意图处理器接口,由游戏项目实现
- IntentProcessorBase: 意图处理器基类,提供常用处理模式
- IntentProcessorRegistry: 按类型注册意图处理器
- GameLoop: 完整的游戏主循环,协调各组件工作

* feat(script-runtime): 添加通用蓝图节点

Memory 节点:
- GetMemory: 读取玩家 Memory
- SetMemory: 写入玩家 Memory
- HasMemoryKey: 检查键是否存在
- DeleteMemory: 删除 Memory 键

Log 节点:
- Log: 记录日志
- Warn: 记录警告
- Error: 记录错误

Game 信息节点:
- GetTick: 获取当前 tick
- GetPlayerId: 获取玩家 ID
- GetDeltaTime: 获取增量时间
- GetGameState: 获取游戏状态

提供 registerScriptRuntimeNodes() 用于批量注册节点

* fix(script-runtime): 修复 CI 构建错误

- 更新 tsconfig.json 继承 tsconfig.base.json
- 添加 references 到 core 和 blueprint 包
- 更新 pnpm-lock.yaml

* fix(script-runtime): 修复 DTS 构建错误

- 添加 tsconfig.build.json 用于 tsup 构建
- 更新 tsup.config.ts 使用 tsconfig.build.json
- 分离构建配置和类型检查配置
2025-12-25 11:00:43 +08:00
YHH
a75c61c049 docs: 添加 Discord 社区链接 (#321) 2025-12-24 23:35:43 +08:00
YHH
770c05402d docs: 优化 README 包列表展示 (#320) 2025-12-24 23:33:12 +08:00
YHH
9d581ccd8d ci: 集成 Turborepo Remote Cache (#319) 2025-12-24 23:06:38 +08:00
YHH
235c432edb feat(network): 基于 TSRPC 的网络同步模块 (#318)
- network-protocols: 共享协议包,使用 TSRPC CLI 生成完整类型验证
- network: 浏览器客户端,提供 NetworkPlugin、NetworkService 和同步系统
- network-server: Node.js 服务端,提供 GameServer 和房间管理
2025-12-24 22:49:29 +08:00
YHH
dbc6793dc4 refactor: 代码规范化与依赖清理 (#317)
* refactor(deps): 统一编辑器包依赖配置 & 优化分层架构

- 将 ecs-engine-bindgen 提升为 Layer 1 核心包
- 统一 9 个编辑器包的依赖声明模式
- 清理废弃的包目录 (ui, ui-editor, network-*)

* refactor(tokens): 修复 PrefabService 令牌冲突 & 补充 module.json

- 将 editor-core 的 PrefabServiceToken 改名为 EditorPrefabServiceToken
  避免与 asset-system 的 PrefabServiceToken 冲突 (Symbol.for 冲突)
- 为 mesh-3d 添加 module.json
- 为 world-streaming 添加 module.json

* refactor(editor-core): 整理导出结构 & 添加 blueprint tokens.ts

- 按功能分组整理 editor-core 的 65 行导出
- 添加清晰的分组注释 (中英双语)
- 为 blueprint 添加占位符 tokens.ts

* chore(editor): 为 14 个编辑器插件包添加 module.json

统一编辑器包的模块配置,包含:
- isEditorPlugin 标识
- runtimeModule 关联
- exports 导出清单 (inspectors, panels, gizmos)

* refactor(core): 改进类型安全 - 减少 as any 使用

- 添加 GlobalTypes.ts 定义小游戏平台和 Chrome API 类型
- SoAStorage 使用 IComponentTypeMetadata 替代 as any
- PlatformDetector 使用类型安全的平台检测
- 添加 ISoAStorageStats/ISoAFieldStats 接口

* feat(editor): 添加 EditorServicesContext 解决 prop drilling

- 新增 contexts/EditorServicesContext.tsx 提供统一服务访问
- App.tsx 包裹 EditorServicesProvider
- 提供 useEditorServices/useMessageHub 等便捷 hooks
- SceneHierarchy 添加迁移注释,后续可移除 props

* docs(editor): 澄清 inspector 目录架构关系

- inspector/ 标记为内部实现,添加 @deprecated 警告
- inspectors/ 标记为公共 API 入口点
- 添加架构说明文档

* refactor(editor): 添加全局类型声明消除 window as any

- 创建 editor-app/src/global.d.ts 声明 Window 接口扩展
- 创建 editor-core/src/global.d.ts 声明 Window 接口扩展
- 更新 App.tsx 使用类型安全的 window 属性访问
- 更新 PluginLoader.ts 使用 window.__ESENGINE_PLUGINS__
- 更新 PluginSDKRegistry.ts 使用 window.__ESENGINE_SDK__
- 更新 UserCodeService.ts 使用类型安全的全局变量访问

* refactor(editor): 提取项目和场景操作到独立 hooks

- 创建 useProjectActions hook 封装项目操作
- 创建 useSceneActions hook 封装场景操作
- 为渐进式重构 App.tsx 做准备

* refactor(editor): 清理冗余代码和未使用文件

删除的目录和文件:
- application/state/ - 重复的状态管理(与 stores/ 重复)
- 8 个孤立 CSS 文件(对应组件不存在)
- AssetBrowser.tsx - 仅为 ContentBrowser 的向后兼容包装
- AssetPicker.tsx - 未被使用
- AssetPickerDialog.tsx (顶级) - 已被 dialogs/ 版本取代
- EntityInspector.tsx (顶级) - 已被 inspectors/views/ 版本取代

修复:
- 移除 App.tsx 中未使用的导入
- 更新 application/index.ts 移除已删除模块
- 修复 useProjectActions.ts 的 MutableRefObject 类型

* refactor(editor): 统一 inspectors 模块导出结构

- 在 inspectors/index.ts 重新导出 PropertyInspector
- 创建 inspectors/fields/index.ts barrel export
- 导出 views、fields、common 子模块
- 更新 EntityInspector 使用统一入口导入

* refactor(editor): 删除废弃的 Profiler 组件

删除未使用的组件(共 1059 行):
- ProfilerPanel.tsx (229 行)
- ProfilerWindow.tsx (589 行)
- ProfilerDockPanel.tsx (241 行)
- ProfilerPanel.css
- ProfilerDockPanel.css

保留:AdvancedProfiler + AdvancedProfilerWindow(正在使用)

* refactor(runtime-core): 统一依赖处理与插件状态管理

- 新增 DependencyUtils 统一拓扑排序和依赖验证
- 新增 PluginState 定义插件生命周期状态机
- 合并 UnifiedPluginLoader 到 PluginLoader
- 清理 index.ts 移除不必要的 Token re-exports
- 新增 RuntimeMode/UserCodeRealm/ImportMapGenerator

* refactor(editor-core): 使用统一的 ImportMapGenerator

- WebBuildPipeline 使用 runtime-core 的 generateImportMap
- UserCodeService 添加 ImportMap 相关接口

* feat(compiler): 增强 esbuild 查找策略

- 支持本地 node_modules、pnpm exec、npx、全局多种来源
- EngineService 使用 RuntimeMode

* refactor(runtime-core): 简化 GameRuntime 代码

- 合并 _disableGameLogicSystems/_enableGameLogicSystems 为 _setGameLogicSystemsEnabled
- 精简本地 Token 定义的注释

* refactor(editor-core): 引入 BaseRegistry 基类消除代码重复

- 新增 BaseRegistry 和 PrioritizedRegistry 基类
- 重构 CompilerRegistry, InspectorRegistry, FieldEditorRegistry
- 统一注册表的日志记录和错误处理

* refactor(editor-core): 扩展 BaseRegistry 重构

- ComponentInspectorRegistry 继承 PrioritizedRegistry
- EditorComponentRegistry 继承 BaseRegistry
- EntityCreationRegistry 继承 BaseRegistry
- PropertyRendererRegistry 继承 PrioritizedRegistry
- 导出 BaseRegistry 基类供外部使用
- 统一双语注释格式

* refactor(editor-core): 代码优雅性优化

CommandManager:
- 提取 tryMergeWithLast() 和 pushToUndoStack() 消除重复代码
- 统一双语注释格式

FileActionRegistry:
- 提取 normalizeExtension() 消除扩展名规范化重复
- 统一私有属性命名风格(_前缀)
- 使用 createRegistryToken 统一 Token 创建

BaseRegistry:
- 添加 IOrdered 接口
- 添加 sortByOrder() 排序辅助方法

EntityCreationRegistry:
- 使用 sortByOrder() 简化排序逻辑

* refactor(editor-core): 统一日志系统 & 代码规范优化

- GizmoRegistry: 使用 createLogger 替代 console.warn
- VirtualNodeRegistry: 使用 createLogger 替代 console.warn
- WindowRegistry: 使用 logger、添加 _ 前缀、导出 IWindowRegistry token
- EditorViewportService: 使用 createLogger 替代 console.warn
- ComponentActionRegistry: 使用 logger、添加 _ 前缀、返回值改进
- SettingsRegistry: 使用 logger、提取 ensureCategory/ensureSection 方法
- 添加 WindowRegistry 到主导出

* refactor(editor-core): ModuleRegistry 使用 logger 替代 console

* refactor(editor-core): SerializerRegistry/UIRegistry 添加 token 和 _ 前缀

* refactor(editor-core): UIRegistry 代码优雅性 & Token 命名统一

- UIRegistry: 提取 _sortByOrder 消除 6 处重复排序逻辑
- UIRegistry: 添加分节注释和双语文档
- FieldEditorRegistry: Token 重命名为 FieldEditorRegistryToken
- PropertyRendererRegistry: Token 重命名为 PropertyRendererRegistryToken

* refactor(core): 统一日志系统 - console 替换为 logger

- ComponentSerializer: 使用 logger 替代 console.warn
- ComponentRegistry: console.warn → logger.warn (已有 logger)
- SceneSerializer: 添加 logger,替换 console.warn/error
- SystemScheduler: 添加 logger,替换 console.warn
- VersionMigration: 添加 logger,替换所有 console.warn
- RuntimeModeService: console.error → logger.error
- Core.ts: _logger 改为 readonly,双语错误消息
- SceneSerializer 修复:使用 getComponentTypeName 替代 constructor.name

* fix(core): 修复 constructor.name 压缩后失效问题

- Scene.ts: 使用 system.systemName 替代 system.constructor.name
- CommandBuffer.ts: 使用 getComponentTypeName() 替代 constructor.name

* refactor(editor-core): 代码规范优化 - 私有方法命名 & 日志统一

- BuildService: console → logger
- FileActionRegistry: 添加 logger, 私有方法 _ 前缀
- SettingsRegistry: 私有方法 _ 前缀 (ensureCategory → _ensureCategory)

* refactor(core): Scene.ts 私有变量命名规范化

- logger → _logger (遵循私有变量 _ 前缀规范)

* refactor(editor-core): 服务类私有成员命名规范化

- CommandManager: 私有变量/方法添加 _ 前缀
  - undoStack/redoStack/config/isExecuting
  - tryMergeWithLast/pushToUndoStack
- LocaleService: 私有变量/方法添加 _ 前缀
  - currentLocale/translations/changeListeners
  - deepMerge/getNestedValue/loadSavedLocale/saveLocale

* refactor(core): 私有成员命名规范化 & 单例模式优化

- Component.ts: _idGenerator 私有静态变量规范化
- PlatformManager.ts: _instance, _adapter, _logger 规范化
- AutoProfiler.ts: _instance, _config 及所有私有方法规范化
- ProfilerSDK.ts: _instance, _config 及所有私有方法规范化
- ComponentPoolManager: _instance, _pools, _usageTracker 规范化
- GlobalEventBus: _instance 规范化
- 添加中英双语 JSDoc 注释

* refactor(editor-app,behavior-tree-editor): 私有成员 & 单例模式命名规范化

editor-app:
- EngineService: private static instance → _instance
- EditorEngineSync: 所有私有成员添加 _ 前缀
- RuntimeResolver: 所有私有成员和方法添加 _ 前缀
- SettingsService: 所有私有成员和方法添加 _ 前缀

behavior-tree-editor:
- GlobalBlackboardService: 所有私有成员和方法添加 _ 前缀
- NotificationService: private static instance → _instance
- NodeRegistryService: 所有私有成员和方法添加 _ 前缀
- TreeStateAdapter: private static instance → _instance

* fix(editor-runtime): 添加 editor-core 到 external 避免传递依赖问题

将 @esengine/editor-core 添加到 vite external 配置,
避免 editor-core → runtime-core → ecs-engine-bindgen 的传递依赖
被错误地打包进 editor-runtime.js,导致 CI 构建失败。

* fix(core): 修复空接口 lint 错误

将 IByteDanceMiniGameAPI、IAlipayMiniGameAPI、IBaiduMiniGameAPI 从空接口改为类型别名,修复 no-empty-object-type 规则报错
2025-12-24 20:57:08 +08:00
YHH
58f70a5783 refactor(core): 代码结构优化 & 添加独立 ECS 框架文档 (#316)
Core 库优化:
- Scene.ts: 提取 _runSystemPhase() 消除 update/lateUpdate 重复代码
- World.ts: 可选链简化、提取 _isSceneCleanupCandidate() 消除重复条件
- Entity.ts: 移除过度设计的 EntityComparer
- 清理方法内部冗余注释,保持代码自解释

文档改进:
- 为 core 包创建独立 README (中英文)
- 明确 ECS 框架可独立用于 Cocos/Laya 等引擎
- 添加 sparse-checkout 命令说明,支持只克隆 core 源码
- 主 README 添加 ECS 独立使用提示
2025-12-23 18:00:21 +08:00
YHH
828ff969e1 feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 (#315)
* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持

* chore: 更新 pnpm-lock.yaml

* fix: 移除未使用的变量和方法

* fix: 修复 mesh-3d-editor tsconfig 引用路径

* fix: 修复正则表达式 ReDoS 漏洞
2025-12-23 15:34:01 +08:00
yhh
49dd6a91c6 docs: v2.4.1 changelog 2025-12-23 15:22:33 +08:00
yhh
1e048d5c04 chore(core): bump version to 2.4.1 2025-12-23 15:20:19 +08:00
yhh
dff2ec564b fix(core): IntervalSystem时间累加bug修复 & 添加Core.paused文档
- 修复 onCheckProcessing() 在 update/lateUpdate 被调用两次导致时间累加翻倍的问题
- 添加 Core.paused 游戏暂停文档(中/英文)
- 新增 IntervalSystem 相关测试用例
2025-12-23 09:41:22 +08:00
yhh
2381919a5c fix(core): Cocos Creator 兼容性 - 类型导出修复
- 将 export interface 转换为 export type 以兼容 Rollup
- 分离类型导入/导出使用 import type 和 export type
- 修复 QueryCondition, QueryResult, SupportedTypedArray 等类型的重导出
2025-12-22 14:54:38 +08:00
yhh
66d9f428b3 feat: 3D 编辑器支持 - 网格、相机、Gizmo 2025-12-22 12:40:43 +08:00
YHH
a1e1189f9d feat(fairygui): FairyGUI 完整集成 (#314)
* feat(fairygui): FairyGUI ECS 集成核心架构

实现 FairyGUI 的 ECS 原生集成,完全替代旧 UI 系统:

核心类:
- GObject: UI 对象基类,支持变换、可见性、关联、齿轮
- GComponent: 容器组件,管理子对象和控制器
- GRoot: 根容器,管理焦点、弹窗、输入分发
- GGroup: 组容器,支持水平/垂直布局

抽象层:
- DisplayObject: 显示对象基类
- EventDispatcher: 事件分发
- Timer: 计时器
- Stage: 舞台,管理输入和缩放

布局系统:
- Relations: 约束关联管理
- RelationItem: 24 种关联类型

基础设施:
- Controller: 状态控制器
- Transition: 过渡动画
- ScrollPane: 滚动面板
- UIPackage: 包管理
- ByteBuffer: 二进制解析

* refactor(ui): 删除旧 UI 系统,使用 FairyGUI 替代

* feat(fairygui): 实现 UI 控件

- 添加显示类:Image、TextField、Graph
- 添加基础控件:GImage、GTextField、GGraph
- 添加交互控件:GButton、GProgressBar、GSlider
- 更新 IRenderCollector 支持 Graph 渲染
- 扩展 Controller 添加 selectedPageId
- 添加 STATE_CHANGED 事件类型

* feat(fairygui): 现代化架构重构

- 增强 EventDispatcher 支持类型安全、优先级和传播控制
- 添加 PropertyBinding 响应式属性绑定系统
- 添加 ServiceContainer 依赖注入容器
- 添加 UIConfig 全局配置系统
- 添加 UIObjectFactory 对象工厂
- 实现 RenderBridge 渲染桥接层
- 实现 Canvas2DBackend 作为默认渲染后端
- 扩展 IRenderCollector 支持更多图元类型

* feat(fairygui): 九宫格渲染和资源加载修复

- 修复 FGUIUpdateSystem 支持路径和 GUID 两种加载方式
- 修复 GTextInput 同时设置 _displayObject 和 _textField
- 实现九宫格渲染展开为 9 个子图元
- 添加 sourceWidth/sourceHeight 用于九宫格计算
- 添加 DOMTextRenderer 文本渲染层(临时方案)

* fix(fairygui): 修复 GGraph 颜色读取

* feat(fairygui): 虚拟节点 Inspector 和文本渲染支持

* fix(fairygui): 编辑器状态刷新和遗留引用修复

- 修复切换 FGUI 包后组件列表未刷新问题
- 修复切换组件后 viewport 未清理旧内容问题
- 修复虚拟节点在包加载后未刷新问题
- 重构为事件驱动架构,移除轮询机制
- 修复 @esengine/ui 遗留引用,统一使用 @esengine/fairygui

* fix: 移除 tsconfig 中的 @esengine/ui 引用
2025-12-22 10:52:54 +08:00
YHH
96b5403d14 refactor(render): 抽象图形后端并迁移渲染器 (#313)
* refactor(render): 抽象图形后端并迁移渲染器

- 新增 engine-shared 包,定义 GraphicsBackend trait 抽象层
- 实现 WebGL2Backend 作为首个后端实现
- 迁移 Renderer2D、SpriteBatch、GridRenderer、GizmoRenderer 使用新抽象
- 修复 VAO 创建时索引缓冲区绑定状态泄漏问题
- 新增 create_vertex_buffer_sized 方法支持预分配缓冲区

* fix(serialization): 修复序列化循环引用导致栈溢出

- 在 serializeValue 添加 WeakSet 检测循环引用
- 跳过已访问对象避免无限递归

* refactor(serialization): 提取 ValueSerializer 统一序列化逻辑

- 新增 ValueSerializer 模块,函数式设计
- 支持可扩展类型处理器注册
- 移除 ComponentSerializer/SceneSerializer 重复代码
- 内置 Date/Map/Set 类型支持

* fix: CodeQL 类型检查警告
2025-12-19 22:46:33 +08:00
YHH
4b74db3f2d refactor(render): 统一渲染API并修复双重渲染问题 (#312)
- 将 submitSpritesDirectly 重命名为 submitSprites,移除旧的对象数组 API
- RenderBatcher 采用 SoA 模式,预分配类型数组避免每帧 GC
- 拆分 worldBatcher 和 screenBatcher 修复 play 模式下的双重渲染
- 移除未使用的材质实例管理代码
2025-12-19 18:40:19 +08:00
YHH
e24c850568 refactor: 类型安全与接口清理 (#311)
* refactor: 分解 IEngineBridge 为单一职责接口

- 新增 ITextureService, IDynamicAtlasService, ICoordinateService, IRenderConfigService
- 移除 EngineBridgeToken,改用具体服务 Token
- 更新 camera, ui, particle 等模块使用新接口
- 优化装饰器类型安全,使用 Symbol-based metadata 访问模式

* refactor: 删除 plugin-types 包,统一 createServiceToken 实现

- 移动 IEditorModuleBase 接口到 engine-core
- 移除 engine-core 和 editor-core 对 plugin-types 的依赖
- 删除冗余的 plugin-types 包
- 统一使用 core 中基于 Symbol.for() 的 createServiceToken

* refactor: 统一 IPlugin 接口,移除 deprecated 别名

- 移除 engine-core、editor-core、runtime-core 中的 IPlugin 别名
- 模块插件统一使用 IRuntimePlugin(运行时)或 IEditorPlugin(编辑器)
- 保留 core 包中的 IPlugin 作为 ECS 核心插件接口(不同概念)
- 更新所有消费方使用正确的类型

* refactor: 重命名 editor-core ComponentRegistry 为 EditorComponentRegistry

- 消除与 core 包 ComponentRegistry(ECS 位掩码管理)的命名歧义
- editor-core 的 EditorComponentRegistry 专用于编辑器组件元数据
- 更新所有编辑器包使用新名称
2025-12-19 17:48:18 +08:00
YHH
ecdb8f2021 feat: UI输入框IME支持和编辑器Inspector重构 (#310)
UI系统改进:
- 添加 IMEHelper 支持中文/日文/韩文输入法
- UIInputFieldComponent 添加组合输入状态管理
- UIInputSystem 添加 IME 事件处理
- UIInputFieldRenderSystem 优化渲染逻辑
- UIRenderCollector 增强纹理处理

引擎改进:
- EngineBridge 添加新的渲染接口
- EngineRenderSystem 优化渲染流程
- Rust 引擎添加新的渲染功能

编辑器改进:
- 新增模块化 Inspector 组件架构
- EntityRefField 增强实体引用选择
- 优化 FlexLayoutDock 和 SceneHierarchy 样式
- 添加国际化文本
2025-12-19 15:45:14 +08:00
YHH
536c4c5593 refactor(ui): UI 系统架构重构 (#309)
* feat(ui): 动态图集系统与渲染调试增强

## 核心功能

### 动态图集系统 (Dynamic Atlas)
- 新增 DynamicAtlasManager:运行时纹理打包,支持 MaxRects 算法
- 新增 DynamicAtlasService:自动纹理加载与图集管理
- 新增 BinPacker:高效矩形打包算法
- 支持动态/固定两种扩展策略
- 自动 UV 重映射,实现 UI 元素合批渲染

### Frame Debugger 增强
- 新增合批分析面板,显示批次中断原因
- 新增 UI 元素层级信息(depth, worldOrderInLayer)
- 新增实体高亮功能,点击可在场景中定位
- 新增动态图集可视化面板
- 改进渲染原语详情展示

### 闪光效果 (Shiny Effect)
- 新增 UIShinyEffectComponent:UI 闪光参数配置
- 新增 UIShinyEffectSystem:材质覆盖驱动的闪光动画
- 新增 ShinyEffectComponent/System(Sprite 版本)

## 引擎层改进

### Rust 纹理管理扩展
- create_blank_texture:创建空白 GPU 纹理
- update_texture_region:局部纹理更新
- 支持动态图集的 GPU 端操作

### 材质系统
- 新增 effects/ 目录:ShinyEffect 等效果实现
- 新增 interfaces/ 目录:IMaterial 等接口定义
- 新增 mixins/ 目录:可组合的材质功能

### EngineBridge 扩展
- 新增 createBlankTexture/updateTextureRegion 方法
- 改进纹理加载回调机制

## UI 渲染改进
- UIRenderCollector:支持合批调试信息
- 稳定排序:addIndex 保证渲染顺序一致性
- 九宫格渲染优化
- 材质覆盖支持

## 其他改进
- 国际化:新增 Frame Debugger 相关翻译
- 编辑器:新增渲染调试入口
- 文档:新增架构设计文档目录

* refactor(ui): 引入新基础组件架构与渲染工具函数

Phase 1 重构 - 组件职责分离与代码复用:

新增基础组件层:
- UIGraphicComponent: 所有可视 UI 元素的基类(颜色、透明度、raycast)
- UIImageComponent: 纹理显示组件(支持简单、切片、平铺、填充模式)
- UISelectableComponent: 可交互元素的基类(状态管理、颜色过渡)

新增渲染工具:
- UIRenderUtils: 提取共享的坐标计算、边框渲染、阴影渲染等工具函数
- getUIRenderTransform: 统一的变换数据提取
- renderBorder/renderShadow: 复用的边框和阴影渲染逻辑

新增渲染系统:
- UIGraphicRenderSystem: 处理新基础组件的统一渲染器

重构现有系统:
- UIRectRenderSystem: 使用新工具函数,移除重复代码
- UIButtonRenderSystem: 使用新工具函数,移除重复代码

这些改动为后续统一渲染系统奠定基础。

* refactor(ui): UIProgressBarRenderSystem 使用渲染工具函数

- 使用 getUIRenderTransform 替代手动变换计算
- 使用 renderBorder 工具函数替代重复的边框渲染
- 使用 lerpColor 工具函数替代重复的颜色插值
- 简化方法签名,使用 UIRenderTransform 类型
- 移除约 135 行重复代码

* refactor(ui): Slider 和 ScrollView 渲染系统使用工具函数

- UISliderRenderSystem: 使用 getUIRenderTransform,简化方法签名
- UIScrollViewRenderSystem: 使用 getUIRenderTransform,简化方法签名
- 统一使用 UIRenderTransform 类型减少参数传递
- 消除重复的变换计算代码

* refactor(ui): 使用 UIWidgetMarker 消除硬编码组件依赖

- 新增 UIWidgetMarker 标记组件
- UIRectRenderSystem 改为检查标记而非硬编码4种组件类型
- 各 Widget 渲染系统自动添加标记组件
- 减少模块间耦合,提高可扩展性

* feat(ui): 实现 Canvas 隔离机制

- 新增 UICanvasComponent 定义 Canvas 渲染组
- UITransformComponent 添加 Canvas 相关字段:canvasEntityId, worldSortingLayer, pixelPerfect
- UILayoutSystem 传播 Canvas 设置给子元素
- UIRenderUtils 使用 Canvas 继承的排序层
- 支持嵌套 Canvas 和不同渲染模式

* refactor(ui): 统一纹理管理工具函数

Phase 4: 纹理管理统一

新增:
- UITextureUtils.ts: 统一的纹理描述符接口和验证函数
  - UITextureDescriptor: 支持 GUID/textureId/path 多种纹理源
  - isValidTextureGuid: GUID 验证
  - getTextureKey: 获取用于合批的纹理键
  - normalizeTextureDescriptor: 规范化各种输入格式
- utils/index.ts: 工具函数导出

修改:
- UIGraphicRenderSystem: 使用新的纹理工具函数
- index.ts: 导出纹理工具类型和函数

* refactor(ui): 实现统一的脏标记机制

Phase 5: Dirty 标记机制

新增:
- UIDirtyFlags.ts: 位标记枚举和追踪工具
  - UIDirtyFlags: Visual/Layout/Transform/Material/Text 标记
  - IDirtyTrackable: 脏追踪接口
  - DirtyTracker: 辅助工具类
  - 帧级别脏状态追踪 (markFrameDirty, isFrameDirty)

修改:
- UIGraphicComponent: 实现 IDirtyTrackable
  - 属性 setter 自动设置脏标记
  - 保留 setDirty/clearDirty 向后兼容
- UIImageComponent: 所有属性支持脏追踪
  - textureGuid/imageType/fillAmount 等变化自动标记
- UIGraphicRenderSystem: 使用 clearDirtyFlags()

导出:
- UIDirtyFlags, IDirtyTrackable, DirtyTracker
- markFrameDirty, isFrameDirty, clearFrameDirty

* refactor(ui): 移除过时的 dirty flag API

移除 UIGraphicComponent 中的兼容性 API:
- 移除 _isDirty getter/setter
- 移除 setDirty() 方法
- 移除 clearDirty() 方法

现在统一使用新的 dirty flag 系统:
- isDirty() / hasDirtyFlag(flags)
- markDirty(flags) / clearDirtyFlags()

* fix(ui): 修复两个 TODO 功能

1. 滑块手柄命中测试 (UIInputSystem)
   - UISliderComponent 添加 getHandleBounds() 计算手柄边界
   - UISliderComponent 添加 isPointInHandle() 精确命中测试
   - UIInputSystem.handleSlider() 使用精确测试更新悬停状态

2. 径向填充渲染 (UIGraphicRenderSystem)
   - 实现 renderRadialFill() 方法
   - 支持 radial90/radial180/radial360 三种模式
   - 支持 fillOrigin (top/right/bottom/left) 和 fillClockwise
   - 使用多段矩形近似饼形填充效果

* feat(ui): 完善 UI 系统架构和九宫格渲染

* fix(ui): 修复文本渲染层级问题并清理调试代码

- 修复纹理就绪后调用 invalidateUIRenderCaches() 导致的无限循环
- 移除 UITextRenderSystem、UIButtonRenderSystem、UIRectRenderSystem 中的首帧调试输出
- 移除 UILayoutSystem 中的布局调试日志
- 清理所有 __UI_RENDER_DEBUG__ 条件日志

* refactor(ui): 优化渲染批处理和输入框组件

渲染系统:
- 修复 RenderBatcher 保持渲染顺序
- 优化 Rust SpriteBatch 避免合并非连续精灵
- 增强 EngineRenderSystem 纹理就绪检测

输入框组件:
- 增强 UIInputFieldComponent 功能
- 改进 UIInputSystem 输入处理
- 新增 TextMeasureService 文本测量服务

* fix(ui): 修复九宫格首帧渲染和InputField输入问题

- 修复九宫格首帧 size=0x0 问题:
  - Viewport.tsx: 预览模式读取图片尺寸存储到 importSettings
  - AssetDatabase: ISpriteSettings 添加 width/height 字段
  - AssetMetadataService: getTextureSpriteInfo 使用元数据尺寸作为后备
  - UIRectRenderSystem: 当 atlasEntry 不存在时使用 spriteInfo 尺寸
  - WebBuildPipeline: 构建时包含 importSettings
  - AssetManager: 从 catalog 初始化时复制 importSettings
  - AssetTypes: IAssetCatalogEntry 添加 importSettings 字段

- 修复 InputField 无法输入问题:
  - UIRuntimeModule: manifest 添加 pluginExport: 'UIPlugin'
  - 确保预览模式正确加载 UI 插件并绑定 UIInputSystem

- 添加调试日志用于排查纹理加载问题

* fix(sprite): 修复类型导出错误

MaterialPropertyOverride 和 MaterialOverrides 应从 @esengine/material-system 导出

* fix(ui-editor): 补充 AnchorPreset 拉伸预设的映射

添加 StretchTop, StretchMiddle, StretchBottom, StretchLeft, StretchCenter, StretchRight 的位置和锚点值映射
2025-12-19 15:33:36 +08:00
yhh
958933cd76 fix(ci): 修正 SignPath artifact-configuration-slug 为 initial 2025-12-16 15:44:42 +08:00
yhh
fbc911463a Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-12-16 15:07:41 +08:00
yhh
5b7746af79 fix(ci): 修复 SignPath 代码签名 artifact 参数错误
SignPath GitHub Action 不支持 github-artifact-name 参数,
改用 github-artifact-id 并通过 GitHub API 获取 artifact ID。

- 移除 download-artifact 步骤(SignPath 直接从 GitHub 获取)
- 添加 get-artifact 步骤通过 API 获取 artifact ID
- 使用 github-artifact-id 替代无效的 github-artifact-name
2025-12-16 15:07:18 +08:00
YHH
9e195ae3fd fix(editor): 修复 Play/Stop 循环中的场景管理器和动态实体问题 (#307)
问题修复:
1. RuntimeSceneManager 在 Stop 后失效
   - 根因:SceneLoadTriggerSystem 闭包缓存了 sceneManager 引用
   - 修复:每次点击时动态从 Core.services 获取服务

2. Play 期间创建的动态实体(如 ClickFx 粒子)Stop 后残留
   - 根因:EntityList.removeAllEntities() 只清空 _entitiesToAdd 队列但没有销毁实体
   - 修复:先销毁待添加队列中的实体再清空

3. 场景切换后动态实体残留
   - 根因:editorSceneLoader 中 saveSceneSnapshot() 覆盖了初始快照
   - 修复:移除该调用,保持 Play 开始时的快照不被覆盖

架构改进:
- RuntimeSceneManager 新增 reset() 方法,区分会话重置和完全销毁
- Viewport 复用 RuntimeSceneManager 实例而非每次创建
- IRuntimeSceneManager 接口补充 setSceneLoader/setBaseUrl 方法
2025-12-16 15:07:11 +08:00
yhh
a18eb5aa3c fix(ci): 保持 Release 为 Draft 状态,防止自动发布 2025-12-16 13:16:33 +08:00
yhh
48d3d14af2 feat(ci): 改进 SignPath 代码签名集成
- 添加 SignPath 配置检查步骤
- 使用 test-signing 策略进行测试
- 即使签名跳过也能继续版本更新 PR
2025-12-16 13:11:59 +08:00
YHH
ed8f6e283b feat: 纹理路径稳定 ID 与架构改进 (#305)
* feat(asset-system): 实现路径稳定 ID 生成器

使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。

* fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用

使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效

* fix(editor-core): 修复场景切换时的资源泄漏

在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性

* fix(runtime-core): 修复 PluginManager 组件注册类型错误

将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例

* refactor(core): 提取 IComponentRegistry 接口

将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表

* refactor(engine-core): 改进插件服务注册机制

- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理

* refactor(modules): 适配新的组件注册接口

更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming

* fix(physics-rapier2d): 修复物理插件组件注册

- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题

* feat(editor-core): 添加 UserCodeService 就绪信号机制

- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题

* fix(editor-app): 在编译完成后调用 signalReady()

确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载

* feat(editor-core): 改进编辑器核心服务

- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口

* feat(engine): 改进 Rust 纹理管理器

- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定

* feat(ui): 添加场景切换和文本闪烁组件

新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化

* feat(editor-app): 添加外部文件修改检测

- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户

* feat(editor-app): 添加渲染调试面板

- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进

* refactor(editor-app): 编辑器服务和组件优化

- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进

* feat(i18n): 更新国际化翻译

- 添加新功能相关翻译
- 更新中文、英文、西班牙文

* feat(tauri): 添加文件修改时间查询命令

- 新增 get_file_mtime 命令
- 支持检测文件外部修改

* refactor(particle): 粒子系统改进

- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试

* refactor(platform): 平台适配层优化

- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化

* refactor(asset-system-editor): 资产元数据改进

- AssetMetaFile 优化
- 导出调整

* fix(asset-system): 移除未使用的 TextureLoader 导入

* fix(tests): 更新测试以使用 GlobalComponentRegistry 实例

修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
2025-12-16 12:46:14 +08:00
yhh
d834ca5e77 ci(editor): add SignPath OSS code signing for Windows builds
- Upload Windows artifacts (exe/msi) for SignPath signing
- Configure SignPath action with organization secrets
- Fix artifact name to 'windows-unsigned'
- Add bilingual comments for required secrets
2025-12-15 10:20:15 +08:00
yhh
85e95ec18c docs: add English entity documentation with EntityHandle 2025-12-15 10:00:44 +08:00
yhh
ff9bc00729 docs: 改进 EntityHandle 文档,添加实际使用场景示例 2025-12-15 09:59:12 +08:00
yhh
7451a78e60 docs: 补充 EntityHandle 实体句柄文档 2025-12-15 09:38:45 +08:00
yhh
23ee2393c6 chore(release): 准备发布 v2.4.0,改进 CI 发布流程
- 更新 @esengine/ecs-framework 版本号到 2.4.0
- 更新中英文 changelog
- CI: 支持标签触发自动发布(v* 或 package-v* 格式)
- CI: 保留手动触发选项
- CI: 标签模式下自动创建 GitHub Release
2025-12-15 09:33:51 +08:00
YHH
cd6ef222d1 feat(ecs): 核心系统改进 - 句柄、调度、变更检测与查询编译 (#304)
新增功能:
- EntityHandle: 轻量级实体句柄 (28位索引 + 20位代数)
- SystemScheduler: 声明式系统调度,支持 @Stage/@Before/@After/@InSet 装饰器
- EpochManager: 帧级变更检测
- CompiledQuery: 预编译类型安全查询

API 改进:
- EntitySystem 添加 getBefore()/getAfter()/getSets() getter 方法
- Entity 添加 markDirty() 辅助方法
- IScene 添加 epochManager 属性
- CommandBuffer.pendingCount 修正为返回实际操作数

文档更新:
- 更新系统调度和查询相关文档
2025-12-15 09:17:00 +08:00
yhh
b5158b6ac6 chore: 移除第三方引擎引用,改进 README 专业度 2025-12-13 20:56:22 +08:00
YHH
beaa1d09de feat: 预制体系统与架构改进 (#303)
* feat(prefab): 实现预制体系统和编辑器 UX 改进

## 预制体系统
- 新增 PrefabSerializer: 预制体序列化/反序列化
- 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改
- 新增 PrefabService: 预制体核心服务
- 新增 PrefabLoader: 预制体资产加载器
- 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink

## 预制体编辑模式
- 支持双击 .prefab 文件进入编辑模式
- 预制体编辑模式工具栏 (保存/退出)
- 预制体实例指示器和操作菜单

## 编辑器 UX 改进
- SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航
- 支持双击实体名称内联编辑
- 删除实体时显示子节点数量警告
- 右键菜单添加重命名/复制选项及快捷键提示
- 布局持久化和重置功能

## Bug 修复
- 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题
- 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见
- 修复 Inspector 资源字段高度不正确问题

* feat(editor): 改进编辑器 UX 交互体验

- ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计
- SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮
- PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理
- EntityInspector: 组件折叠状态持久化、属性搜索清除按钮
- Viewport: 变换操作实时数值显示
- 国际化: 添加相关文本 (en/zh)

* fix(build): 修复 Web 构建资产加载和编辑器 UX 改进

构建系统修复:
- 修复 asset-catalog.json 字段名不匹配 (entries vs assets)
- 修复 BrowserFileSystemService 支持两种目录格式
- 修复 bundle 策略检测逻辑 (空对象判断)
- 修复 module.json 中 assetExtensions 声明和类型推断

行为树修复:
- 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath
- 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree)

编辑器 UX 改进:
- 构建完成对话框添加"打开文件夹"按钮
- 构建完成对话框样式优化 (圆形图标背景、按钮布局)
- SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列)
- SceneHierarchy 隐藏滚动条

错误追踪:
- 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log)
- 添加 append_to_log Tauri 命令

* feat(render): 修复 UI 渲染和点击特效系统

## UI 渲染修复
- 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数
- 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap
- Web 运行时添加 assetPathResolver 支持 GUID 解析
- UIInteractableComponent.blockEvents 默认值改为 false

## 点击特效系统
- 新增 ClickFxComponent 和 ClickFxSystem
- 支持在点击位置播放粒子效果
- 支持多种触发模式和粒子轮换

## Camera 系统重构
- CameraSystem 从 ecs-engine-bindgen 移至 camera 包
- 新增 CameraManager 统一管理相机

## 编辑器改进
- 改进属性面板 UI 交互
- 粒子编辑器面板优化
- Transform 命令系统

* feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层

- 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay)
- 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性
- 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染
- 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer
- 更新粒子编辑器面板支持新的排序属性
- 优化 UI 渲染系统使用新的排序层级

* feat(ci): 集成 SignPath 代码签名服务

- 添加 SignPath 自动签名工作流(Windows)
- 配置 release-editor.yml 支持代码签名
- 将构建改为草稿模式,等待签名完成后发布
- 添加证书文件到 .gitignore 防止泄露

* fix(asset): 修复 Web 构建资产路径解析和全局单例移除

## 资产路径修复
- 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接
- BrowserPathResolver 支持两种模式:
  - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser)
  - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建)
- BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理

## 架构改进 - 移除全局单例
- 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入
- 移除 globalPathResolver 导出,改用 PathResolutionService
- 移除 globalPathResolutionService 导出
- ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖
- EngineService 使用 new AssetManager() 替代全局实例

## 新增服务
- PathResolutionService: 统一路径解析接口
- RuntimeModeService: 运行时模式查询服务
- SerializationContext: EntityRef 序列化上下文

## 其他改进
- 完善 ServiceToken 注释说明本地定义的意图
- 导出 BrowserPathResolveMode 类型

* fix(build): 添加 world-streaming composite 设置修复类型检查

* fix(build): 移除 world-streaming 引用避免 composite 冲突

* fix(build): 将 const enum 改为 enum 兼容 isolatedModules

* fix(build): 添加缺失的 IAssetManager 导入
2025-12-13 19:44:08 +08:00
YHH
a716d8006c fix(build): 修复 Web 构建组件注册和用户脚本打包问题 (#302)
* refactor(build): 重构 Web 构建管线,支持配置驱动的 Import Maps

- 重构 WebBuildPipeline 支持 split-bundles 和 single-bundle 两种构建模式
- 使用 module.json 的 isCore 字段识别核心模块,消除硬编码列表
- 动态生成 Import Map,从模块清单的 name 字段获取包名映射
- 动态扫描 module.json 文件,不再依赖固定模块列表
- 添加 HTTP 服务器启动脚本 (start-server.bat/sh) 支持 ESM 模块
- 更新 BuildSettingsPanel UI 支持新的构建模式选项
- 添加多语言支持 (zh/en/es)

* fix(build): 修复 Web 构建组件注册和用户脚本打包问题

主要修复:
- 修复组件反序列化时找不到类型的问题
- @ECSComponent 装饰器现在自动注册到 ComponentRegistry
- 添加未使用装饰器的组件警告
- 构建管线自动扫描用户脚本(无需入口文件)

架构改进:
- 解决 Decorators ↔ ComponentRegistry 循环依赖
- 新建 ComponentTypeUtils.ts 作为底层无依赖模块
- 移除冗余的防御性 register 调用
- 统一 ComponentType 定义位置

* refactor(build): 统一 WASM 配置架构,移除硬编码

- 新增 wasmConfig 统一配置替代 wasmPaths/wasmBindings
- wasmConfig.files 支持多候选源路径和明确目标路径
- wasmConfig.runtimePath 指定运行时加载路径
- 重构 _copyWasmFiles 使用统一配置
- HTML 生成使用配置中的 runtimePath
- 移除 physics-rapier2d 的冗余 WASM 配置(由 rapier2d 负责)
- IBuildFileSystem 新增 deleteFile 方法

* feat(build): 单文件构建模式完善和场景配置驱动

## 主要改动

### 单文件构建(single-file mode)
- 修复 WASM 初始化问题,支持 initSync 同步初始化
- 配置驱动的 WASM 识别,通过 wasmConfig.isEngineCore 标识核心引擎模块
- 从 wasmConfig.files 动态获取 JS 绑定路径,消除硬编码

### 场景配置
- 构建验证:必须选择至少一个场景才能构建
- 自动扫描:项目加载时扫描 scenes 目录
- 抽取 _filterScenesByWhitelist 公共方法统一过滤逻辑

### 构建面板优化
- availableScenes prop 传递场景列表
- 场景复选框可点击切换启用状态
- 移除动态 import,使用 prop 传入数据

* chore(build): 补充构建相关的辅助改动

- 添加 BuildFileSystemService 的 listFilesByExtension 优化
- 更新 module.json 添加 externalDependencies 配置
- BrowserRuntime 支持 wasmModule 参数传递
- GameRuntime 添加 loadSceneFromData 方法
- Rust 构建命令更新
- 国际化文案更新

* feat(build): 持久化构建设置到项目配置

## 设计架构

### ProjectService 扩展
- 新增 BuildSettingsConfig 接口定义构建配置字段
- ProjectConfig 添加 buildSettings 字段
- 新增 getBuildSettings / updateBuildSettings 方法

### BuildSettingsPanel
- 组件挂载时从 projectService 加载已保存配置
- 设置变化时自动保存(500ms 防抖)
- 场景选择状态与项目配置同步

### 配置保存位置
保存在项目的 ecs-editor.config.json 中:
- scenes: 选中的场景列表
- buildMode: 构建模式
- companyName/productName/version: 产品信息
- developmentBuild/sourceMap: 构建选项

* fix(editor): Ctrl+S 仅在主编辑区域触发保存场景

- 模态窗口打开时跳过(构建设置、设置、关于等)
- 焦点在 input/textarea/contenteditable 时跳过

* fix(tests): 修复 ECS 测试中 Component 注册问题

- 为所有测试 Component 类添加 @ECSComponent 装饰器
- 移除 beforeEach 中的 ComponentRegistry.reset() 调用
- 将内联 Component 类移到文件顶层以支持装饰器
- 更新测试预期值匹配新的组件类型名称
- 添加缺失的 HierarchyComponent 导入

所有 1388 个测试现已通过。
2025-12-10 18:23:29 +08:00
YHH
1b0d38edce feat(i18n): 统一国际化系统架构,支持插件独立翻译 (#301)
* feat(i18n): 统一国际化系统架构,支持插件独立翻译

## 主要改动

### 核心架构
- 增强 LocaleService,支持插件命名空间翻译扩展
- 新增 editor-runtime/i18n 模块,提供 createPluginLocale/createPluginTranslator
- 新增 editor-core/tokens.ts,定义 LocaleServiceToken 等服务令牌
- 改进 PluginAPI 类型安全,使用 ServiceToken<T> 替代 any

### 编辑器本地化
- 扩展 en.ts/zh.ts 翻译文件,覆盖所有 UI 组件
- 新增 es.ts 西班牙语支持
- 重构 40+ 组件使用 useLocale() hook

### 插件本地化系统
- behavior-tree-editor: 新增 locales/ 和 useBTLocale hook
- material-editor: 新增 locales/ 和 useMaterialLocale hook
- particle-editor: 新增 locales/ 和 useParticleLocale hook
- tilemap-editor: 新增 locales/ 和 useTilemapLocale hook
- ui-editor: 新增 locales/ 和 useUILocale hook

### 类型安全改进
- 修复 Debug 工具使用公共接口替代 as any
- 修复 ChunkStreamingSystem 添加 forEachChunk 公共方法
- 修复 blueprint-editor 移除不必要的向后兼容代码

* fix(behavior-tree-editor): 使用 ServiceToken 模式修复服务解析

- 创建 BehaviorTreeServiceToken 遵循"谁定义接口,谁导出Token"原则
- 使用 ServiceToken.id (symbol) 注册服务到 ServiceContainer
- 更新 PluginSDKRegistry.resolveService 支持 ServiceToken 检测
- BehaviorTreeEditorPanel 现在使用类型安全的 PluginAPI.resolve

* fix(behavior-tree-editor): 使用 ServiceContainer.resolve 获取类注册的服务

* fix: 修复多个包的依赖和类型问题

- core: EntityDataCollector.getEntityDetails 使用 HierarchySystem 获取父实体
- ui-editor: 添加 @esengine/editor-runtime 依赖
- tilemap-editor: 添加 @esengine/editor-runtime 依赖
- particle-editor: 添加 @esengine/editor-runtime 依赖
2025-12-09 18:04:03 +08:00
YHH
995fa2d514 refactor(arch): 改进 ServiceToken 设计,统一服务获取模式 (#300)
* refactor(arch): 移除全局变量,使用 ServiceToken 模式

- 创建 PluginServiceRegistry 类,提供类型安全的服务注册/获取
- 添加 ProfilerServiceToken 和 CollisionLayerConfigToken
- 重构所有 __PROFILER_SERVICE__ 全局变量访问为 getProfilerService()
- 重构 __PHYSICS_RAPIER2D__ 全局变量访问为 CollisionLayerConfigToken
- 在 Core 类添加 pluginServices 静态属性
- 添加 getService.ts 辅助模块简化服务获取

这是 ServiceToken 模式重构的第一阶段,移除了最常用的两个全局变量。
后续可继续应用到其他模块(Camera/Audio 等)。

* refactor(arch): 改进 ServiceToken 设计,移除重复常量

- tokens.ts: 从 engine-core 导入 createServiceToken(符合规范)
- tokens.ts: Token 使用接口 IProfilerService 而非具体类
- 移除 AssetPickerDialog 和 ContentBrowser 中重复的 MANAGED_ASSET_DIRECTORIES
- 统一从 editor-core 导入 MANAGED_ASSET_DIRECTORIES

* fix(type): 修复 IProfilerService 接口与实现类型不匹配

- 将 ProfilerData 等数据类型移到 tokens.ts 以避免循环依赖
- ProfilerService 显式实现 IProfilerService 接口
- 更新使用方使用 IProfilerService 接口类型而非具体类

* refactor(type): 移除类型重导出,改进类型安全

- 删除 ProfilerService.ts 中的类型重导出,消费方直接从 tokens.ts 导入
- PanelDescriptor 接口添加 titleZh 属性,移除 App.tsx 中的 as any
- 改进 useDynamicIcon.ts 的类型安全,使用正确的 Record 类型

* refactor(arch): 为模块添加 ServiceToken 支持

- Material System: 创建 tokens.ts,定义 IMaterialManager 接口和 MaterialManagerToken
- Audio: 创建预留 tokens.ts 文件,为未来 AudioManager 服务扩展做准备
- Camera: 创建预留 tokens.ts 文件,为未来 CameraManager 服务扩展做准备

遵循"谁定义接口,谁导出 Token"原则,统一服务访问模式
2025-12-09 11:07:44 +08:00
yhh
c71a47f2b0 docs: 更新文档链接到 esengine.cn 2025-12-09 09:09:45 +08:00
YHH
6c99b811ec refactor(editor): 统一配置管理、完善插件卸载和热更新同步 (#298)
主要变更:

1. 统一配置管理 (EditorConfig)
   - 新增 EditorConfig 集中管理路径、文件名、全局变量等配置
   - 添加 SDK 模块配置系统 (ISDKModuleConfig)
   - 重构 PluginSDKRegistry 使用配置而非硬编码

2. 完善插件卸载机制
   - 扩展 PluginRegisteredResources 追踪运行时资源
   - 实现完整的 deactivatePluginRuntime 清理流程
   - ComponentRegistry 添加 unregister/getRegisteredComponents 方法

3. 热更新同步机制
   - 新增 HotReloadCoordinator 协调热更新过程
   - 热更新期间暂停 ECS 循环避免竞态条件
   - 支持超时保护和失败恢复
2025-12-09 09:06:29 +08:00
yhh
40a38b8b88 docs: 更新品牌形象和项目文档
- 新增 ES logo (docs/public/logo.svg)
- README 添加居中 logo、徽章和导航链接
- 更新 LICENSE 版权为 ESEngine Contributors
- SECURITY.md 添加英文版本,更新联系方式
- 移除不稳定的性能测试
2025-12-08 21:47:47 +08:00
yhh
ad96edfad0 fix: 恢复 @esengine/ecs-framework 包名
上一个提交错误地将 npm 包名也改了,这里恢复正确的包名。
只更新 GitHub 仓库 URL,不改变 npm 包名。
2025-12-08 21:26:35 +08:00
yhh
240b165970 chore: 更新仓库 URL (ecs-framework → esengine)
仓库已从 esengine/ecs-framework 重命名为 esengine/esengine
更新所有引用旧 URL 的文件
2025-12-08 21:23:37 +08:00
YHH
c3b7250f85 refactor(plugin): 重构插件系统架构,统一类型导入路径 (#296)
* refactor(plugin): 重构插件系统架构,统一类型导入路径

## 主要更改

### 新增 @esengine/plugin-types 包
- 提供打破循环依赖的最小类型定义
- 包含 ServiceToken, createServiceToken, PluginServiceRegistry, IEditorModuleBase

### engine-core 类型统一
- IPlugin<T> 泛型参数改为 T = unknown
- 所有运行时类型(IRuntimeModule, ModuleManifest, SystemContext)在此定义
- 新增 PluginServiceRegistry.ts 导出服务令牌相关类型

### editor-core 类型优化
- 重命名 IPluginLoader.ts 为 EditorModule.ts
- 新增 IEditorPlugin = IPlugin<IEditorModuleLoader> 类型别名
- 移除弃用别名 IPluginLoader, IRuntimeModuleLoader

### 编辑器插件更新
- 所有 9 个编辑器插件使用 IEditorPlugin 类型
- 统一从 editor-core 导入编辑器类型

### 服务令牌规范
- 各模块在 tokens.ts 中定义自己的服务接口和令牌
- 遵循"谁定义接口,谁导出 Token"原则

## 导入规范

| 场景 | 导入来源 |
|------|---------|
| 运行时模块 | @esengine/engine-core |
| 编辑器插件 | @esengine/editor-core |
| 服务令牌 | @esengine/engine-core |

* refactor(plugin): 完善服务令牌规范,统一运行时模块

## 更改内容

### 运行时模块优化
- particle: 使用 PluginServiceRegistry 获取依赖服务
- physics-rapier2d: 通过服务令牌注册/获取物理查询接口
- tilemap: 使用服务令牌获取物理系统依赖
- sprite: 导出服务令牌
- ui: 导出服务令牌
- behavior-tree: 使用服务令牌系统

### 资产系统增强
- IAssetManager 接口扩展
- 加载器使用服务令牌获取依赖

### 运行时核心
- GameRuntime 使用 PluginServiceRegistry
- 导出服务令牌相关类型

### 编辑器服务
- EngineService 适配新的服务令牌系统
- AssetRegistryService 优化

* fix: 修复 editor-app 和 behavior-tree-editor 中的类型引用

- editor-app/PluginLoader.ts: 使用 IPlugin 替代 IPluginLoader
- behavior-tree-editor: 使用 IEditorPlugin 替代 IPluginLoader

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

UIRuntimeModule 使用 EngineBridgeToken,需要声明对 ecs-engine-bindgen 的依赖

* fix(type): 解决 ServiceToken 跨包类型兼容性问题

- 在 engine-core 中直接定义 ServiceToken 和 PluginServiceRegistry
  而不是从 plugin-types 重新导出,确保 tsup 生成的类型声明
  以 engine-core 作为类型来源
- 移除 RuntimeResolver.ts 中的硬编码模块 ID 检查,
  改用 module.json 中的 name 配置
- 修复 pnpm-lock.yaml 中的依赖记录

* refactor(arch): 改进架构设计,移除硬编码

- 统一类型导出:editor-core 从 engine-core 导入 IEditorModuleBase
- RuntimeResolver: 将硬编码路径改为配置常量和搜索路径列表
- 添加跨平台安装路径支持(Windows/macOS/Linux)
- 使用 ENGINE_WASM_CONFIG 配置引擎 WASM 文件信息
- IBundlePackOptions 添加 preloadBundles 配置项

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

ParticleRuntimeModule 导入了 @esengine/ecs-engine-bindgen 的 tokens,
但 package.json 中未声明该依赖,导致 CI 构建失败。

* fix(physics-rapier2d): 移除不存在的 PhysicsSystemContext 导出

PhysicsRuntimeModule 中不存在该类型,导致 type-check 失败
2025-12-08 21:10:57 +08:00
yhh
2476379af1 docs: 更新 worker-generator 版本号为 v1.0.2 2025-12-08 18:34:49 +08:00
yhh
e0d659fe46 fix(worker-generator): 映射文件不再放入 workers 目录避免微信编译错误 2025-12-08 18:33:23 +08:00
yhh
9ff03c04f3 docs: 更新 worker-generator 版本号为 v1.0.1 2025-12-08 17:17:26 +08:00
yhh
7b45fbeab3 fix(worker-generator): 将 typescript 移至 dependencies 以修复运行时依赖问题 2025-12-08 17:12:30 +08:00
yhh
a733a53d3e chore(release): 准备发布 v2.3.2 和 worker-generator v1.0.0
- 更新 @esengine/ecs-framework 版本号到 2.3.2
- 更新中英文 changelog
2025-12-08 17:04:01 +08:00
YHH
dfd0dfc7f9 feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI (#297)
* feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI

- 新增 @esengine/worker-generator 包,用于从 WorkerEntitySystem 生成 Worker 文件
- WorkerEntitySystem 添加 workerScriptPath 配置项,支持预编译 Worker 脚本
- CLI 工具支持 --wechat 模式,自动转换 ES6+ 为 ES5 语法
- 修复微信小游戏 Worker 消息格式差异(res 直接是数据,无需 .data)
- 更新中英文文档,添加微信小游戏支持章节

* docs: 更新 changelog,添加 v2.3.1 说明并标注 v2.3.0 为废弃

* fix: 修复 CI 检查问题

- 移除 cli.ts 中未使用的 toKebabCase 函数
- 修复 generator.ts 中正则表达式的 ReDoS 风险(使用 [ \t] 替代 \s*)
- 更新 changelog 版本号(2.3.1 -> 2.3.2)

* docs: 移除未发布版本的 changelog 条目

* fix(worker-generator): 使用 TypeScript 编译器替代手写正则进行 ES5 转换

- 修复 CodeQL 检测的 ReDoS 安全问题
- 使用 ts.transpileModule 进行安全可靠的代码转换
- 移除所有可能导致回溯的正则表达式
2025-12-08 17:02:11 +08:00
YHH
52bbccd53c feat(particle): 添加粒子与 Rapier2D 物理碰撞集成 (#295)
* feat(particle): 添加粒子与 Rapier2D 物理碰撞集成

- 新增 Physics2DCollisionModule 模块,支持粒子与场景碰撞体交互
- 支持圆形重叠检测和射线检测两种模式
- 支持 Kill/Bounce/Stop 三种碰撞行为
- 修复 module.update() 参数顺序 bug
- 物理模块自动通过 context 注入,用户只需添加模块即可

* chore: update pnpm-lock.yaml for particle physics dependency

Update lockfile to include @esengine/physics-rapier2d as optional peer dependency.
2025-12-08 09:38:37 +08:00
YHH
d92c2a7b66 feat(asset): 增强资产管理系统和编辑器 UI (#291)
* fix(editor): 修复粒子实体创建和优化检视器

- 添加 effects 分类到右键菜单,修复粒子实体无法创建的问题
- 添加粒子效果的本地化标签
- 简化粒子组件检视器,优先显示资产文件选择
- 高级属性只在未选择资产时显示,且默认折叠
- 添加可折叠的属性分组提升用户体验

* fix(particle): 修复粒子系统在浏览器预览中的资产加载和渲染

- 添加粒子 Gizmo 支持,显示发射形状并响应 Transform 缩放/旋转
- 修复资产热重载:添加 reloadAsset() 方法和 assets:refresh 事件监听
- 修复 VectorFieldEditors 数值输入精度(step 改为 0.01)
- 修复浏览器预览中粒子资产加载失败的问题:
  - 将相对路径转换为绝对路径以正确复制资产文件
  - 使用原始 GUID 而非生成的 GUID 构建 asset catalog
  - 初始化全局 assetManager 单例的 catalog 和 loader
  - 在 GameRuntime 的 systemContext 中添加 engineIntegration
- 公开 AssetManager.initializeFromCatalog 方法供运行时使用

* feat(asset): 增强资产管理系统和编辑器 UI

主要改动:
- 添加 loaderType 字段支持显式指定加载器类型覆盖
- 添加 .particle 扩展名和类型映射
- 新增 MANAGED_ASSET_DIRECTORIES 常量和相关工具方法
- EngineService 使用全局 assetManager 并同步 AssetRegistry 数据
- 修复插件启用逻辑,defaultEnabled=true 的新插件不被旧配置禁用
- ContentBrowser 添加 GUID 管理目录指示和非托管目录警告
- AssetPickerDialog 和 AssetFileInspector UI 增强
2025-12-07 20:26:03 +08:00
YHH
568b327425 fix(particle): 修复粒子系统在浏览器预览中的资产加载和渲染 (#290)
* fix(editor): 修复粒子实体创建和优化检视器

- 添加 effects 分类到右键菜单,修复粒子实体无法创建的问题
- 添加粒子效果的本地化标签
- 简化粒子组件检视器,优先显示资产文件选择
- 高级属性只在未选择资产时显示,且默认折叠
- 添加可折叠的属性分组提升用户体验

* fix(particle): 修复粒子系统在浏览器预览中的资产加载和渲染

- 添加粒子 Gizmo 支持,显示发射形状并响应 Transform 缩放/旋转
- 修复资产热重载:添加 reloadAsset() 方法和 assets:refresh 事件监听
- 修复 VectorFieldEditors 数值输入精度(step 改为 0.01)
- 修复浏览器预览中粒子资产加载失败的问题:
  - 将相对路径转换为绝对路径以正确复制资产文件
  - 使用原始 GUID 而非生成的 GUID 构建 asset catalog
  - 初始化全局 assetManager 单例的 catalog 和 loader
  - 在 GameRuntime 的 systemContext 中添加 engineIntegration
- 公开 AssetManager.initializeFromCatalog 方法供运行时使用
2025-12-07 01:00:35 +08:00
YHH
1fb702169e feat(asset-system): 完善资源加载和场景资源管理 (#289)
- 添加 AudioLoader 支持音频资源加载 (mp3/wav/ogg/m4a/flac/aac)
- EngineIntegration 添加音频资源加载/卸载支持
- EngineIntegration 添加数据(JSON)资源加载/卸载支持
- SceneResourceManager 实现完整的引用计数机制
- SceneResourceManager 实现场景资源卸载,仅卸载无引用的资源
- 添加资源统计和引用计数查询接口
2025-12-06 14:47:35 +08:00
YHH
3617f40309 feat(asset): 统一资产引用使用 GUID 替代路径 (#287)
* feat(world-streaming): 添加世界流式加载系统

实现基于区块的世界流式加载系统,支持开放世界游戏:

运行时包 (@esengine/world-streaming):
- ChunkComponent: 区块实体组件,包含坐标、边界、状态
- StreamingAnchorComponent: 流式锚点组件(玩家/摄像机)
- ChunkLoaderComponent: 流式加载配置组件
- ChunkStreamingSystem: 区块加载/卸载调度系统
- ChunkCullingSystem: 区块可见性剔除系统
- ChunkManager: 区块生命周期管理服务
- SpatialHashGrid: 空间哈希网格
- ChunkSerializer: 区块序列化

编辑器包 (@esengine/world-streaming-editor):
- ChunkVisualizer: 区块可视化覆盖层
- ChunkLoaderInspectorProvider: 区块加载器检视器
- StreamingAnchorInspectorProvider: 流式锚点检视器
- WorldStreamingPlugin: 完整插件导出

* feat(asset): 统一资产引用使用 GUID 替代路径

将所有组件的资产引用字段从路径改为 GUID:
- SpriteComponent: texture -> textureGuid, material -> materialGuid
- SpriteAnimatorComponent: AnimationFrame.texture -> textureGuid
- UIRenderComponent: texture -> textureGuid
- UIButtonComponent: normalTexture -> normalTextureGuid 等
- AudioSourceComponent: clip -> clipGuid
- ParticleSystemComponent: 已使用 textureGuid

修复 AssetRegistryService 注册问题和路径规范化,
添加渲染系统的 GUID 解析支持。

* fix(sprite-editor): 更新 material 为 materialGuid

* fix(editor-app): 更新 AnimationFrame.texture 为 textureGuid
2025-12-06 14:08:48 +08:00
YHH
0c03b13d74 feat(world-streaming): 添加世界流式加载系统 (#288)
实现基于区块的世界流式加载系统,支持开放世界游戏:

运行时包 (@esengine/world-streaming):
- ChunkComponent: 区块实体组件,包含坐标、边界、状态
- StreamingAnchorComponent: 流式锚点组件(玩家/摄像机)
- ChunkLoaderComponent: 流式加载配置组件
- ChunkStreamingSystem: 区块加载/卸载调度系统
- ChunkCullingSystem: 区块可见性剔除系统
- ChunkManager: 区块生命周期管理服务
- SpatialHashGrid: 空间哈希网格
- ChunkSerializer: 区块序列化

编辑器包 (@esengine/world-streaming-editor):
- ChunkVisualizer: 区块可视化覆盖层
- ChunkLoaderInspectorProvider: 区块加载器检视器
- StreamingAnchorInspectorProvider: 流式锚点检视器
- WorldStreamingPlugin: 完整插件导出
2025-12-06 13:56:01 +08:00
YHH
3cbfa1e4cb fix(editor): 修复右键菜单和粒子编辑器问题 (#286)
- 修复右键菜单被状态栏遮挡的问题
- 修复右键菜单边界检测,考虑标题栏和状态栏高度
- 调整右键菜单结构:新建文件夹 → 资源类型 → 工具操作
- 修复 Particle 插件默认未启用的问题(defaultEnabled 的新插件不被旧配置禁用)
- 修复 SizeOverLifetime 模块在预览中无效果的问题
- 移除 MaterialEditorModule 中的重复模板注册
2025-12-06 11:56:25 +08:00
yhh
397f79caa5 docs: 更新 v2.3.0 版本文档和 changelog
- 添加 v2.3.0 changelog(中英文)
- 更新文档版本号从 v2.2.22+ 到 v2.3.0+
- 更新 package.json 版本至 2.3.0
- vitepress 配置添加 ignoreDeadLinks
2025-12-06 10:44:08 +08:00
yhh
972c1d5357 Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-12-06 10:15:13 +08:00
YHH
32d35ef2ee feat(particle): 添加完整粒子系统和粒子编辑器 (#284)
* feat(editor-core): 添加用户系统自动注册功能

- IUserCodeService 新增 registerSystems/unregisterSystems/getRegisteredSystems 方法
- UserCodeService 实现系统检测、实例化和场景注册逻辑
- ServiceRegistry 在预览开始时注册用户系统,停止时移除
- 热更新时自动重新加载用户系统
- 更新 System 脚本模板添加 @ECSSystem 装饰器

* feat(editor-core): 添加编辑器脚本支持(Inspector/Gizmo)

- registerEditorExtensions 实际注册用户 Inspector 和 Gizmo
- 添加 unregisterEditorExtensions 方法
- ServiceRegistry 在项目加载时编译并加载编辑器脚本
- 项目关闭时自动清理编辑器扩展
- 添加 Inspector 和 Gizmo 脚本创建模板

* feat(particle): 添加粒子系统和粒子编辑器

新增两个包:
- @esengine/particle: 粒子系统核心库
- @esengine/particle-editor: 粒子编辑器 UI

粒子系统功能:
- ECS 组件架构,支持播放/暂停/重置控制
- 7种发射形状:点、圆、环、矩形、边缘、线、锥形
- 5个动画模块:颜色渐变、缩放曲线、速度控制、旋转、噪声
- 纹理动画模块支持精灵表动画
- 3种混合模式:Normal、Additive、Multiply
- 11个内置预设:火焰、烟雾、爆炸、雨、雪等
- 对象池优化,支持粒子复用

编辑器功能:
- 实时 Canvas 预览,支持全屏和鼠标跟随
- 点击触发爆发效果(用于测试爆炸类特效)
- 渐变编辑器:可视化颜色关键帧编辑
- 曲线编辑器:支持缩放曲线和缓动函数
- 预设浏览器:快速应用内置预设
- 模块开关:独立启用/禁用各个模块
- Vector2 样式输入(重力 X/Y)

* feat(particle): 完善粒子系统核心功能

1. Burst 定时爆发系统
   - BurstConfig 接口支持时间、数量、循环次数、间隔
   - 运行时自动处理定时爆发
   - 支持无限循环爆发

2. 速度曲线模块 (VelocityOverLifetimeModule)
   - 6种曲线类型:Constant、Linear、EaseIn、EaseOut、EaseInOut、Custom
   - 自定义关键帧曲线支持
   - 附加速度 X/Y
   - 轨道速度和径向速度

3. 碰撞边界模块 (CollisionModule)
   - 矩形和圆形边界类型
   - 3种碰撞行为:Kill、Bounce、Wrap
   - 反弹系数和最小速度阈值
   - 反弹时生命损失

* feat(particle): 添加力场模块、碰撞模块和世界/本地空间支持

- 新增 ForceFieldModule 支持风力、吸引点、漩涡、湍流四种力场类型
- 新增 SimulationSpace 枚举支持世界空间和本地空间切换
- ParticleSystemComponent 集成力场模块和空间模式
- 粒子编辑器添加 Collision 和 ForceField 模块的 UI 编辑支持
- 新增 Vortex、Leaves、Bouncing 三个预设展示新功能
- 编辑器预览实现完整的碰撞和力场效果

* fix(particle): 移除未使用的 transform 循环变量
2025-12-05 23:03:31 +08:00
yhh
57e165779e Merge branch 'master' of https://github.com/esengine/ecs-framework 2025-12-05 22:59:24 +08:00
YHH
690d7859c8 feat(core): 添加持久化实体支持跨场景迁移 (#285)
实现实体生命周期策略,允许标记实体为持久化,在场景切换时自动迁移到新场景。

主要变更:
- 新增 EEntityLifecyclePolicy 枚举(SceneLocal/Persistent)
- Entity 添加 setPersistent()、setSceneLocal()、isPersistent API
- Scene 添加 findPersistentEntities()、extractPersistentEntities()、receiveMigratedEntities()
- SceneManager.setScene() 自动处理持久化实体迁移
- 添加完整的中英文文档和 21 个测试用例
2025-12-05 22:54:41 +08:00
yhh
8f9a7d8581 feat(core): 添加持久化实体支持跨场景迁移
实现实体生命周期策略,允许标记实体为持久化,在场景切换时自动迁移到新场景。

主要变更:
- 新增 EEntityLifecyclePolicy 枚举(SceneLocal/Persistent)
- Entity 添加 setPersistent()、setSceneLocal()、isPersistent API
- Scene 添加 findPersistentEntities()、extractPersistentEntities()、receiveMigratedEntities()
- SceneManager.setScene() 自动处理持久化实体迁移
- 添加完整的中英文文档和 21 个测试用例
2025-12-05 22:46:53 +08:00
YHH
3d5fcc1a55 feat(editor-core): 添加用户系统自动注册功能 (#283)
* feat(editor-core): 添加用户系统自动注册功能

- IUserCodeService 新增 registerSystems/unregisterSystems/getRegisteredSystems 方法
- UserCodeService 实现系统检测、实例化和场景注册逻辑
- ServiceRegistry 在预览开始时注册用户系统,停止时移除
- 热更新时自动重新加载用户系统
- 更新 System 脚本模板添加 @ECSSystem 装饰器

* feat(editor-core): 添加编辑器脚本支持(Inspector/Gizmo)

- registerEditorExtensions 实际注册用户 Inspector 和 Gizmo
- 添加 unregisterEditorExtensions 方法
- ServiceRegistry 在项目加载时编译并加载编辑器脚本
- 项目关闭时自动清理编辑器扩展
- 添加 Inspector 和 Gizmo 脚本创建模板
2025-12-05 18:28:11 +08:00
YHH
823e0c1d94 feat(engine-core): 添加统一输入系统 (#282)
* perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令

ReactiveQuery 快照优化:
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照

CommandBuffer 延迟命令系统:
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 commands 属性
- 命令在帧末统一执行,避免迭代过程中修改实体列表

Scene 更新:
- 在 lateUpdate 后自动刷新所有系统的命令缓冲区

文档:
- 更新系统文档,添加 CommandBuffer 使用说明

* fix(ci): upgrade first-interaction action to v1.3.0

Fix Docker build failure in welcome workflow.

* fix(ci): upgrade pnpm/action-setup to v4 and fix unused import

- Upgrade pnpm/action-setup@v2 to v4 in all workflow files
- Remove unused CommandType import in CommandBuffer.test.ts

* fix(ci): remove duplicate pnpm version specification

* feat(engine-core): 添加统一输入系统

添加完整的输入系统,支持平台抽象:

- IPlatformInputSubsystem: 扩展接口支持键盘/鼠标/滚轮事件
- WebInputSubsystem: 浏览器实现,支持事件绑定/解绑
- InputManager: 全局输入状态管理器(键盘、鼠标、触摸)
- InputSystem: ECS 系统,连接平台事件到 InputManager
- GameRuntime 集成: 自动创建 InputSystem 并绑定平台子系统

使用方式:
```typescript
import { Input, MouseButton } from '@esengine/engine-core';

if (Input.isKeyDown('KeyW')) { /* 移动 */ }
if (Input.isKeyJustPressed('Space')) { /* 跳跃 */ }
if (Input.isMouseButtonDown(MouseButton.Left)) { /* 射击 */ }
```

* fix(runtime-core): 添加缺失的 platform-common 依赖

* fix(runtime-core): 移除 platform-web 依赖避免循环依赖

* fix(runtime-core): 使用工厂函数注入 InputSubsystem 避免循环依赖

- BrowserPlatformAdapter 通过 inputSubsystemFactory 配置接收输入子系统
- 在 IPlatformInputSubsystem 接口添加可选的 dispose 方法
- 移除对 @esengine/platform-web 的直接依赖
2025-12-05 18:15:50 +08:00
YHH
13a149c3a2 perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令 (#281)
* perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令

ReactiveQuery 快照优化:
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照

CommandBuffer 延迟命令系统:
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 commands 属性
- 命令在帧末统一执行,避免迭代过程中修改实体列表

Scene 更新:
- 在 lateUpdate 后自动刷新所有系统的命令缓冲区

文档:
- 更新系统文档,添加 CommandBuffer 使用说明

* fix(ci): upgrade first-interaction action to v1.3.0

Fix Docker build failure in welcome workflow.

* fix(ci): upgrade pnpm/action-setup to v4 and fix unused import

- Upgrade pnpm/action-setup@v2 to v4 in all workflow files
- Remove unused CommandType import in CommandBuffer.test.ts

* fix(ci): remove duplicate pnpm version specification
2025-12-05 17:24:33 +08:00
github-actions[bot]
dd130eacb0 chore(editor): bump version to 1.0.14 (#280)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-05 16:45:52 +08:00
YHH
d0238add2d fix: 当 esbuild 未打包时创建占位文件避免 Tauri 构建失败 (#276) 2025-12-05 16:24:26 +08:00
yhh
fe96d72ac6 docs: 补充 v2.2.21 changelog 中遗漏的迭代安全修复 (#272) 2025-12-05 16:17:18 +08:00
yhh
b2b8df9340 chore(core): release v2.2.21
- 优化 HierarchySystem 性能 (#279)
- 更新 changelog
2025-12-05 16:10:24 +08:00
YHH
2d56eaf11a perf(core): 优化 HierarchySystem 避免每帧遍历所有实体 (#279)
使用脏实体集合代替每帧遍历所有实体,静态场景下 process() 从 O(n) 优化为 O(1)。

性能提升:
- 1000 实体静态场景: 81.79μs -> 0.07μs (快 1168 倍)
- 10000 实体静态场景: 939.43μs -> 0.56μs (快 1677 倍)
- 服务端模拟 (100房间 x 100实体): 2.7ms -> 1.4ms 每 tick

改动:
- 新增 dirtyEntities Set 追踪需要更新缓存的实体
- process() 只遍历脏实体
- markCacheDirty() 将实体加入脏集合
- onAdded() 在实体注册时将脏实体加入集合
- onRemoved() 将实体从脏集合移除
2025-12-05 16:07:53 +08:00
YHH
2cb9c471f9 fix(docs): 修正 v2.2.16 组件生命周期描述 (#278) 2025-12-05 16:01:07 +08:00
YHH
e8fc7f497b docs: 添加 core 库更新日志 (v2.2.16 - v2.2.20) (#277) 2025-12-05 15:10:14 +08:00
YHH
6702f0bfad feat(editor): 完善用户代码热更新和环境检测 (#275)
* fix: 更新 bundle-runtime 脚本使用正确的 platform-web 输出文件

原脚本引用的 runtime.browser.js 不存在,改为使用 index.mjs

* feat(editor): 完善用户代码热更新和环境检测

## 热更新改进
- 添加 hotReloadInstances() 方法,通过更新原型链实现真正的热更新
- 组件实例保留数据,仅更新方法
- ComponentRegistry 支持热更新时替换同名组件类

## 环境检测
- 启动时检测 esbuild 可用性
- 在启动页面底部显示环境状态指示器
- 添加 check_environment Rust 命令和前端 API

## esbuild 打包
- 将 esbuild 二进制文件打包到应用中
- 用户无需全局安装 esbuild
- 支持 Windows/macOS/Linux 平台

## 文件监视优化
- 添加 300ms 防抖,避免重复编译
- 修复路径分隔符混合问题

## 资源打包修复
- 修复 Tauri 资源配置,保留 engine 目录结构
- 添加 src-tauri/bin/ 和 src-tauri/engine/ 到 gitignore

* fix: 将热更新模式改为可选,修复测试失败

- ComponentRegistry 添加 hotReloadEnabled 标志,默认禁用
- 只有启用热更新模式时才会替换同名组件类
- 编辑器环境自动启用热更新模式
- reset() 方法中重置热更新标志

* test: 添加热更新模式的测试用例
2025-12-05 14:24:09 +08:00
YHH
d7454e3ca4 feat(engine): 添加编辑器模式标志控制编辑器UI显示 (#274)
* feat(engine): 添加编辑器模式标志控制编辑器UI显示

- 在 Rust 引擎中添加 isEditor 标志,控制网格、gizmos、坐标轴指示器的显示
- 运行时模式下自动隐藏所有编辑器专用 UI
- 编辑器预览和浏览器运行时通过 setEditorMode(false) 禁用编辑器 UI
- 添加 Scene.isEditorMode 延迟组件生命周期回调,直到 begin() 调用
- 修复用户组件注册到 Core ComponentRegistry 以支持序列化
- 修复 Run in Browser 时用户组件加载问题

* fix: 复制引擎模块的类型定义文件到 dist/engine

* fix: 修复用户项目 tsconfig paths 类型定义路径

- 从 module.json 读取实际包名而不是使用目录名
- 修复 .d.ts 文件复制逻辑,支持 .mjs 扩展名
2025-12-04 22:43:26 +08:00
YHH
0d9bab910e feat(editor): 实现用户脚本编译加载和自动重编译 (#273) 2025-12-04 19:32:51 +08:00
YHH
3d16bbdc64 fix: 修复 process/lateProcess 迭代时组件变化导致跳过实体的问题 (#272)
- 在 update() 和 lateUpdate() 中创建实体数组副本,防止迭代过程中数组被修改
- lateUpdate() 现在重新查询实体以获取 update 阶段添加的新实体
- 添加 lawn-mower-demo 场景测试用例验证修复
- 更新中英文文档说明 onAdded/onRemoved 同步调用时机和 process/lateProcess 安全性
2025-12-04 15:11:01 +08:00
YHH
b4e7ba2abd fix: 修复项目切换时运行时和系统重复初始化问题 (#267)
* feat(editor): 添加 GitHub Discussions 社区论坛功能

* chore: 更新 pnpm-lock.yaml

* chore: 删除测试图片

* refactor: 改用 Imgur 图床上传图片

* fix: 修复项目切换时运行时和系统重复初始化问题

* fix: 修复多个编辑器问题

* feat: 添加脚本编辑器配置和类型定义支持

* feat: 实现资产 .meta 文件自动生成功能
2025-12-04 14:04:39 +08:00
github-actions[bot]
374b26f7c6 chore(core): release v2.2.20 (#271)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-04 13:01:47 +08:00
YHH
dbebb4f4fb fix: 修复系统 onAdded 回调受注册顺序影响的问题 (#270) 2025-12-04 12:56:19 +08:00
github-actions[bot]
eec89b626c chore(core): release v2.2.19 (#268)
Co-authored-by: esengine <18465053+esengine@users.noreply.github.com>
2025-12-04 11:36:12 +08:00
yhh
763d23e960 fix: 改用 Imgur 图床并删除测试图片 2025-12-04 09:57:33 +08:00
YHH
3b56ed17fe feat(editor): 添加 GitHub Discussions 社区论坛功能 (#266)
* feat(editor): 添加 GitHub Discussions 社区论坛功能

* chore: 更新 pnpm-lock.yaml
2025-12-04 09:51:04 +08:00
YHH
4b8d22ac32 Upload forum image: 1764812524764-ic2nic.png 2025-12-04 09:42:06 +08:00
YHH
9cd873da14 Merge pull request #260 from esengine/fix/docs-dead-links
fix(docs): 修复英文文档dead links问题
2025-12-03 23:02:48 +08:00
yhh
c1799bf7b3 fix(docs): 修复英文文档dead links问题 2025-12-03 23:01:31 +08:00
YHH
85be826b62 Merge pull request #259 from esengine/feat/docs-i18n-clean
feat(docs): 添加中英文国际化支持
2025-12-03 22:51:54 +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
YHH
ddc7a7750e Chore/lint fixes (#212)
* fix(eslint): 修复装饰器缩进配置

* fix(eslint): 修复装饰器缩进配置

* chore: 删除未使用的导入

* chore(lint): 移除未使用的导入和变量

* chore(lint): 修复editor-app中未使用的函数参数

* chore(lint): 修复未使用的赋值变量

* chore(eslint): 将所有错误级别改为警告以通过CI

* fix(codeql): 修复GitHub Advanced Security检测到的问题
2025-11-02 23:50:41 +08:00
YHH
50a01d9dd3 chore: 统一并强化ESLint配置规则 2025-11-02 12:45:47 +08:00
YHH
793aad0a5e chore: 移除旧版ESLint配置并更新子模块 2025-11-02 12:22:27 +08:00
YHH
9c1bf8dbed refactor(core): 移除全局EventBus,实现场景级事件隔离 (#211) 2025-11-01 18:19:23 +08:00
YHH
620f3eecc7 style(core): ESLint自动修复代码格式问题 (#210) 2025-11-01 17:41:50 +08:00
YHH
4355538d8d refactor(core): 重构Scene和Entity的代码质量,消除所有Lint警告 (#209) 2025-11-01 17:18:12 +08:00
YHH
3ad5dc9ca3 refactor(core): 改进事件系统类型安全并消除 ESLint 警告 (#208) 2025-11-01 16:12:18 +08:00
YHH
57c7e7be3f feat(core):统一 Core 库的命名规范和代码风格 (#207) 2025-11-01 10:23:46 +08:00
2455 changed files with 423796 additions and 89508 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

57
.changeset/config.json Normal file
View File

@@ -0,0 +1,57 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "esengine/esengine" }
],
"commit": false,
"fixed": [],
"linked": [
["@esengine/ecs-framework", "@esengine/ecs-framework-math"]
],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": [
"@esengine/engine-core",
"@esengine/runtime-core",
"@esengine/asset-system",
"@esengine/material-system",
"@esengine/ecs-engine-bindgen",
"@esengine/script-runtime",
"@esengine/platform-common",
"@esengine/platform-web",
"@esengine/platform-wechat",
"@esengine/sprite",
"@esengine/camera",
"@esengine/particle",
"@esengine/tilemap",
"@esengine/mesh-3d",
"@esengine/effect",
"@esengine/audio",
"@esengine/fairygui",
"@esengine/physics-rapier2d",
"@esengine/rapier2d",
"@esengine/world-streaming",
"@esengine/editor-core",
"@esengine/editor-runtime",
"@esengine/editor-app",
"@esengine/sprite-editor",
"@esengine/camera-editor",
"@esengine/particle-editor",
"@esengine/tilemap-editor",
"@esengine/mesh-3d-editor",
"@esengine/fairygui-editor",
"@esengine/physics-rapier2d-editor",
"@esengine/behavior-tree-editor",
"@esengine/blueprint-editor",
"@esengine/asset-system-editor",
"@esengine/material-editor",
"@esengine/shader-editor",
"@esengine/world-streaming-editor",
"@esengine/node-editor",
"@esengine/sdk",
"@esengine/worker-generator",
"@esengine/engine"
]
}

View File

@@ -1,50 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "single", { "avoidEscape": true }],
"indent": ["error", 4, { "SwitchCase": 1 }],
"no-trailing-spaces": "error",
"eol-last": ["error", "always"],
"comma-dangle": ["error", "none"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"arrow-parens": ["error", "always"],
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
"no-console": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-non-null-assertion": "off"
},
"ignorePatterns": [
"node_modules/",
"dist/",
"bin/",
"build/",
"coverage/",
"thirdparty/",
"examples/lawn-mower-demo/",
"extensions/",
"*.min.js",
"*.d.ts"
]
}

2
.github/FUNDING.yml vendored
View File

@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://github.com/esengine/ecs-framework/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/ecs-framework/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
custom: ['https://github.com/esengine/esengine/blob/master/sponsor/alipay.jpg', 'https://github.com/esengine/esengine/blob/master/sponsor/wechatpay.png'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -5,7 +5,7 @@ contact_links:
about: 查看完整文档和教程 / View full documentation and tutorials
- name: 🤖 AI 文档助手 / AI Documentation Assistant
url: https://deepwiki.com/esengine/ecs-framework
url: https://deepwiki.com/esengine/esengine
about: 使用 AI 助手快速找到答案 / Use AI assistant to quickly find answers
- name: 💬 QQ 交流群 / QQ Group
@@ -13,5 +13,5 @@ contact_links:
about: 加入社区交流群 / Join the community group
- name: 🌟 GitHub Discussions
url: https://github.com/esengine/ecs-framework/discussions
url: https://github.com/esengine/esengine/discussions
about: 参与社区讨论 / Join community discussions

View File

@@ -8,12 +8,12 @@ body:
value: |
💡 提示:如果是简单问题,可以先查看:
- [📚 文档](https://esengine.github.io/ecs-framework/)
- [📖 AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
- [📖 AI 文档助手](https://deepwiki.com/esengine/esengine)
- [💬 QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
💡 Tip: For simple questions, please check first:
- [📚 Documentation](https://esengine.github.io/ecs-framework/)
- [📖 AI Documentation](https://deepwiki.com/esengine/ecs-framework)
- [📖 AI Documentation](https://deepwiki.com/esengine/esengine)
- type: textarea
id: question

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

@@ -0,0 +1,13 @@
name: "CodeQL Config"
# Paths to exclude from analysis
paths-ignore:
- thirdparty
- "**/node_modules"
- "**/dist"
- "**/bin"
- "**/tests"
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/test"
- "**/__tests__"

32
.github/labeler.yml vendored
View File

@@ -1,32 +0,0 @@
# 自动标签配置
# 根据 issue/PR 内容自动打标签
'bug':
- '/(bug|错误|崩溃|crash|error|exception|问题)/i'
'enhancement':
- '/(feature|功能|enhancement|improve|优化|建议)/i'
'documentation':
- '/(doc|文档|readme|guide|tutorial|教程)/i'
'question':
- '/(question|疑问|how to|如何|怎么)/i'
'performance':
- '/(performance|性能|slow|慢|lag|卡顿|optimize)/i'
'core':
- '/(@esengine\/ecs-framework|packages\/core|core package)/i'
'editor':
- '/(editor|编辑器|tauri)/i'
'network':
- '/(network|网络|multiplayer|多人)/i'
'help wanted':
- '/(help wanted|需要帮助|求助)/i'
'good first issue':
- '/(good first issue|新手友好|beginner)/i'

View File

@@ -1,73 +0,0 @@
name: AI Batch Analyze Issues
on:
workflow_dispatch:
inputs:
mode:
description: '分析模式'
required: true
type: choice
options:
- 'recent' # 最近 10 个 issue
- 'open' # 所有打开的 issue
- 'all' # 所有 issue慎用
default: 'recent'
permissions:
issues: write
contents: read
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install GitHub CLI
run: |
gh --version || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh)
- name: Batch Analyze Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MODE="${{ github.event.inputs.mode }}"
# 获取 issue 列表
if [ "$MODE" = "recent" ]; then
echo "📊 分析最近 10 个 issue..."
ISSUES=$(gh issue list --limit 10 --json number --jq '.[].number')
elif [ "$MODE" = "open" ]; then
echo "📊 分析所有打开的 issue..."
ISSUES=$(gh issue list --state open --json number --jq '.[].number')
else
echo "📊 分析所有 issue这可能需要很长时间..."
ISSUES=$(gh issue list --state all --limit 100 --json number --jq '.[].number')
fi
# 为每个 issue 添加 AI 分析评论
for issue_num in $ISSUES; do
echo "🤖 分析 Issue #$issue_num..."
# 获取 issue 内容
ISSUE_BODY=$(gh issue view $issue_num --json body --jq '.body')
ISSUE_TITLE=$(gh issue view $issue_num --json title --jq '.title')
# 添加触发评论
gh issue comment $issue_num --body "@ai-helper 请帮我分析这个 issue" || true
# 避免 API 限制
sleep 2
done
echo "✅ 批量分析完成!"
echo "查看结果https://github.com/${{ github.repository }}/issues"

View File

@@ -1,61 +0,0 @@
name: AI Helper Tip
# 对所有新创建的 issue 自动回复 AI 助手使用说明(新老用户都适用)
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
tip:
runs-on: ubuntu-latest
steps:
- name: Post AI Helper Usage Tip
uses: actions/github-script@v7
with:
script: |
const message = [
"## 🤖 AI 助手可用 | AI Helper Available",
"",
"**中文说明:**",
"",
"本项目配备了 AI 智能助手,可以帮助你快速获得解答!",
"",
"**使用方法:** 在评论中提及 `@ai-helper`AI 会自动搜索项目代码并提供解决方案。",
"",
"**示例:**",
"```",
"@ai-helper 如何创建一个新的 System",
"@ai-helper 这个报错是什么原因?",
"```",
"",
"---",
"",
"**English:**",
"",
"This project has an AI assistant to help you get answers quickly!",
"",
"**How to use:** Mention `@ai-helper` in a comment, and AI will automatically search the codebase and provide solutions.",
"",
"**Examples:**",
"```",
"@ai-helper How do I create a new System?",
"@ai-helper What causes this error?",
"```",
"",
"---",
"",
"💡 *AI 助手基于代码库提供建议,复杂问题建议等待维护者回复*",
"💡 *AI suggestions are based on the codebase. For complex issues, please wait for maintainer responses*"
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
console.log('✅ AI helper tip posted successfully');

View File

@@ -1,85 +0,0 @@
name: AI Issue Helper
on:
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
jobs:
ai-helper:
runs-on: ubuntu-latest
# 只在真实用户提到 @ai-helper 时触发,忽略机器人评论
if: |
contains(github.event.comment.body, '@ai-helper') &&
github.event.comment.user.type != 'Bot'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Issue Details
id: issue
uses: actions/github-script@v7
with:
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// 限制长度,避免超过 token 限制
const maxLength = 1000;
const truncate = (str, max) => {
if (!str) return '';
return str.length > max ? str.substring(0, max) + '...[内容过长已截断]' : str;
};
core.exportVariable('ISSUE_TITLE', truncate(issue.data.title || '', 200));
core.exportVariable('ISSUE_BODY', truncate(issue.data.body || '', maxLength));
core.exportVariable('COMMENT_BODY', truncate(context.payload.comment.body || '', 500));
core.exportVariable('ISSUE_NUMBER', context.issue.number);
- name: Create Prompt
id: prompt
run: |
cat > prompt.txt << 'PROMPT_EOF'
Issue #${{ env.ISSUE_NUMBER }}
标题: ${{ env.ISSUE_TITLE }}
内容: ${{ env.ISSUE_BODY }}
评论: ${{ env.COMMENT_BODY }}
请搜索项目代码并提供解决方案。
PROMPT_EOF
- name: AI Analysis
uses: actions/ai-inference@v1
id: ai
with:
model: 'gpt-4o'
enable-github-mcp: true
max-tokens: 1500
system-prompt: |
你是 ECS Framework (TypeScript ECS 框架) 的 AI 助手。
主要代码在 packages/core/src。
搜索相关代码后,用中文简洁回答问题,包含问题分析、解决方案和代码引用。
prompt-file: prompt.txt
- name: Post AI Response
env:
AI_RESPONSE: ${{ steps.ai.outputs.response }}
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: process.env.AI_RESPONSE
});

View File

@@ -1,56 +0,0 @@
name: AI Issue Moderator
on:
issues:
types: [opened]
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: Check Content
uses: actions/ai-inference@v1
id: check
with:
model: 'gpt-4o-mini'
system-prompt: |
你是一个内容审查助手。
检查内容是否包含:
1. 垃圾信息或广告
2. 恶意或攻击性内容
3. 与项目完全无关的内容
只返回 "SPAM" 或 "OK",不要其他内容。
prompt: |
标题:${{ github.event.issue.title || github.event.comment.body }}
内容:
${{ github.event.issue.body || github.event.comment.body }}
- name: Mark as Spam
if: contains(steps.check.outputs.response, 'SPAM')
uses: actions/github-script@v7
with:
script: |
// 添加 spam 标签
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['spam']
});
// 添加评论
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '🤖 这个内容被 AI 检测为可能的垃圾内容。如果这是误判,请联系维护者。\n\n🤖 This content was detected as potential spam by AI. If this is a false positive, please contact the maintainers.'
});

View File

@@ -1,160 +0,0 @@
name: Batch Label Issues
on:
workflow_dispatch:
inputs:
mode:
description: '标签模式'
required: true
type: choice
options:
- 'recent' # 最近 20 个 issue
- 'open' # 所有打开的 issue
- 'unlabeled' # 只处理没有标签的 issue
- 'all' # 所有 issue慎用
default: 'recent'
skip_labeled:
description: '跳过已有标签的 issue'
required: false
type: boolean
default: true
permissions:
issues: write
contents: read
jobs:
batch-label:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Batch Label Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MODE="${{ github.event.inputs.mode }}"
SKIP_LABELED="${{ github.event.inputs.skip_labeled }}"
echo "📊 开始批量打标签..."
echo "模式: $MODE"
echo "跳过已标签: $SKIP_LABELED"
# 获取 issue 列表
if [ "$MODE" = "recent" ]; then
echo "📋 获取最近 20 个 issue..."
ISSUES=$(gh issue list --limit 20 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "open" ]; then
echo "📋 获取所有打开的 issue..."
ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "unlabeled" ]; then
echo "📋 获取没有标签的 issue..."
ISSUES=$(gh issue list --state all --json number,labels,title,body --jq '.[] | select(.labels | length == 0) | {number, labels: [.labels[].name], title, body}')
else
echo "📋 获取所有 issue限制 100 个)..."
ISSUES=$(gh issue list --state all --limit 100 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
fi
# 临时文件
echo "$ISSUES" > /tmp/issues.json
# 处理每个 issue
cat /tmp/issues.json | jq -c '.' | while read -r issue; do
ISSUE_NUM=$(echo "$issue" | jq -r '.number')
EXISTING_LABELS=$(echo "$issue" | jq -r '.labels | join(",")')
TITLE=$(echo "$issue" | jq -r '.title')
BODY=$(echo "$issue" | jq -r '.body')
echo ""
echo "🔍 处理 Issue #$ISSUE_NUM: $TITLE"
echo " 现有标签: $EXISTING_LABELS"
# 跳过已有标签的 issue
if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
echo " ⏭️ 跳过(已有标签)"
continue
fi
# 分析内容并打标签
LABELS_TO_ADD=""
# 检测 bug
if echo "$TITLE $BODY" | grep -iE "(bug|错误|崩溃|crash|error|exception|问题|fix)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD bug"
echo " 🐛 检测到: bug"
fi
# 检测 feature request
if echo "$TITLE $BODY" | grep -iE "(feature|功能|enhancement|improve|优化|建议|新增|添加|add)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD enhancement"
echo " ✨ 检测到: enhancement"
fi
# 检测 question
if echo "$TITLE $BODY" | grep -iE "(question|疑问|how to|如何|怎么|为什么|why|咨询|\?|)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD question"
echo " ❓ 检测到: question"
fi
# 检测 documentation
if echo "$TITLE $BODY" | grep -iE "(doc|文档|readme|guide|tutorial|教程|说明)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD documentation"
echo " 📖 检测到: documentation"
fi
# 检测 performance
if echo "$TITLE $BODY" | grep -iE "(performance|性能|slow|慢|lag|卡顿|optimize|优化)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD performance"
echo " ⚡ 检测到: performance"
fi
# 检测 core
if echo "$TITLE $BODY" | grep -iE "(@esengine/ecs-framework|packages/core|core package|核心包)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD core"
echo " 🎯 检测到: core"
fi
# 检测 editor
if echo "$TITLE $BODY" | grep -iE "(editor|编辑器|tauri)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD editor"
echo " 🎨 检测到: editor"
fi
# 检测 network
if echo "$TITLE $BODY" | grep -iE "(network|网络|multiplayer|多人|同步)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD network"
echo " 🌐 检测到: network"
fi
# 检测 help wanted
if echo "$TITLE $BODY" | grep -iE "(help wanted|需要帮助|求助)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD help wanted"
echo " 🆘 检测到: help wanted"
fi
# 添加标签
if [ ! -z "$LABELS_TO_ADD" ]; then
echo " ✅ 添加标签: $LABELS_TO_ADD"
for label in $LABELS_TO_ADD; do
gh issue edit $ISSUE_NUM --add-label "$label" 2>&1 | grep -v "already exists" || true
done
echo " 💬 添加说明评论..."
gh issue comment $ISSUE_NUM --body $'🤖 自动标签系统检测到此 issue 并添加了相关标签。如有误判,请告知维护者。\n\n🤖 Auto-labeling system detected and labeled this issue. Please let maintainers know if this is incorrect.' || true
else
echo " 未检测到明确类型"
fi
# 避免 API 限制
sleep 1
done
echo ""
echo "✅ 批量标签完成!"
echo "查看结果: https://github.com/${{ github.repository }}/issues"

View File

@@ -6,85 +6,107 @@ on:
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
pull_request:
branches: [ master, main, develop ]
paths:
- 'packages/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
# Run on all PRs to satisfy branch protection, but skip build if no code changes
jobs:
test:
# Check if we need to run the full CI
check-changes:
runs-on: ubuntu-latest
outputs:
should-run: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
ci:
needs: check-changes
if: needs.check-changes.outputs.should-run == 'true'
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- 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 --no-frozen-lockfile
- name: Type check
run: npm run type-check
# 构建 framework 包 (可独立发布的通用库,无外部依赖)
- name: Build framework packages
run: |
pnpm --filter @esengine/ecs-framework build
pnpm --filter @esengine/ecs-framework-math build
pnpm --filter @esengine/behavior-tree build
pnpm --filter @esengine/blueprint build
pnpm --filter @esengine/fsm build
pnpm --filter @esengine/timer build
pnpm --filter @esengine/spatial build
pnpm --filter @esengine/procgen build
pnpm --filter @esengine/pathfinding build
pnpm --filter @esengine/network-protocols build
pnpm --filter @esengine/network build
- name: Lint check
run: npm run lint
# 类型检查 (仅 framework 包)
- name: Type check (framework packages)
run: pnpm run type-check:framework
- name: Build core package first
run: npm run build:core
# Lint 检查 (仅 framework 包)
- name: Lint check (framework packages)
run: pnpm run lint:framework
# 测试 (仅 framework 包)
- name: Run tests with coverage
run: npm run test:ci
run: pnpm run test:ci:framework
- 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 包 (core 和 math)
- name: Build npm packages
run: |
pnpm run build:npm:core
pnpm run build:npm:math
# 上传构建产物
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
bin/
dist/
retention-days: 7
packages/framework/**/dist/
packages/framework/**/bin/
retention-days: 7

View File

@@ -1,146 +0,0 @@
name: Cleanup Old Dependabot PRs
# 手动触发的 workflow用于清理堆积的 Dependabot PR
on:
workflow_dispatch:
inputs:
days_old:
description: '关闭多少天前创建的 PR默认 7 天)'
required: false
default: '7'
dry_run:
description: '试运行模式true=仅显示,不关闭)'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: List and Close Old Dependabot PRs
uses: actions/github-script@v7
with:
script: |
const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
console.log('---');
// 获取所有 Dependabot PR
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
const dependabotPRs = pulls.filter(pr =>
pr.user.login === 'dependabot[bot]' &&
new Date(pr.created_at) < cutoffDate
);
console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
console.log('');
if (dependabotPRs.length === 0) {
console.log('✅ 没有需要清理的 PR');
return;
}
// 按类型分组
const byType = {
dev: [],
prod: [],
actions: [],
other: []
};
for (const pr of dependabotPRs) {
const title = pr.title.toLowerCase();
const labels = pr.labels.map(l => l.name);
let type = 'other';
if (title.includes('dev-dependencies') || title.includes('development')) {
type = 'dev';
} else if (title.includes('production-dependencies')) {
type = 'prod';
} else if (labels.includes('github-actions')) {
type = 'actions';
}
byType[type].push(pr);
}
console.log('📋 PR 分类统计:');
console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
console.log(` ❓ 其他: ${byType.other.length} 个`);
console.log('');
// 处理每个 PR
for (const pr of dependabotPRs) {
const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
console.log(` 链接: ${pr.html_url}`);
if (!dryRun) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `🤖 **自动关闭旧的 Dependabot PR**
此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
📌 **下一步:**
- Dependabot 已配置为月度运行,届时会创建新的分组更新
- 新的 Mergify 规则会智能处理不同类型的依赖更新
- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
- 生产依赖需要 CI 通过才会自动合并
如果需要立即应用此更新,请手动更新依赖。
---
*此操作由仓库维护者手动触发的清理工作流执行*`
});
console.log(' ✅ 已关闭并添加说明');
} else {
console.log(' 试运行模式 - 未执行操作');
}
console.log('');
}
console.log('---');
if (dryRun) {
console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
} else {
console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
}

View File

@@ -14,32 +14,36 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- 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
cd packages/framework/core
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
files: ./packages/framework/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
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: packages/core/coverage/
path: packages/framework/core/coverage/

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,18 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
- 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,29 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
- 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

@@ -1,23 +0,0 @@
name: Issue Labeler
on:
issues:
types: [opened, edited]
permissions:
issues: write
contents: read
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Label Issues
uses: github/issue-labeler@v3.4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler.yml
enable-versioned-regex: 1

View File

@@ -1,28 +0,0 @@
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
permissions:
issues: write
jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Translate Issues
uses: tomsun28/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
# 设置为 true 会修改标题false 只在评论中添加翻译
CUSTOM_BOT_NOTE: |
<details>
<summary>🌏 Translation / 翻译</summary>
Bot detected the issue body's language is not English, translate it automatically.
机器人检测到 issue 内容非英文,自动翻译。
</details>

View File

@@ -0,0 +1,70 @@
name: Release (Changesets)
on:
push:
branches:
- master
paths:
- '.changeset/**'
- 'packages/*/package.json'
- 'packages/*/*/package.json'
- 'packages/*/*/*/package.json'
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build framework packages
run: |
# Only build packages managed by Changesets (not in ignore list)
pnpm --filter "@esengine/ecs-framework" build
pnpm --filter "@esengine/ecs-framework-math" build
pnpm --filter "@esengine/behavior-tree" build
pnpm --filter "@esengine/blueprint" build
pnpm --filter "@esengine/fsm" build
pnpm --filter "@esengine/timer" build
pnpm --filter "@esengine/spatial" build
pnpm --filter "@esengine/procgen" build
pnpm --filter "@esengine/pathfinding" build
pnpm --filter "@esengine/network-protocols" build
pnpm --filter "@esengine/network" build
pnpm --filter "@esengine/cli" build
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
version: pnpm changeset:version
publish: pnpm changeset:publish
title: 'chore: release packages'
commit: 'chore: release packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -33,11 +33,14 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- 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
@@ -47,7 +50,7 @@ jobs:
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/editor-app/src-tauri
workspaces: packages/editor/editor-app/src-tauri
cache-on-failure: true
- name: Install dependencies (Ubuntu)
@@ -57,61 +60,154 @@ 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
cd packages/editor/editor-app
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/engine/ecs-engine-bindgen/src/wasm
cp packages/rust/engine/pkg/es_engine.js packages/engine/ecs-engine-bindgen/src/wasm/
cp packages/rust/engine/pkg/es_engine.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
cp packages/rust/engine/pkg/es_engine_bg.wasm packages/engine/ecs-engine-bindgen/src/wasm/
cp packages/rust/engine/pkg/es_engine_bg.wasm.d.ts packages/engine/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/editor-app
node scripts/bundle-runtime.mjs
- name: Build Tauri app
id: tauri
uses: tauri-apps/tauri-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
projectPath: packages/editor-app
projectPath: packages/editor/editor-app
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
releaseBody: 'See the assets to download this version and install.'
releaseDraft: false
releaseDraft: true
prerelease: false
includeUpdaterJson: true
updaterJsonKeepUniversal: false
args: ${{ matrix.platform == 'macos-latest' && format('--target {0}', matrix.target) || '' }}
# 构建成功后,创建 PR 更新版本号
update-version-pr:
# Windows 构建上传 artifact 供 SignPath 签名
- name: Upload Windows artifacts for signing
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-unsigned
path: |
packages/editor/editor-app/src-tauri/target/release/bundle/nsis/*.exe
packages/editor/editor-app/src-tauri/target/release/bundle/msi/*.msi
retention-days: 1
# SignPath 代码签名Windows
# SignPath OSS code signing for Windows
#
# 配置步骤 | Setup Steps:
# 1. 在 SignPath 门户创建项目 | Create project in SignPath portal
# 2. 导入 .signpath/artifact-configuration.xml | Import artifact configuration
# 3. 使用 'test-signing' 策略测试 | Use 'test-signing' policy for testing
# 生产环境改为 'release-signing' | Change to 'release-signing' for production
# 4. 配置 GitHub Secrets | Configure GitHub Secrets:
# - SIGNPATH_API_TOKEN: API token from SignPath
# - SIGNPATH_ORGANIZATION_ID: Your organization ID
#
# 文档 | Documentation: https://about.signpath.io/documentation/trusted-build-systems/github
sign-windows:
needs: build-tauri
if: github.event_name == 'workflow_dispatch' && success()
runs-on: ubuntu-latest
# 只有在构建成功时才运行 | Only run on successful build
if: success()
steps:
- name: Check SignPath configuration
id: check-signpath
run: |
if [ -n "${{ secrets.SIGNPATH_API_TOKEN }}" ] && [ -n "${{ secrets.SIGNPATH_ORGANIZATION_ID }}" ]; then
echo "enabled=true" >> $GITHUB_OUTPUT
echo "SignPath is configured, proceeding with code signing"
else
echo "enabled=false" >> $GITHUB_OUTPUT
echo "SignPath secrets not configured, skipping code signing"
echo "To enable: add SIGNPATH_API_TOKEN and SIGNPATH_ORGANIZATION_ID secrets"
fi
- name: Checkout
if: steps.check-signpath.outputs.enabled == 'true'
uses: actions/checkout@v4
- name: Get artifact ID
if: steps.check-signpath.outputs.enabled == 'true'
id: get-artifact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# 获取 windows-unsigned artifact 的 ID
ARTIFACT_ID=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
if [ -z "$ARTIFACT_ID" ]; then
echo "Error: Could not find artifact 'windows-unsigned'"
exit 1
fi
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
echo "Found artifact ID: $ARTIFACT_ID"
- name: Submit to SignPath for code signing
if: steps.check-signpath.outputs.enabled == 'true'
id: signpath
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: 'ecs-framework'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: 'initial'
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 600
output-artifact-directory: './signed'
- name: Upload signed artifacts to release
if: steps.check-signpath.outputs.enabled == 'true'
uses: softprops/action-gh-release@v1
with:
files: ./signed/*
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 构建成功后,创建 PR 更新版本号
# Create PR to update version after successful build
update-version-pr:
needs: [build-tauri, sign-windows]
# 即使签名跳过也要运行 | Run even if signing is skipped
if: github.event_name == 'workflow_dispatch' && !failure()
runs-on: ubuntu-latest
steps:
@@ -125,8 +221,8 @@ jobs:
- name: Update version files
run: |
cd packages/editor-app
npm version ${{ github.event.inputs.version }} --no-git-tag-version
cd packages/editor/editor-app
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 +234,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/editor-app/package.json` → `${{ github.event.inputs.version }}`
- Updated `packages/editor/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

@@ -1,29 +1,43 @@
name: Release NPM Packages
on:
# Tag trigger: supports v* and {package}-v* formats
push:
tags:
- 'v*'
- 'core-v*'
- 'behavior-tree-v*'
- 'editor-core-v*'
- 'node-editor-v*'
- 'blueprint-v*'
- 'tilemap-v*'
- 'physics-rapier2d-v*'
- 'worker-generator-v*'
# Manual trigger option
workflow_dispatch:
inputs:
package:
description: '选择要发布的包'
description: 'Select package to publish'
required: true
type: choice
options:
- core
- behavior-tree
- editor-core
- node-editor
- blueprint
- tilemap
- physics-rapier2d
- worker-generator
version_type:
description: '版本更新类型'
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
- custom
custom_version:
description: '自定义版本号 (仅当选择 custom 时使用,例如: 2.2.9)'
required: false
type: string
permissions:
contents: write
@@ -32,7 +46,7 @@ permissions:
jobs:
release-package:
name: Release ${{ github.event.inputs.package }} Package
name: Release Package
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -41,72 +55,171 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Parse tag or input
id: parse
run: |
if [ "${{ github.event_name }}" = "push" ]; then
# Parse package and version from tag
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
# Parse format: v1.0.0 or package-v1.0.0
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
PACKAGE="core"
VERSION="${BASH_REMATCH[1]}"
elif [[ "$TAG" =~ ^([a-z-]+)-v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
PACKAGE="${BASH_REMATCH[1]}"
VERSION="${BASH_REMATCH[2]}"
else
echo "::error::Invalid tag format: $TAG"
echo "Expected: v1.0.0 or package-v1.0.0"
exit 1
fi
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "mode=tag" >> $GITHUB_OUTPUT
echo "Package: $PACKAGE"
echo "Version: $VERSION"
else
# Manual trigger: read from package.json and bump version
PACKAGE="${{ github.event.inputs.package }}"
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
echo "mode=manual" >> $GITHUB_OUTPUT
echo "version_type=${{ github.event.inputs.version_type }}" >> $GITHUB_OUTPUT
fi
- name: Install pnpm
uses: pnpm/action-setup@v4
- 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' }}
- name: Verify version (tag mode)
if: steps.parse.outputs.mode == 'tag'
run: |
cd packages/core
npm run build
PACKAGE="${{ steps.parse.outputs.package }}"
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
# - name: Run tests
# run: |
# cd packages/${{ github.event.inputs.package }}
# npm run test:ci
# Get version from package.json
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
- name: Update version
if [ "$EXPECTED_VERSION" != "$ACTUAL_VERSION" ]; then
echo "::error::Version mismatch!"
echo "Tag version: $EXPECTED_VERSION"
echo "package.json version: $ACTUAL_VERSION"
echo ""
echo "Please update packages/$PACKAGE/package.json to version $EXPECTED_VERSION before tagging."
exit 1
fi
echo "Version verified: $EXPECTED_VERSION"
- name: Bump version (manual mode)
if: steps.parse.outputs.mode == 'manual'
id: bump
run: |
PACKAGE="${{ steps.parse.outputs.package }}"
cd packages/$PACKAGE
CURRENT=$(node -p "require('./package.json').version")
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ steps.parse.outputs.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
# Update package.json
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 "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Bumped version: $CURRENT -> $NEW_VERSION"
- name: Set final version
id: version
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
if [ "${{ steps.parse.outputs.mode }}" = "tag" ]; then
echo "value=${{ steps.parse.outputs.version }}" >> $GITHUB_OUTPUT
else
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
echo "value=${{ steps.bump.outputs.version }}" >> $GITHUB_OUTPUT
fi
NEW_VERSION=$(node -p "require('./package.json').version")
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "发布版本: $NEW_VERSION"
- name: Build core package (if needed)
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
run: |
cd packages/framework/core
pnpm run build
- name: Build node-editor package (if needed for blueprint)
if: ${{ steps.parse.outputs.package == 'blueprint' }}
run: |
cd packages/node-editor
pnpm run build
- name: Build package
run: |
cd packages/${{ github.event.inputs.package }}
npm run build:npm
cd packages/${{ steps.parse.outputs.package }}
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
cd packages/${{ steps.parse.outputs.package }}/dist
pnpm publish --access public --no-git-checks
- name: Create Pull Request
- name: Create GitHub Release (tag mode)
if: steps.parse.outputs.mode == 'tag'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.parse.outputs.tag }}
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
make_latest: false
body: |
## @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
**NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
```bash
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
```
---
*Auto-released by GitHub Actions*
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request (manual mode)
if: steps.parse.outputs.mode == 'manual'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(${{ github.event.inputs.package }}): release v${{ steps.version.outputs.new_version }}"
branch: release/${{ github.event.inputs.package }}-v${{ steps.version.outputs.new_version }}
commit-message: "chore(${{ steps.parse.outputs.package }}): release v${{ steps.version.outputs.value }}"
branch: release/${{ steps.parse.outputs.package }}-v${{ steps.version.outputs.value }}
delete-branch: true
title: "chore(${{ github.event.inputs.package }}): Release v${{ steps.version.outputs.new_version }}"
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
body: |
## 🚀 Release v${{ steps.version.outputs.new_version }}
## Release v${{ steps.version.outputs.value }}
此 PR 更新 `@esengine/${{ github.event.inputs.package }}` 包的版本号
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
### 变更
- ✅ 已发布到 npm: [@esengine/${{ github.event.inputs.package }}@${{ steps.version.outputs.new_version }}](https://www.npmjs.com/package/@esengine/${{ github.event.inputs.package }}/v/${{ steps.version.outputs.new_version }})
- ✅ 更新 `packages/${{ github.event.inputs.package }}/package.json` `${{ steps.version.outputs.new_version }}`
### Changes
- Published to npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
- Updated `packages/${{ steps.parse.outputs.package }}/package.json` to `${{ steps.version.outputs.value }}`
---
*此 PR 由发布工作流自动创建*
*This PR was automatically created by the release workflow*
labels: |
release
${{ github.event.inputs.package }}
${{ steps.parse.outputs.package }}
automated pr

View File

@@ -1,43 +0,0 @@
name: Size Limit
on:
pull_request:
branches:
- master
- main
paths:
- 'packages/core/src/**'
- 'packages/core/package.json'
- '.size-limit.json'
permissions:
contents: read
pull-requests: write
issues: write
jobs:
size:
runs-on: ubuntu-latest
steps:
- name: Checkout
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 core package
run: |
cd packages/core
npm run build:npm
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
skip_step: install

View File

@@ -1,58 +0,0 @@
name: Welcome
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
welcome:
runs-on: ubuntu-latest
steps:
- name: Welcome new contributors
uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
👋 你好!感谢你提交第一个 issue
我们会尽快查看并回复。同时,建议你:
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/ecs-framework)
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
---
👋 Hello! Thanks for opening your first issue!
We'll review it as soon as possible. Meanwhile, you might want to:
- 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/ecs-framework)
pr-message: |
👋 你好!感谢你提交第一个 Pull Request
在我们 Review 之前,请确保:
- ✅ 代码遵循项目规范
- ✅ 通过所有测试
- ✅ 更新了相关文档
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
查看完整的[贡献指南](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md)。
---
👋 Hello! Thanks for your first Pull Request!
Before we review, please ensure:
- ✅ Code follows project conventions
- ✅ All tests pass
- ✅ Documentation is updated
- ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
See the full [Contributing Guide](https://github.com/esengine/ecs-framework/blob/master/CONTRIBUTING.md).

20
.gitignore vendored
View File

@@ -16,6 +16,10 @@ dist/
*.tmp
*.temp
.cache/
.build-cache/
# Turborepo
.turbo/
# IDE 配置
.idea/
@@ -44,13 +48,21 @@ logs/
.env.test.local
.env.production.local
# 代码签名证书(敏感文件)
certs/
*.pfx
*.p12
*.cer
*.pem
*.key
# 测试覆盖率
coverage/
*.lcov
# 包管理器锁文件保留npm的,忽略其他的
# 包管理器锁文件(忽略yarn保留pnpm
yarn.lock
pnpm-lock.yaml
package-lock.json
# 文档生成
docs/api/
@@ -78,3 +90,7 @@ docs/.vitepress/dist/
# Tauri 捆绑输出
**/src-tauri/target/release/bundle/
**/src-tauri/target/debug/bundle/
# Rust 构建产物
**/engine-shared/target/
external/

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**: 文档
@@ -122,9 +119,9 @@ npm run format
## 问题反馈 / Issue Reporting
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/ecs-framework/issues/new)。
如果你发现了 bug 或有新功能建议,请[创建 Issue](https://github.com/esengine/esengine/issues/new)。
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/ecs-framework/issues/new).
If you find a bug or have a feature request, please [create an issue](https://github.com/esengine/esengine/issues/new).
## 许可证 / License

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 ECS Framework
Copyright (c) 2025 ESEngine Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

424
README.md
View File

@@ -1,90 +1,93 @@
# ECS Framework
<h1 align="center">
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
<br>
ESEngine
</h1>
[![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)
<p align="center">
<strong>Modular Game Framework for TypeScript</strong>
</p>
<div align="center">
<p align="center">
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
</p>
<p>一个高性能的 TypeScript ECS (Entity-Component-System) 框架,专为现代游戏开发而设计。</p>
<p align="center">
<b>English</b> | <a href="./README_CN.md">中文</a>
</p>
<p>A high-performance TypeScript ECS (Entity-Component-System) framework designed for modern game development.</p>
</div>
<p align="center">
<a href="https://esengine.cn/">Documentation</a> ·
<a href="https://esengine.cn/api/README">API Reference</a> ·
<a href="./examples/">Examples</a>
</p>
---
## 📊 项目统计 / Project Stats
## What is ESEngine?
<div align="center">
ESEngine is a collection of **engine-agnostic game development modules** for TypeScript. Use them with Cocos Creator, Laya, Phaser, PixiJS, or any JavaScript game engine.
[![Star History Chart](https://api.star-history.com/svg?repos=esengine/ecs-framework&type=Date)](https://star-history.com/#esengine/ecs-framework&Date)
</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平台
## 安装
The core is a high-performance **ECS (Entity-Component-System)** framework, accompanied by optional modules for AI, networking, physics, and more.
```bash
npm install @esengine/ecs-framework
```
## 快速开始
## Features
| Module | Description | Engine Required |
|--------|-------------|:---------------:|
| **ECS Core** | Entity-Component-System framework with reactive queries | No |
| **Behavior Tree** | AI behavior trees with visual editor support | No |
| **Blueprint** | Visual scripting system | No |
| **FSM** | Finite state machine | No |
| **Timer** | Timer and cooldown systems | No |
| **Spatial** | Spatial indexing and queries (QuadTree, Grid) | No |
| **Pathfinding** | A* and navigation mesh pathfinding | No |
| **Network** | Client/server networking with TSRPC | No |
> All framework modules can be used standalone with any rendering engine.
## Quick Start
### Using CLI (Recommended)
The easiest way to add ECS to your existing project:
```bash
# In your project directory
npx @esengine/cli init
```
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code.
### Manual Setup
```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';
// 定义组件
// Define components (data only)
@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;
}
// 创建系统
// Define system (logic)
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
@@ -93,182 +96,201 @@ 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());
// Initialize
Core.create();
const scene = new Scene();
scene.addSystem(new MovementSystem());
const player = this.createEntity("Player");
player.addComponent(new Position(100, 100));
player.addComponent(new Velocity(50, 0));
const player = scene.createEntity('Player');
player.addComponent(new Position());
player.addComponent(new Velocity());
Core.setScene(scene);
// Integrate with your game loop
function gameLoop(currentTime: number) {
Core.update(currentTime / 1000);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## Using with Other Engines
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
### With Cocos Creator
```typescript
import { Component as CCComponent, _decorator } from 'cc';
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends CCComponent {
private ecsScene!: Scene;
start() {
Core.create();
this.ecsScene = new Scene();
// Add ECS systems
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
this.ecsScene.addSystem(new MyGameSystem());
Core.setScene(this.ecsScene);
}
update(dt: number) {
Core.update(dt);
}
}
```
// 启动游戏
Core.create();
Core.setScene(new GameScene());
### With Laya 3.x
// 游戏循环中更新
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import { FSMSystem } from '@esengine/fsm';
const { regClass } = Laya;
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new Scene();
onAwake(): void {
Core.create();
this.ecsScene.addSystem(new FSMSystem());
Core.setScene(this.ecsScene);
}
onUpdate(): void {
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
Core.destroy();
}
}
```
## 核心特性
## Packages
- **实体查询** - 使用 Matcher API 进行高效的实体过滤
- **事件系统** - 类型安全的事件发布/订阅机制
- **性能优化** - SoA 存储优化,支持大规模实体处理
- **多线程支持** - Worker系统实现真正的并行计算充分利用多核CPU
- **多场景** - 支持 World/Scene 分层架构
- **时间管理** - 内置定时器和时间控制系统
### Framework (Engine-Agnostic)
## 🏗️ 架构设计 / Architecture
These packages have **zero rendering dependencies** and work with any engine:
```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 工作线程系统]
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
```bash
npm install @esengine/ecs-framework # Core ECS
npm install @esengine/behavior-tree # AI behavior trees
npm install @esengine/blueprint # Visual scripting
npm install @esengine/fsm # State machines
npm install @esengine/timer # Timers & cooldowns
npm install @esengine/spatial # Spatial indexing
npm install @esengine/pathfinding # Pathfinding
npm install @esengine/network # Networking
```
## 平台支持
### ESEngine Runtime (Optional)
支持主流游戏引擎和 Web 平台:
If you want a complete engine solution with rendering:
- **Cocos Creator**
- **Laya 引擎**
- **原生 Web** - 浏览器环境直接运行
- **小游戏平台** - 微信、支付宝等小游戏
| Category | Packages |
|----------|----------|
| **Core** | `engine-core`, `asset-system`, `material-system` |
| **Rendering** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
| **Physics** | `physics-rapier2d` |
| **Platform** | `platform-web`, `platform-wechat` |
## ECS Framework Editor
### Editor (Optional)
跨平台桌面编辑器,提供可视化开发和调试工具。
A visual editor built with Tauri for scene management:
### 主要功能
- Download from [Releases](https://github.com/esengine/esengine/releases)
- Supports behavior tree editing, tilemap painting, visual scripting
- **场景管理** - 可视化场景层级和实体管理
- **组件检视** - 实时查看和编辑实体组件
- **性能分析** - 内置 Profiler 监控系统性能
- **插件系统** - 可扩展的插件架构
- **远程调试** - 连接运行中的游戏进行实时调试
- **自动更新** - 支持热更新,自动获取最新版本
## Project Structure
### 下载
```
esengine/
├── packages/
│ ├── framework/ # Engine-agnostic modules (NPM publishable)
│ │ ├── core/ # ECS Framework
│ │ ├── math/ # Math utilities
│ │ ├── behavior-tree/ # AI behavior trees
│ │ ├── blueprint/ # Visual scripting
│ │ ├── fsm/ # Finite state machine
│ │ ├── timer/ # Timer system
│ │ ├── spatial/ # Spatial queries
│ │ ├── pathfinding/ # Pathfinding
│ │ ├── procgen/ # Procedural generation
│ │ └── network/ # Networking
│ │
│ ├── engine/ # ESEngine runtime
│ ├── rendering/ # Rendering modules
│ ├── physics/ # Physics modules
│ ├── editor/ # Visual editor
│ └── rust/ # WASM renderer
├── docs/ # Documentation
└── examples/ # Examples
```
[![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)
## Building from Source
支持 Windows、macOS (Intel & Apple Silicon)
```bash
git clone https://github.com/esengine/esengine.git
cd esengine
### 截图
pnpm install
pnpm build
<img src="screenshots/main_screetshot.png" alt="ECS Framework Editor" width="800">
# Type check framework packages
pnpm type-check:framework
<details>
<summary>查看更多截图</summary>
# Run tests
pnpm test
```
**性能分析器**
<img src="screenshots/performance_profiler.png" alt="Performance Profiler" width="600">
## Documentation
**插件管理**
<img src="screenshots/plugin_manager.png" alt="Plugin Manager" width="600">
- [ECS Framework Guide](./packages/framework/core/README.md)
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
- [API Reference](https://esengine.cn/api/README)
**设置界面**
<img src="screenshots/settings.png" alt="Settings" width="600">
## Community
</details>
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
- [Discord](https://discord.gg/gCAgzXFW) - Chat with the community
## 示例项目
## Contributing
- [Worker系统演示](https://esengine.github.io/ecs-framework/demos/worker-system/) - 多线程物理系统演示,展示高性能并行计算
- [割草机演示](https://github.com/esengine/lawn-mower-demo) - 完整的游戏示例
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
## 文档
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
- [📚 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 文档
## License
## 生态系统
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
- [路径寻找](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
<p align="center">
Made with care by the ESEngine community
</p>

297
README_CN.md Normal file
View File

@@ -0,0 +1,297 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/esengine/esengine/master/docs/public/logo.svg" alt="ESEngine" width="180">
<br>
ESEngine
</h1>
<p align="center">
<strong>TypeScript 模块化游戏框架</strong>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@esengine/ecs-framework"><img src="https://img.shields.io/npm/v/@esengine/ecs-framework?style=flat-square&color=blue" alt="npm"></a>
<a href="https://github.com/esengine/esengine/actions"><img src="https://img.shields.io/github/actions/workflow/status/esengine/esengine/ci.yml?branch=master&style=flat-square" alt="build"></a>
<a href="https://github.com/esengine/esengine/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="license"></a>
<a href="https://github.com/esengine/esengine/stargazers"><img src="https://img.shields.io/github/stars/esengine/esengine?style=flat-square" alt="stars"></a>
<img src="https://img.shields.io/badge/TypeScript-5.0+-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
</p>
<p align="center">
<a href="./README.md">English</a> | <b>中文</b>
</p>
<p align="center">
<a href="https://esengine.cn/">文档</a> ·
<a href="https://esengine.cn/api/README">API 参考</a> ·
<a href="./examples/">示例</a>
</p>
---
## ESEngine 是什么?
ESEngine 是一套**引擎无关的游戏开发模块**,可与 Cocos Creator、Laya、Phaser、PixiJS 等任何 JavaScript 游戏引擎配合使用。
核心是一个高性能的 **ECS实体-组件-系统)** 框架,配套 AI、网络、物理等可选模块。
```bash
npm install @esengine/ecs-framework
```
## 功能模块
| 模块 | 描述 | 需要渲染引擎 |
|------|------|:----------:|
| **ECS 核心** | 实体-组件-系统框架,支持响应式查询 | 否 |
| **行为树** | AI 行为树,支持可视化编辑 | 否 |
| **蓝图** | 可视化脚本系统 | 否 |
| **状态机** | 有限状态机 | 否 |
| **定时器** | 定时器和冷却系统 | 否 |
| **空间索引** | 空间查询(四叉树、网格) | 否 |
| **寻路** | A* 和导航网格寻路 | 否 |
| **网络** | 客户端/服务端网络通信 (TSRPC) | 否 |
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
## 快速开始
### 使用 CLI推荐
在现有项目中添加 ECS 的最简单方式:
```bash
# 在项目目录中运行
npx @esengine/cli init
```
CLI 会自动检测项目类型Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js并生成相应的集成代码。
### 手动配置
```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);
// 集成到你的游戏循环
function gameLoop(currentTime: number) {
Core.update(currentTime / 1000);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
```
## 与其他引擎配合使用
ESEngine 的框架模块设计为可与你喜欢的渲染引擎配合使用:
### 与 Cocos Creator 配合
```typescript
import { Component as CCComponent, _decorator } from 'cc';
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends CCComponent {
private ecsScene!: Scene;
start() {
Core.create();
this.ecsScene = new Scene();
// 添加 ECS 系统
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
this.ecsScene.addSystem(new MyGameSystem());
Core.setScene(this.ecsScene);
}
update(dt: number) {
Core.update(dt);
}
}
```
### 与 Laya 3.x 配合
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import { FSMSystem } from '@esengine/fsm';
const { regClass } = Laya;
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new Scene();
onAwake(): void {
Core.create();
this.ecsScene.addSystem(new FSMSystem());
Core.setScene(this.ecsScene);
}
onUpdate(): void {
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
Core.destroy();
}
}
```
## 包列表
### 框架包(引擎无关)
这些包**零渲染依赖**,可与任何引擎配合使用:
```bash
npm install @esengine/ecs-framework # ECS 核心
npm install @esengine/behavior-tree # AI 行为树
npm install @esengine/blueprint # 可视化脚本
npm install @esengine/fsm # 状态机
npm install @esengine/timer # 定时器和冷却
npm install @esengine/spatial # 空间索引
npm install @esengine/pathfinding # 寻路
npm install @esengine/network # 网络
```
### ESEngine 运行时(可选)
如果你需要完整的引擎解决方案:
| 分类 | 包名 |
|------|------|
| **核心** | `engine-core`, `asset-system`, `material-system` |
| **渲染** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
| **物理** | `physics-rapier2d` |
| **平台** | `platform-web`, `platform-wechat` |
### 编辑器(可选)
基于 Tauri 构建的可视化编辑器:
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
- 支持行为树编辑、Tilemap 绘制、可视化脚本
## 项目结构
```
esengine/
├── packages/
│ ├── framework/ # 引擎无关模块(可发布到 NPM
│ │ ├── core/ # ECS 框架
│ │ ├── math/ # 数学工具
│ │ ├── behavior-tree/ # AI 行为树
│ │ ├── blueprint/ # 可视化脚本
│ │ ├── fsm/ # 有限状态机
│ │ ├── timer/ # 定时器系统
│ │ ├── spatial/ # 空间查询
│ │ ├── pathfinding/ # 寻路
│ │ ├── procgen/ # 程序化生成
│ │ └── network/ # 网络
│ │
│ ├── engine/ # ESEngine 运行时
│ ├── rendering/ # 渲染模块
│ ├── physics/ # 物理模块
│ ├── editor/ # 可视化编辑器
│ └── rust/ # WASM 渲染器
├── docs/ # 文档
└── examples/ # 示例
```
## 从源码构建
```bash
git clone https://github.com/esengine/esengine.git
cd esengine
pnpm install
pnpm build
# 框架包类型检查
pnpm type-check:framework
# 运行测试
pnpm test
```
## 文档
- [ECS 框架指南](./packages/framework/core/README.md)
- [行为树指南](./packages/framework/behavior-tree/README.md)
- [API 参考](https://esengine.cn/api/README)
## 社区
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
## 贡献
欢迎贡献代码!提交 PR 前请阅读贡献指南。
1. Fork 仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交修改 (`git commit -m 'Add amazing feature'`)
4. 推送分支 (`git push origin feature/amazing-feature`)
5. 发起 Pull Request
## 许可证
ESEngine 基于 [MIT 协议](LICENSE) 开源,个人和商业使用均免费。
---
<p align="center">
由 ESEngine 社区用心打造
</p>

View File

@@ -1,13 +1,71 @@
# Security Policy / 安全政策
**English** | [中文](#安全政策-1)
## Supported Versions
We provide security updates for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 2.x.x | :white_check_mark: |
| 1.x.x | :x: |
## Reporting a Vulnerability
If you discover a security vulnerability, please report it through the following channels:
### Reporting Channels
- **GitHub Security Advisories**: [Report a vulnerability](https://github.com/esengine/esengine/security/advisories/new) (Recommended)
- **Email**: security@esengine.dev
### Reporting Guidelines
1. **Do NOT** report security vulnerabilities in public issues
2. Provide a detailed description of the vulnerability, including:
- Affected versions
- Steps to reproduce
- Potential impact
- Suggested fix (if available)
### Response Timeline
- **Acknowledgment**: Within 72 hours
- **Initial Assessment**: Within 1 week
- **Fix Release**: Typically within 2-4 weeks, depending on severity
### Process
1. We will confirm the existence and severity of the vulnerability
2. Develop and test a fix
3. Release a security update
4. Publicly disclose the vulnerability details after the fix is released
## Security Best Practices
When using ESEngine, please follow these security recommendations:
- Always use the latest stable version
- Regularly update dependencies
- Disable debug mode in production
- Validate all external input data
- Do not store sensitive information on the client side
---
# 安全政策
[English](#security-policy--安全政策) | **中文**
## 支持的版本
我们为以下版本提供安全更新:
| 版本 | 支持状态 |
| ------- | ------------------ |
| 2.0.x | :white_check_mark: |
| 1.0.x | :x: |
| 2.x.x | :white_check_mark: |
| 1.x.x | :x: |
## 报告漏洞
@@ -15,10 +73,10 @@
### 报告渠道
- **邮箱**: [安全邮箱将在实际部署时提供]
- **GitHub**: 创建私有安全报告(推荐)
- **GitHub 安全公告**: [报告漏洞](https://github.com/esengine/esengine/security/advisories/new)(推荐)
- **邮箱**: security@esengine.dev
### 报告流程
### 报告指南
1. **不要**在公开的 issue 中报告安全漏洞
2. 提供详细的漏洞描述,包括:
@@ -40,9 +98,9 @@
3. 发布安全更新
4. 在修复发布后,会在相关渠道公布漏洞详情
### 安全最佳实践
## 安全最佳实践
使用 ECS Framework 时,请遵循以下安全建议:
使用 ESEngine 时,请遵循以下安全建议:
- 始终使用最新的稳定版本
- 定期更新依赖项
@@ -50,4 +108,6 @@
- 验证所有外部输入数据
- 不要在客户端存储敏感信息
感谢您帮助保持 ECS Framework 的安全性!
感谢您帮助保持 ESEngine 的安全性!
Thank you for helping keep ESEngine secure!

View File

@@ -6,9 +6,233 @@ import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const corePackageJson = JSON.parse(
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
readFileSync(join(__dirname, '../../packages/framework/core/package.json'), 'utf-8')
)
// Import i18n messages
import en from './i18n/en.json' with { type: 'json' }
import zh from './i18n/zh.json' with { type: 'json' }
// 创建侧边栏配置 | Create sidebar config
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
function createSidebar(t, prefix = '') {
return {
[`${prefix}/guide/`]: [
{
text: t.sidebar.gettingStarted,
items: [
{ text: t.sidebar.quickStart, link: `${prefix}/guide/getting-started` },
{ text: t.sidebar.guideOverview, link: `${prefix}/guide/` }
]
},
{
text: t.sidebar.coreConcepts,
collapsed: false,
items: [
{ text: t.sidebar.entity, link: `${prefix}/guide/entity` },
{ text: t.sidebar.hierarchy, link: `${prefix}/guide/hierarchy` },
{ text: t.sidebar.component, link: `${prefix}/guide/component` },
{ text: t.sidebar.entityQuery, link: `${prefix}/guide/entity-query` },
{
text: t.sidebar.system,
link: `${prefix}/guide/system`,
items: [
{ text: t.sidebar.workerSystem, link: `${prefix}/guide/worker-system` }
]
},
{
text: t.sidebar.scene,
link: `${prefix}/guide/scene`,
items: [
{ text: t.sidebar.sceneManager, link: `${prefix}/guide/scene-manager` },
{ text: t.sidebar.worldManager, link: `${prefix}/guide/world-manager` },
{ text: t.sidebar.persistentEntity, link: `${prefix}/guide/persistent-entity` }
]
},
{ text: t.sidebar.serialization, link: `${prefix}/guide/serialization` },
{ text: t.sidebar.eventSystem, link: `${prefix}/guide/event-system` },
{ text: t.sidebar.timeAndTimers, link: `${prefix}/guide/time-and-timers` },
{ text: t.sidebar.logging, link: `${prefix}/guide/logging` }
]
},
{
text: t.sidebar.advancedFeatures,
collapsed: false,
items: [
{ text: t.sidebar.serviceContainer, link: `${prefix}/guide/service-container` },
{ text: t.sidebar.pluginSystem, link: `${prefix}/guide/plugin-system` }
]
},
{
text: t.sidebar.platformAdapters,
link: `${prefix}/guide/platform-adapter`,
collapsed: false,
items: [
{ text: t.sidebar.browserAdapter, link: `${prefix}/guide/platform-adapter/browser` },
{ text: t.sidebar.wechatAdapter, link: `${prefix}/guide/platform-adapter/wechat-minigame` },
{ text: t.sidebar.nodejsAdapter, link: `${prefix}/guide/platform-adapter/nodejs` }
]
}
],
// 模块总览侧边栏 | Modules overview sidebar
[`${prefix}/modules/`]: [
{
text: t.sidebar.modulesOverview,
link: `${prefix}/modules/`,
items: [
{
text: t.sidebar.aiModules,
collapsed: false,
items: [
{ text: t.sidebar.behaviorTree, link: `${prefix}/modules/behavior-tree/` },
{ text: t.sidebar.fsm, link: `${prefix}/modules/fsm/` }
]
},
{
text: t.sidebar.gameplayModules,
collapsed: false,
items: [
{ text: t.sidebar.timer, link: `${prefix}/modules/timer/` },
{ text: t.sidebar.spatial, link: `${prefix}/modules/spatial/` },
{ text: t.sidebar.pathfinding, link: `${prefix}/modules/pathfinding/` }
]
},
{
text: t.sidebar.toolModules,
collapsed: false,
items: [
{ text: t.sidebar.blueprint, link: `${prefix}/modules/blueprint/` },
{ text: t.sidebar.procgen, link: `${prefix}/modules/procgen/` }
]
},
{
text: t.sidebar.networkModules,
collapsed: false,
items: [
{ text: t.sidebar.network, link: `${prefix}/modules/network/` }
]
}
]
}
],
// 行为树模块侧边栏 | Behavior tree module sidebar
[`${prefix}/modules/behavior-tree/`]: [
{
text: t.sidebar.behaviorTree,
items: [
{ text: t.sidebar.btGettingStarted, link: `${prefix}/modules/behavior-tree/getting-started` },
{ text: t.sidebar.btCoreConcepts, link: `${prefix}/modules/behavior-tree/core-concepts` },
{ text: t.sidebar.btEditorGuide, link: `${prefix}/modules/behavior-tree/editor-guide` },
{ text: t.sidebar.btEditorWorkflow, link: `${prefix}/modules/behavior-tree/editor-workflow` },
{ text: t.sidebar.btCustomActions, link: `${prefix}/modules/behavior-tree/custom-actions` },
{ text: t.sidebar.btCocosIntegration, link: `${prefix}/modules/behavior-tree/cocos-integration` },
{ text: t.sidebar.btLayaIntegration, link: `${prefix}/modules/behavior-tree/laya-integration` },
{ text: t.sidebar.btAdvancedUsage, link: `${prefix}/modules/behavior-tree/advanced-usage` },
{ text: t.sidebar.btBestPractices, link: `${prefix}/modules/behavior-tree/best-practices` }
]
}
],
[`${prefix}/examples/`]: [
{
text: t.sidebar.examples,
items: [
{ text: t.sidebar.examplesOverview, link: `${prefix}/examples/` },
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` }
]
}
],
[`${prefix}/api/`]: [
{
text: t.sidebar.apiReference,
items: [
{ text: t.sidebar.overview, link: `${prefix}/api/README` },
{
text: t.sidebar.coreClasses,
collapsed: false,
items: [
{ text: 'Core', link: `${prefix}/api/classes/Core` },
{ text: 'Scene', link: `${prefix}/api/classes/Scene` },
{ text: 'World', link: `${prefix}/api/classes/World` },
{ text: 'Entity', link: `${prefix}/api/classes/Entity` },
{ text: 'Component', link: `${prefix}/api/classes/Component` },
{ text: 'EntitySystem', link: `${prefix}/api/classes/EntitySystem` }
]
},
{
text: t.sidebar.systemClasses,
collapsed: true,
items: [
{ text: 'PassiveSystem', link: `${prefix}/api/classes/PassiveSystem` },
{ text: 'ProcessingSystem', link: `${prefix}/api/classes/ProcessingSystem` },
{ text: 'IntervalSystem', link: `${prefix}/api/classes/IntervalSystem` }
]
},
{
text: t.sidebar.utilities,
collapsed: true,
items: [
{ text: 'Matcher', link: `${prefix}/api/classes/Matcher` },
{ text: 'Time', link: `${prefix}/api/classes/Time` },
{ text: 'PerformanceMonitor', link: `${prefix}/api/classes/PerformanceMonitor` },
{ text: 'DebugManager', link: `${prefix}/api/classes/DebugManager` }
]
},
{
text: t.sidebar.interfaces,
collapsed: true,
items: [
{ text: 'IScene', link: `${prefix}/api/interfaces/IScene` },
{ text: 'IComponent', link: `${prefix}/api/interfaces/IComponent` },
{ text: 'ISystemBase', link: `${prefix}/api/interfaces/ISystemBase` },
{ text: 'ICoreConfig', link: `${prefix}/api/interfaces/ICoreConfig` }
]
},
{
text: t.sidebar.decorators,
collapsed: true,
items: [
{ text: '@ECSComponent', link: `${prefix}/api/functions/ECSComponent` },
{ text: '@ECSSystem', link: `${prefix}/api/functions/ECSSystem` }
]
},
{
text: t.sidebar.enums,
collapsed: true,
items: [
{ text: 'ECSEventType', link: `${prefix}/api/enumerations/ECSEventType` },
{ text: 'LogLevel', link: `${prefix}/api/enumerations/LogLevel` }
]
}
]
}
]
}
}
// 创建导航配置 | Create nav config
// prefix: 路径前缀,如 '' 或 '/en' | Path prefix like '' or '/en'
function createNav(t, prefix = '') {
return [
{ text: t.nav.home, link: `${prefix}/` },
{ text: t.nav.quickStart, link: `${prefix}/guide/getting-started` },
{ text: t.nav.guide, link: `${prefix}/guide/` },
{ text: t.nav.modules, link: `${prefix}/modules/` },
{ text: t.nav.api, link: `${prefix}/api/README` },
{
text: t.nav.examples,
items: [
{ text: t.nav.workerDemo, link: `${prefix}/examples/worker-system-demo` },
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
]
},
{ text: t.nav.changelog, link: `${prefix}/changelog` },
{
text: `v${corePackageJson.version}`,
link: 'https://github.com/esengine/esengine/releases'
}
]
}
export default defineConfig({
vite: {
plugins: [
@@ -28,178 +252,52 @@ 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/esengine/edit/master/docs/:path',
text: zh.common.editOnGithub
},
outline: {
level: [2, 3],
label: zh.common.onThisPage
}
}
},
en: {
label: 'English',
lang: 'en',
link: '/en/',
description: 'High-performance TypeScript ECS Framework for Game Development',
themeConfig: {
nav: createNav(en, '/en'),
sidebar: createSidebar(en, '/en'),
editLink: {
pattern: 'https://github.com/esengine/esengine/edit/master/docs/:path',
text: en.common.editOnGithub
},
outline: {
level: [2, 3],
label: en.common.onThisPage
}
}
}
},
themeConfig: {
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' }
{ icon: 'github', link: 'https://github.com/esengine/esengine' }
],
footer: {
@@ -207,18 +305,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,8 +315,9 @@ export default defineConfig({
['link', { rel: 'icon', href: '/favicon.ico' }]
],
base: '/ecs-framework/',
base: '/',
cleanUrls: true,
ignoreDeadLinks: true,
markdown: {
lineNumbers: true,
@@ -237,4 +326,4 @@ export default defineConfig({
dark: 'github-dark'
}
}
})
})

View File

@@ -0,0 +1,107 @@
{
"nav": {
"home": "Home",
"quickStart": "Quick Start",
"guide": "Guide",
"modules": "Modules",
"api": "API",
"examples": "Examples",
"workerDemo": "Worker System Demo",
"lawnMowerDemo": "Lawn Mower Demo",
"changelog": "Changelog"
},
"sidebar": {
"gettingStarted": "Getting Started",
"quickStart": "Quick Start",
"guideOverview": "Guide Overview",
"coreConcepts": "Core Concepts",
"entity": "Entity",
"hierarchy": "Hierarchy",
"component": "Component",
"entityQuery": "Entity Query",
"system": "System",
"workerSystem": "Worker System (Multithreading)",
"scene": "Scene",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"persistentEntity": "Persistent Entity",
"behaviorTree": "Behavior Tree",
"btGettingStarted": "Getting Started",
"btCoreConcepts": "Core Concepts",
"btEditorGuide": "Editor Guide",
"btEditorWorkflow": "Editor Workflow",
"btCustomActions": "Custom Actions",
"btCocosIntegration": "Cocos Creator Integration",
"btLayaIntegration": "Laya Engine Integration",
"btAdvancedUsage": "Advanced Usage",
"btBestPractices": "Best Practices",
"serialization": "Serialization",
"eventSystem": "Event System",
"timeAndTimers": "Time and Timers",
"logging": "Logging",
"advancedFeatures": "Advanced Features",
"serviceContainer": "Service Container",
"pluginSystem": "Plugin System",
"platformAdapters": "Platform Adapters",
"browserAdapter": "Browser Adapter",
"wechatAdapter": "WeChat Mini Game Adapter",
"nodejsAdapter": "Node.js Adapter",
"examples": "Examples",
"examplesOverview": "Examples Overview",
"apiReference": "API Reference",
"overview": "Overview",
"coreClasses": "Core Classes",
"systemClasses": "System Classes",
"utilities": "Utilities",
"interfaces": "Interfaces",
"decorators": "Decorators",
"enums": "Enums",
"modulesOverview": "Modules Overview",
"aiModules": "AI Modules",
"gameplayModules": "Gameplay",
"toolModules": "Tools",
"networkModules": "Network",
"fsm": "State Machine (FSM)",
"fsmOverview": "Overview",
"timer": "Timer System",
"timerOverview": "Overview",
"spatial": "Spatial Index",
"spatialOverview": "Overview",
"pathfinding": "Pathfinding",
"pathfindingOverview": "Overview",
"blueprint": "Visual Scripting",
"blueprintOverview": "Overview",
"procgen": "Procedural Generation",
"procgenOverview": "Overview",
"network": "Network Sync",
"networkOverview": "Overview"
},
"home": {
"title": "ESEngine - High-performance TypeScript ECS Framework",
"quickLinks": "Quick Links",
"viewDocs": "View Docs",
"getStarted": "Get Started",
"getStartedDesc": "From installation to your first ECS app, learn the core concepts in 5 minutes.",
"aiSystem": "AI System",
"behaviorTreeEditor": "Visual Behavior Tree Editor",
"behaviorTreeDesc": "Built-in AI behavior tree system with visual editing and real-time debugging.",
"coreFeatures": "Core Features",
"ecsArchitecture": "High-performance ECS Architecture",
"ecsArchitectureDesc": "Data-driven entity component system for large-scale entity processing with cache-friendly memory layout.",
"typeSupport": "Full Type Support",
"typeSupportDesc": "100% TypeScript with complete type definitions and compile-time checking for the best development experience.",
"visualBehaviorTree": "Visual Behavior Tree",
"visualBehaviorTreeDesc": "Built-in AI behavior tree system with visual editor, custom nodes, and real-time debugging.",
"multiPlatform": "Multi-Platform Support",
"multiPlatformDesc": "Support for browsers, Node.js, WeChat Mini Games, and seamless integration with major game engines.",
"modularDesign": "Modular Design",
"modularDesignDesc": "Core features packaged independently, import only what you need. Support for custom plugin extensions.",
"devTools": "Developer Tools",
"devToolsDesc": "Built-in performance monitoring, debugging tools, serialization system, and complete development toolchain.",
"learnMore": "Learn more →"
},
"common": {
"editOnGithub": "Edit this page on GitHub",
"onThisPage": "On this page"
}
}

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,107 @@
{
"nav": {
"home": "首页",
"quickStart": "快速开始",
"guide": "指南",
"modules": "模块",
"api": "API",
"examples": "示例",
"workerDemo": "Worker系统演示",
"lawnMowerDemo": "割草机演示",
"changelog": "更新日志"
},
"sidebar": {
"gettingStarted": "开始使用",
"quickStart": "快速开始",
"guideOverview": "指南概览",
"coreConcepts": "核心概念",
"entity": "实体类 (Entity)",
"hierarchy": "层级系统 (Hierarchy)",
"component": "组件系统 (Component)",
"entityQuery": "实体查询系统",
"system": "系统架构 (System)",
"workerSystem": "Worker系统 (多线程)",
"scene": "场景管理 (Scene)",
"sceneManager": "SceneManager",
"worldManager": "WorldManager",
"persistentEntity": "持久化实体 (Persistent Entity)",
"behaviorTree": "行为树系统 (Behavior Tree)",
"btGettingStarted": "快速开始",
"btCoreConcepts": "核心概念",
"btEditorGuide": "编辑器指南",
"btEditorWorkflow": "编辑器工作流",
"btCustomActions": "自定义动作组件",
"btCocosIntegration": "Cocos Creator集成",
"btLayaIntegration": "Laya引擎集成",
"btAdvancedUsage": "高级用法",
"btBestPractices": "最佳实践",
"serialization": "序列化系统 (Serialization)",
"eventSystem": "事件系统 (Event)",
"timeAndTimers": "时间和定时器 (Time)",
"logging": "日志系统 (Logger)",
"advancedFeatures": "高级特性",
"serviceContainer": "服务容器 (Service Container)",
"pluginSystem": "插件系统 (Plugin System)",
"platformAdapters": "平台适配器",
"browserAdapter": "浏览器适配器",
"wechatAdapter": "微信小游戏适配器",
"nodejsAdapter": "Node.js适配器",
"examples": "示例",
"examplesOverview": "示例概览",
"apiReference": "API 参考",
"overview": "概述",
"coreClasses": "核心类",
"systemClasses": "系统类",
"utilities": "工具类",
"interfaces": "接口",
"decorators": "装饰器",
"enums": "枚举",
"modulesOverview": "模块总览",
"aiModules": "AI 模块",
"gameplayModules": "游戏逻辑",
"toolModules": "工具模块",
"networkModules": "网络模块",
"fsm": "状态机 (FSM)",
"fsmOverview": "概述",
"timer": "定时器系统",
"timerOverview": "概述",
"spatial": "空间索引",
"spatialOverview": "概述",
"pathfinding": "寻路系统",
"pathfindingOverview": "概述",
"blueprint": "可视化脚本",
"blueprintOverview": "概述",
"procgen": "程序化生成",
"procgenOverview": "概述",
"network": "网络同步",
"networkOverview": "概述"
},
"home": {
"title": "ESEngine - 高性能 TypeScript ECS 框架",
"quickLinks": "快速入口",
"viewDocs": "查看文档",
"getStarted": "快速开始",
"getStartedDesc": "从安装到创建第一个 ECS 应用,快速了解核心概念。",
"aiSystem": "AI 系统",
"behaviorTreeEditor": "行为树可视化编辑器",
"behaviorTreeDesc": "内置 AI 行为树系统,支持可视化编辑和实时调试。",
"coreFeatures": "核心特性",
"ecsArchitecture": "高性能 ECS 架构",
"ecsArchitectureDesc": "基于数据驱动的实体组件系统,支持大规模实体处理,缓存友好的内存布局。",
"typeSupport": "完整类型支持",
"typeSupportDesc": "100% TypeScript 编写,完整的类型定义和编译时检查,提供最佳的开发体验。",
"visualBehaviorTree": "可视化行为树",
"visualBehaviorTreeDesc": "内置 AI 行为树系统,提供可视化编辑器,支持自定义节点和实时调试。",
"multiPlatform": "多平台支持",
"multiPlatformDesc": "支持浏览器、Node.js、微信小游戏等多平台可与主流游戏引擎无缝集成。",
"modularDesign": "模块化设计",
"modularDesignDesc": "核心功能独立打包,按需引入。支持自定义插件扩展,灵活适配不同项目。",
"devTools": "开发者工具",
"devToolsDesc": "内置性能监控、调试工具、序列化系统等,提供完整的开发工具链。",
"learnMore": "了解更多 →"
},
"common": {
"editOnGithub": "在 GitHub 上编辑此页",
"onThisPage": "在这个页面上"
}
}

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/esengine" class="btn-secondary" target="_blank">了解更多</a>
</div>
</div>
<!-- 右侧粒子动画区域 -->
<div class="hero-visual">
<div class="visual-container">
<canvas ref="canvasRef" class="particle-canvas"></canvas>
<div class="visual-label">
<span class="label-title">Entity Component System</span>
<span class="label-subtitle">High Performance Framework</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.hero-section {
background: #0d0d0d;
padding: 80px 0;
min-height: calc(100vh - 64px);
display: flex;
align-items: center;
}
.hero-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 64px;
align-items: center;
}
/* 左侧文字 */
.hero-text {
display: flex;
flex-direction: column;
gap: 24px;
}
.hero-logo {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.1em;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
margin: 0;
}
.hero-description {
font-size: 1.125rem;
color: #707070;
line-height: 1.7;
margin: 0;
max-width: 480px;
}
.hero-actions {
display: flex;
gap: 16px;
margin-top: 8px;
}
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 28px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9375rem;
text-decoration: none;
transition: all 0.2s ease;
}
.btn-primary {
background: #3b9eff;
color: #ffffff;
border: 1px solid #3b9eff;
border-radius: 6px;
}
.btn-primary:hover {
background: #5aadff;
border-color: #5aadff;
}
.btn-secondary {
background: #1a1a1a;
color: #a0a0a0;
border: 1px solid #2a2a2a;
border-radius: 6px;
}
.btn-secondary:hover {
background: #252525;
color: #ffffff;
}
.hero-visual {
display: flex;
justify-content: center;
}
.visual-container {
position: relative;
width: 100%;
max-width: 600px;
aspect-ratio: 4 / 3;
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
border-radius: 12px;
border: 1px solid #2a2a2a;
overflow: hidden;
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
}
.particle-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.visual-label {
position: absolute;
bottom: 24px;
left: 24px;
display: flex;
flex-direction: column;
gap: 4px;
}
.label-title {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
}
.label-subtitle {
font-size: 0.875rem;
color: #737373;
}
/* 响应式 */
@media (max-width: 1024px) {
.hero-container {
grid-template-columns: 1fr;
gap: 48px;
padding: 0 24px;
}
.hero-section {
padding: 48px 0;
min-height: auto;
}
.hero-title {
font-size: 2.25rem;
}
.hero-description {
font-size: 1rem;
}
.visual-container {
max-width: 100%;
aspect-ratio: 16 / 9;
}
}
@media (max-width: 640px) {
.hero-title {
font-size: 1.75rem;
}
.hero-actions {
flex-direction: column;
}
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>

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/esengine" class="btn-secondary" target="_blank">Learn More</a>
</div>
</div>
<!-- Right particle animation area -->
<div class="hero-visual">
<div class="visual-container">
<canvas ref="canvasRef" class="particle-canvas"></canvas>
<div class="visual-label">
<span class="label-title">Entity Component System</span>
<span class="label-subtitle">High Performance Framework</span>
</div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.hero-section {
background: #0d0d0d;
padding: 80px 0;
min-height: calc(100vh - 64px);
display: flex;
align-items: center;
}
.hero-container {
max-width: 1400px;
margin: 0 auto;
padding: 0 48px;
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 64px;
align-items: center;
}
/* Left text */
.hero-text {
display: flex;
flex-direction: column;
gap: 24px;
}
.hero-logo {
display: flex;
align-items: center;
gap: 12px;
color: #ffffff;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.1em;
}
.hero-title {
font-size: 3rem;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
margin: 0;
}
.hero-description {
font-size: 1.125rem;
color: #707070;
line-height: 1.7;
margin: 0;
max-width: 480px;
}
.hero-actions {
display: flex;
gap: 16px;
margin-top: 8px;
}
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 28px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9375rem;
text-decoration: none;
transition: all 0.2s ease;
}
.btn-primary {
background: #3b9eff;
color: #ffffff;
border: 1px solid #3b9eff;
border-radius: 6px;
}
.btn-primary:hover {
background: #5aadff;
border-color: #5aadff;
}
.btn-secondary {
background: #1a1a1a;
color: #a0a0a0;
border: 1px solid #2a2a2a;
border-radius: 6px;
}
.btn-secondary:hover {
background: #252525;
color: #ffffff;
}
.hero-visual {
display: flex;
justify-content: center;
}
.visual-container {
position: relative;
width: 100%;
max-width: 600px;
aspect-ratio: 4 / 3;
background: linear-gradient(135deg, #1a2a3a 0%, #1a1a1a 50%, #0d0d0d 100%);
border-radius: 12px;
border: 1px solid #2a2a2a;
overflow: hidden;
box-shadow: 0 20px 60px rgba(59, 158, 255, 0.1);
}
.particle-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.visual-label {
position: absolute;
bottom: 24px;
left: 24px;
display: flex;
flex-direction: column;
gap: 4px;
}
.label-title {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
}
.label-subtitle {
font-size: 0.875rem;
color: #737373;
}
/* Responsive */
@media (max-width: 1024px) {
.hero-container {
grid-template-columns: 1fr;
gap: 48px;
padding: 0 24px;
}
.hero-section {
padding: 48px 0;
min-height: auto;
}
.hero-title {
font-size: 2.25rem;
}
.hero-description {
font-size: 1rem;
}
.visual-container {
max-width: 100%;
aspect-ratio: 16 / 9;
}
}
@media (max-width: 640px) {
.hero-title {
font-size: 1.75rem;
}
.hero-actions {
flex-direction: column;
}
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,595 @@
:root {
color-scheme: dark;
--vp-nav-height: 64px;
--es-bg-base: #1a1a1a;
--es-bg-elevated: #222222;
--es-bg-overlay: #2a2a2a;
--es-bg-input: #333333;
--es-bg-inset: #151515;
--es-bg-hover: #2a2d2e;
--es-bg-active: #37373d;
--es-bg-sidebar: #1e1e1e;
--es-bg-card: #242424;
--es-bg-header: #1e1e1e;
/* 提高文字对比度 | Improve text contrast */
--es-text-primary: #e0e0e0;
--es-text-secondary: #b0b0b0;
--es-text-tertiary: #888888;
--es-text-inverse: #ffffff;
--es-text-muted: #c0c0c0;
--es-text-dim: #888888;
--es-font-xs: 11px;
--es-font-sm: 12px;
--es-font-base: 13px;
--es-font-md: 14px;
--es-font-lg: 16px;
--es-border-default: #3a3a3a;
--es-border-subtle: #1a1a1a;
--es-border-strong: #4a4a4a;
--es-primary: #3b82f6;
--es-primary-hover: #2563eb;
--es-success: #4ade80;
--es-warning: #f59e0b;
--es-error: #ef4444;
--es-info: #3b82f6;
--es-selected: #3d5a80;
--es-selected-hover: #4a6a90;
}
body {
background: var(--es-bg-base) !important;
}
html,
html.dark {
--vp-c-bg: var(--es-bg-base);
--vp-c-bg-soft: var(--es-bg-elevated);
--vp-c-bg-mute: var(--es-bg-overlay);
--vp-c-bg-alt: var(--es-bg-sidebar);
--vp-c-text-1: var(--es-text-primary);
--vp-c-text-2: var(--es-text-tertiary);
--vp-c-text-3: var(--es-text-muted);
--vp-c-divider: var(--es-border-default);
--vp-c-divider-light: var(--es-border-subtle);
}
html:not(.dark) {
--vp-c-bg: var(--es-bg-base) !important;
--vp-c-bg-soft: var(--es-bg-elevated) !important;
--vp-c-bg-mute: var(--es-bg-overlay) !important;
--vp-c-bg-alt: var(--es-bg-sidebar) !important;
--vp-c-text-1: var(--es-text-primary) !important;
--vp-c-text-2: var(--es-text-tertiary) !important;
--vp-c-text-3: var(--es-text-muted) !important;
}
.VPNav {
background: var(--es-bg-header) !important;
border-bottom: 1px solid var(--es-border-subtle) !important;
}
.VPNav .VPNavBar {
background: var(--es-bg-header) !important;
}
.VPNav .VPNavBar .wrapper {
background: var(--es-bg-header) !important;
}
.VPNav .VPNavBar::before,
.VPNav .VPNavBar::after {
display: none !important;
}
.VPNavBar {
background: var(--es-bg-header) !important;
}
.VPNavBar::before {
display: none !important;
}
.VPNavBarTitle .title {
color: var(--es-text-primary);
font-weight: 500;
font-size: var(--es-font-base);
}
.VPNavBarMenuLink {
color: var(--es-text-secondary) !important;
font-size: var(--es-font-sm) !important;
font-weight: 400 !important;
}
.VPNavBarMenuLink:hover {
color: var(--es-text-primary) !important;
}
.VPNavBarMenuLink.active {
color: var(--es-text-primary) !important;
}
.VPNavBarSearch .DocSearch-Button {
background: var(--es-bg-input) !important;
border: 1px solid var(--es-border-default) !important;
border-radius: 2px;
height: 26px;
}
.VPSidebar {
background: var(--es-bg-sidebar) !important;
border-right: 1px solid var(--es-border-subtle) !important;
}
.VPSidebarItem.level-0 > .item {
padding: 8px 0 4px 0;
}
.VPSidebarItem.level-0 > .item > .text {
font-weight: 600;
font-size: var(--es-font-xs);
color: var(--es-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.VPSidebarItem .link {
padding: 4px 8px;
margin: 1px 0;
border-radius: 2px;
color: var(--es-text-primary);
font-size: var(--es-font-sm);
transition: all 0.1s ease;
border-left: 2px solid transparent;
}
.VPSidebarItem .link:hover {
background: rgba(255, 255, 255, 0.03);
color: var(--es-text-inverse);
}
.VPSidebarItem.is-active > .item > .link {
background: var(--es-selected);
color: var(--es-text-inverse);
border-left: 2px solid var(--es-primary);
}
.VPSidebarItem.is-active > .item > .link:hover {
background: var(--es-selected-hover);
}
.VPSidebarItem.level-1 .link {
padding-left: 20px;
font-size: var(--es-font-sm);
}
.VPSidebarItem.level-2 .link {
padding-left: 32px;
font-size: var(--es-font-sm);
}
.VPSidebarItem .caret {
color: var(--es-text-secondary);
}
.VPSidebarItem .caret:hover {
color: var(--es-text-primary);
}
.VPContent {
background: var(--es-bg-card) !important;
padding-top: 0 !important;
}
.VPContent.has-sidebar {
background: var(--es-bg-card) !important;
}
/* 首页布局修复 | Home page layout fix */
.VPPage {
padding-top: 0 !important;
}
.Layout > .VPContent {
padding-top: var(--vp-nav-height) !important;
}
.VPDoc {
background: transparent !important;
}
.VPNavBar .content {
background: var(--es-bg-header) !important;
}
.VPNavBar .content-body {
background: var(--es-bg-header) !important;
}
.VPNavBar .divider {
display: none;
}
.VPLocalNav {
background: var(--es-bg-header) !important;
border-bottom: 1px solid var(--es-border-subtle) !important;
}
.VPNavScreenMenu {
background: var(--es-bg-base) !important;
}
.VPNavScreen {
background: var(--es-bg-base) !important;
}
.curtain {
display: none !important;
}
.VPNav .curtain,
.VPNavBar .curtain {
display: none !important;
}
[class*="curtain"] {
display: none !important;
}
.VPNav > div::before,
.VPNav > div::after {
display: none !important;
}
.vp-doc {
color: var(--es-text-primary);
}
.vp-doc h1 {
font-size: var(--es-font-lg);
font-weight: 600;
color: var(--es-text-inverse);
border-bottom: none;
padding-bottom: 0;
margin-bottom: 16px;
line-height: 1.3;
}
.vp-doc h2 {
font-size: var(--es-font-md);
font-weight: 600;
color: var(--es-text-inverse);
border-bottom: none;
padding-bottom: 0;
margin-top: 32px;
margin-bottom: 12px;
padding: 6px 12px;
background: var(--es-bg-header);
border-left: 3px solid var(--es-primary);
}
.vp-doc h3 {
font-size: var(--es-font-base);
font-weight: 600;
color: var(--es-text-primary);
margin-top: 20px;
margin-bottom: 8px;
}
.vp-doc p {
color: var(--es-text-primary);
line-height: 1.7;
font-size: var(--es-font-base);
margin: 12px 0;
}
.vp-doc ul,
.vp-doc ol {
padding-left: 20px;
margin: 12px 0;
}
.vp-doc li {
line-height: 1.7;
margin: 4px 0;
color: var(--es-text-primary);
font-size: var(--es-font-base);
}
.vp-doc li::marker {
color: var(--es-text-secondary);
}
.vp-doc strong {
color: var(--es-text-primary);
font-weight: 600;
}
.vp-doc a {
color: var(--es-primary);
text-decoration: none;
}
.vp-doc a:hover {
text-decoration: underline;
}
.VPDocAside {
padding-left: 16px;
border-left: 1px solid var(--es-border-subtle);
}
.VPDocAsideOutline {
padding: 0;
border: none !important;
}
.VPDocAsideOutline .content {
border: none !important;
padding-left: 0 !important;
}
.VPDocAsideOutline .outline-title {
font-size: var(--es-font-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--es-text-secondary);
padding-bottom: 8px;
}
.VPDocAsideOutline .outline-link {
color: var(--es-text-secondary);
font-size: var(--es-font-xs);
padding: 4px 0;
line-height: 1.4;
display: block;
}
.VPDocAsideOutline .outline-link:hover {
color: var(--es-text-primary);
}
.VPDocAsideOutline .outline-link.active {
color: var(--es-primary);
}
.VPDocAsideOutline .outline-marker {
display: none;
}
div[class*='language-'] {
background: var(--es-bg-inset) !important;
border: 1px solid var(--es-border-default);
border-radius: 2px;
margin: 12px 0;
}
.vp-code-group .tabs {
background: var(--es-bg-header);
border-bottom: 1px solid var(--es-border-subtle);
}
.vp-doc :not(pre) > code {
background: var(--es-bg-input);
color: var(--es-primary);
padding: 2px 6px;
border-radius: 2px;
font-size: var(--es-font-xs);
}
.vp-doc table {
display: table;
width: 100%;
background: transparent;
border: none;
border-collapse: collapse;
margin: 16px 0;
font-size: var(--es-font-sm);
}
.vp-doc tr {
border-bottom: 1px solid var(--es-border-subtle);
background: transparent;
}
.vp-doc tr:last-child {
border-bottom: none;
}
.vp-doc tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.vp-doc th {
background: var(--es-bg-header);
font-weight: 600;
font-size: var(--es-font-xs);
color: var(--es-text-secondary);
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--es-border-subtle);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.vp-doc td {
font-size: var(--es-font-sm);
color: var(--es-text-primary);
padding: 8px 12px;
vertical-align: top;
line-height: 1.5;
}
.vp-doc td:first-child {
font-weight: 500;
color: var(--es-text-primary);
min-width: 100px;
}
.vp-doc .warning,
.vp-doc .custom-block.warning {
background: rgba(245, 158, 11, 0.08);
border: none;
border-left: 3px solid var(--es-warning);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .warning .custom-block-title,
.vp-doc .custom-block.warning .custom-block-title {
color: var(--es-warning);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .warning p {
color: var(--es-text-primary);
margin: 0;
font-size: var(--es-font-xs);
}
.vp-doc .tip,
.vp-doc .custom-block.tip {
background: rgba(59, 130, 246, 0.08);
border: none;
border-left: 3px solid var(--es-primary);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .tip .custom-block-title,
.vp-doc .custom-block.tip .custom-block-title {
color: var(--es-primary);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .tip p {
color: var(--es-text-primary);
margin: 0;
font-size: var(--es-font-xs);
}
.vp-doc .info,
.vp-doc .custom-block.info {
background: rgba(74, 222, 128, 0.08);
border: none;
border-left: 3px solid var(--es-success);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .info .custom-block-title,
.vp-doc .custom-block.info .custom-block-title {
color: var(--es-success);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .danger,
.vp-doc .custom-block.danger {
background: rgba(239, 68, 68, 0.08);
border: none;
border-left: 3px solid var(--es-error);
border-radius: 0 2px 2px 0;
padding: 10px 12px;
margin: 16px 0;
}
.vp-doc .danger .custom-block-title,
.vp-doc .custom-block.danger .custom-block-title {
color: var(--es-error);
font-weight: 600;
font-size: var(--es-font-xs);
margin-bottom: 4px;
}
.vp-doc .card {
background: var(--es-bg-sidebar);
border: 1px solid var(--es-border-subtle);
border-radius: 4px;
padding: 12px;
margin: 16px 0;
}
.vp-doc .card-title {
font-size: var(--es-font-sm);
font-weight: 600;
color: var(--es-text-primary);
margin-bottom: 6px;
}
.vp-doc .card-description {
font-size: var(--es-font-xs);
color: var(--es-text-muted);
line-height: 1.5;
}
.vp-doc .tag {
display: inline-block;
padding: 2px 8px;
background: transparent;
border: 1px solid var(--es-border-default);
border-radius: 2px;
color: var(--es-text-secondary);
font-size: var(--es-font-xs);
margin-right: 4px;
margin-bottom: 4px;
}
.VPFooter {
background: var(--es-bg-sidebar) !important;
border-top: 1px solid var(--es-border-subtle) !important;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--es-bg-card);
}
::-webkit-scrollbar-thumb {
background: var(--es-border-strong);
border-radius: 4px;
border: 2px solid var(--es-bg-card);
}
::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
::-webkit-scrollbar-corner {
background: transparent;
}
.home-container {
max-width: 1000px;
margin: 0 auto;
padding: 0 16px;
}
.home-section {
padding: 32px 0;
}
@media (max-width: 960px) {
.VPDoc .content {
padding: 16px !important;
}
}

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,663 @@
# ESEngine 材质系统统一架构重构方案
## 问题概述
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
| 重复项 | Sprite | UI | 重复度 |
|--------|--------|----|----|
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
**根本原因**:缺乏统一的材质覆盖接口抽象层。
---
## 一、统一材质覆盖接口
### 1.1 定义通用接口
`@esengine/material-system` 包中定义统一接口:
```typescript
// packages/material-system/src/interfaces/IMaterialOverridable.ts
/**
* Material property override definition.
* 材质属性覆盖定义。
*/
export interface MaterialPropertyOverride {
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
value: number | number[];
}
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
/**
* Interface for components that support material property overrides.
* 支持材质属性覆盖的组件接口。
*/
export interface IMaterialOverridable {
/** Material GUID for asset reference | 材质资产引用的 GUID */
materialGuid: string;
/** Current material overrides | 当前材质覆盖 */
readonly materialOverrides: MaterialOverrides;
/** Get current material ID | 获取当前材质 ID */
getMaterialId(): number;
/** Set material ID | 设置材质 ID */
setMaterialId(id: number): void;
// Uniform setters
setOverrideFloat(name: string, value: number): this;
setOverrideVec2(name: string, x: number, y: number): this;
setOverrideVec3(name: string, x: number, y: number, z: number): this;
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
setOverrideInt(name: string, value: number): this;
// Uniform getters
getOverride(name: string): MaterialPropertyOverride | undefined;
removeOverride(name: string): this;
clearOverrides(): this;
hasOverrides(): boolean;
}
```
### 1.2 创建 Mixin 实现
使用 Mixin 模式避免代码重复:
```typescript
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
/**
* Mixin that provides material override functionality.
* 提供材质覆盖功能的 Mixin。
*/
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
return class extends Base {
materialGuid: string = '';
private _materialId: number = 0;
private _materialOverrides: MaterialOverrides = {};
get materialOverrides(): MaterialOverrides {
return this._materialOverrides;
}
getMaterialId(): number {
return this._materialId;
}
setMaterialId(id: number): void {
this._materialId = id;
}
setOverrideFloat(name: string, value: number): this {
this._materialOverrides[name] = { type: 'float', value };
return this;
}
setOverrideVec2(name: string, x: number, y: number): this {
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
return this;
}
setOverrideVec3(name: string, x: number, y: number, z: number): this {
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
return this;
}
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
return this;
}
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
return this;
}
setOverrideInt(name: string, value: number): this {
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
return this;
}
getOverride(name: string): MaterialPropertyOverride | undefined {
return this._materialOverrides[name];
}
removeOverride(name: string): this {
delete this._materialOverrides[name];
return this;
}
clearOverrides(): this {
this._materialOverrides = {};
return this;
}
hasOverrides(): boolean {
return Object.keys(this._materialOverrides).length > 0;
}
};
}
```
---
## 二、Shader Property 元数据系统
### 2.1 定义属性元数据接口
```typescript
// packages/material-system/src/interfaces/IShaderProperty.ts
/**
* Shader property UI metadata.
* 着色器属性 UI 元数据。
*/
export interface ShaderPropertyMeta {
/** Property type | 属性类型 */
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
label: string;
/** Property group for organization | 属性分组 */
group?: string;
/** Default value | 默认值 */
default?: number | number[] | string;
// Numeric constraints
min?: number;
max?: number;
step?: number;
/** UI hints | UI 提示 */
hint?: 'range' | 'angle' | 'hdr' | 'normal';
/** Tooltip description | 工具提示描述 */
tooltip?: string;
/** Whether to hide in inspector | 是否在检查器中隐藏 */
hidden?: boolean;
}
/**
* Extended shader definition with property metadata.
* 带属性元数据的扩展着色器定义。
*/
export interface ShaderAssetDefinition {
/** Shader name | 着色器名称 */
name: string;
/** Display name for UI | UI 显示名称 */
displayName?: string;
/** Shader description | 着色器描述 */
description?: string;
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
vertexSource: string;
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
fragmentSource: string;
/** Property metadata for inspector | 检查器属性元数据 */
properties?: Record<string, ShaderPropertyMeta>;
/** Render queue / order | 渲染队列/顺序 */
renderQueue?: number;
/** Preset blend mode | 预设混合模式 */
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
}
```
### 2.2 .shader 资产文件格式
```json
{
"$schema": "esengine://schemas/shader.json",
"version": 1,
"name": "Shiny",
"displayName": "闪光效果 | Shiny Effect",
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
"vertexSource": "./shaders/sprite.vert",
"fragmentSource": "./shaders/shiny.frag",
"blendMode": "alpha",
"renderQueue": 2000,
"properties": {
"u_shinyProgress": {
"type": "float",
"label": "进度 | Progress",
"group": "Animation",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"hidden": true
},
"u_shinyWidth": {
"type": "float",
"label": "宽度 | Width",
"group": "Effect",
"default": 0.25,
"min": 0,
"max": 1,
"step": 0.01,
"tooltip": "闪光带宽度 | Width of the shiny band"
},
"u_shinyRotation": {
"type": "float",
"label": "角度 | Rotation",
"group": "Effect",
"default": 2.25,
"min": 0,
"max": 6.28,
"step": 0.01,
"hint": "angle"
},
"u_shinySoftness": {
"type": "float",
"label": "柔和度 | Softness",
"group": "Effect",
"default": 1.0,
"min": 0,
"max": 1,
"step": 0.01
},
"u_shinyBrightness": {
"type": "float",
"label": "亮度 | Brightness",
"group": "Effect",
"default": 1.0,
"min": 0,
"max": 2,
"step": 0.01
},
"u_shinyGloss": {
"type": "float",
"label": "光泽度 | Gloss",
"group": "Effect",
"default": 1.0,
"min": 0,
"max": 1,
"step": 0.01,
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
}
}
}
```
---
## 三、统一效果组件/系统架构
### 3.1 抽取通用 ShinyEffect 基类
```typescript
// packages/material-system/src/effects/BaseShinyEffect.ts
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
/**
* Base shiny effect configuration (shared between UI and Sprite).
* 基础闪光效果配置UI 和 Sprite 共享)。
*/
export abstract class BaseShinyEffect extends Component {
// ============= Effect Parameters =============
@Serialize()
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
public width: number = 0.25;
@Serialize()
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
public rotation: number = 129;
@Serialize()
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
public softness: number = 1.0;
@Serialize()
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
public brightness: number = 1.0;
@Serialize()
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
public gloss: number = 1.0;
// ============= Animation Settings =============
@Serialize()
@Property({ type: 'boolean', label: 'Play' })
public play: boolean = true;
@Serialize()
@Property({ type: 'boolean', label: 'Loop' })
public loop: boolean = true;
@Serialize()
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
public duration: number = 2.0;
@Serialize()
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
public loopDelay: number = 2.0;
@Serialize()
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
public initialDelay: number = 0;
// ============= Runtime State =============
public progress: number = 0;
public elapsedTime: number = 0;
public inDelay: boolean = false;
public delayRemaining: number = 0;
public initialDelayProcessed: boolean = false;
reset(): void {
this.progress = 0;
this.elapsedTime = 0;
this.inDelay = false;
this.delayRemaining = 0;
this.initialDelayProcessed = false;
}
start(): void {
this.reset();
this.play = true;
}
stop(): void {
this.play = false;
}
getRotationRadians(): number {
return this.rotation * Math.PI / 180;
}
}
```
### 3.2 通用动画更新逻辑
```typescript
// packages/material-system/src/effects/ShinyEffectAnimator.ts
import type { BaseShinyEffect } from './BaseShinyEffect';
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
import { BuiltInShaders } from '../types';
/**
* Shared animator logic for shiny effect.
* 闪光效果共享的动画逻辑。
*/
export class ShinyEffectAnimator {
/**
* Update animation state.
* 更新动画状态。
*/
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
shiny.delayRemaining = shiny.initialDelay;
shiny.inDelay = true;
shiny.initialDelayProcessed = true;
}
if (shiny.inDelay) {
shiny.delayRemaining -= deltaTime;
if (shiny.delayRemaining <= 0) {
shiny.inDelay = false;
shiny.elapsedTime = 0;
}
return;
}
shiny.elapsedTime += deltaTime;
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
if (shiny.progress >= 1.0) {
if (shiny.loop) {
shiny.inDelay = true;
shiny.delayRemaining = shiny.loopDelay;
shiny.progress = 0;
shiny.elapsedTime = 0;
} else {
shiny.play = false;
shiny.progress = 1.0;
}
}
}
/**
* Apply material overrides.
* 应用材质覆盖。
*/
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
if (target.getMaterialId() === 0) {
target.setMaterialId(BuiltInShaders.Shiny);
}
target.setOverrideFloat('u_shinyProgress', shiny.progress);
target.setOverrideFloat('u_shinyWidth', shiny.width);
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
target.setOverrideFloat('u_shinySoftness', shiny.softness);
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
}
}
```
---
## 四、Material Inspector 设计
### 4.1 组件架构
```
MaterialPropertiesEditor (容器组件)
├── ShaderSelector (着色器选择器)
├── PropertyGroup (属性分组)
│ ├── FloatProperty (浮点属性)
│ ├── VectorProperty (向量属性)
│ ├── ColorProperty (颜色属性)
│ └── TextureProperty (纹理属性)
└── OverrideIndicator (覆盖指示器)
```
### 4.2 核心组件
```typescript
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
interface MaterialPropertiesEditorProps {
/** Target component implementing IMaterialOverridable */
target: IMaterialOverridable;
/** Current shader definition with property metadata */
shaderDef?: ShaderAssetDefinition;
/** Callback when property changes */
onChange?: (name: string, value: MaterialPropertyOverride) => void;
}
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
target,
shaderDef,
onChange
}) => {
// Group properties by their group field
const groupedProps = useMemo(() => {
if (!shaderDef?.properties) return {};
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
for (const [name, meta] of Object.entries(shaderDef.properties)) {
if (meta.hidden) continue;
const group = meta.group || 'Default';
if (!groups[group]) groups[group] = [];
groups[group].push([name, meta]);
}
return groups;
}, [shaderDef]);
return (
<div className="material-properties-editor">
<ShaderSelector
currentShaderId={target.getMaterialId()}
onSelect={(id) => target.setMaterialId(id)}
/>
{Object.entries(groupedProps).map(([group, props]) => (
<PropertyGroup key={group} title={group}>
{props.map(([name, meta]) => (
<PropertyField
key={name}
name={name}
meta={meta}
value={target.getOverride(name)?.value ?? meta.default}
onChange={(value) => {
applyOverride(target, name, meta.type, value);
onChange?.(name, target.getOverride(name)!);
}}
/>
))}
</PropertyGroup>
))}
</div>
);
};
```
---
## 五、实施计划
### Phase 1: 接口层 (1-2 天)
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
3. **导出新接口** (`packages/material-system/src/index.ts`)
### Phase 2: 重构现有组件 (2-3 天)
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
3. **删除重复代码**:移除各组件中的重复材质方法
### Phase 3: 统一效果系统 (2-3 天)
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
5. **重构系统**:使用 ShinyEffectAnimator
### Phase 4: Shader Property 系统 (2-3 天)
1. **定义 ShaderPropertyMeta 接口**
2. **扩展 ShaderDefinition** 添加 properties 字段
3. **创建 ShaderLoader** 支持 .shader 文件
4. **注册内置着色器属性元数据**
### Phase 5: Material Inspector (3-4 天)
1. **创建 MaterialPropertiesEditor 组件**
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
3. **集成到现有 Inspector 系统**
4. **支持实时预览**
---
## 六、文件修改清单
| 优先级 | 包 | 文件 | 操作 |
|--------|-----|------|------|
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
---
## 七、Transform 组件统一(可选)
### 7.1 现状分析
| 特性 | TransformComponent | UITransformComponent |
|------|-------------------|---------------------|
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
| **可见性** | ❌ 无 | ✅ visible, alpha |
### 7.2 结论
**不建议完全合并**,但可提取公共基类:
```typescript
// packages/engine-core/src/interfaces/ITransformBase.ts
export interface ITransformBase {
/** 旋转角度(度) | Rotation in degrees */
rotation: number;
/** X 缩放 | Scale X */
scaleX: number;
/** Y 缩放 | Scale Y */
scaleY: number;
/** 本地到世界矩阵 | Local to world matrix */
readonly localToWorldMatrix: Matrix2D;
/** 是否需要更新 | Dirty flag */
isDirty: boolean;
/** 世界坐标 X | World position X */
readonly worldX: number;
/** 世界坐标 Y | World position Y */
readonly worldY: number;
/** 世界旋转 | World rotation */
readonly worldRotation: number;
/** 世界缩放 X | World scale X */
readonly worldScaleX: number;
/** 世界缩放 Y | World scale Y */
readonly worldScaleY: number;
}
```
### 7.3 收益
- 渲染系统可以统一处理 `ITransformBase`
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
- Gizmo 系统可以共享变换操作逻辑
---
## 八、向后兼容性
1. **接口兼容**:现有组件的 API 保持不变
2. **序列化兼容**:不改变现有序列化格式
3. **渐进迁移**:可分阶段进行,不影响现有功能

293
docs/changelog.md Normal file
View File

@@ -0,0 +1,293 @@
# Changelog
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
---
## v2.4.2 (2025-12-25)
### Features
- **IncrementalSerializer 实体过滤**: 增量序列化支持 `entityFilter` 选项 (#335)
- 创建快照时可按条件过滤实体
- 支持按标签、组件类型等自定义过滤逻辑
- 适用于只同步部分实体的场景(如只同步玩家)
```typescript
// 只快照玩家实体
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.tag === PLAYER_TAG
});
// 只快照有特定组件的实体
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
});
```
### Refactor
- 优化 `PlatformWorkerPool` 代码规范,提取为独立模块 (#335)
- 优化 `WorkerEntitySystem` 实现,改进代码结构 (#334)
- 代码规范化与依赖清理 (#317)
- 代码结构优化,添加 `GlobalTypes.ts` 统一类型定义 (#316)
---
## v2.4.1 (2025-12-23)
### Bug Fixes
- 修复 `IntervalSystem` 时间累加 bug间隔计时更加准确
- 修复 Cocos Creator 兼容性问题,类型导出更完整
### Documentation
- 新增 `Core.paused` 属性文档说明
---
## v2.4.0 (2025-12-15)
### Features
- **EntityHandle 实体句柄**: 轻量级实体引用抽象 (#304)
- 28位索引 + 20位代数generation设计高效复用已销毁实体槽位
- `EntityHandleManager` 管理句柄生命周期和有效性验证
- 支持句柄转换为实体引用,检测悬空引用
- **SystemScheduler 系统调度器**: 声明式系统调度 (#304)
- 新增 `@Stage(name)` 装饰器指定系统执行阶段
- 新增 `@Before(SystemClass)` / `@After(SystemClass)` 装饰器声明系统依赖
- 新增 `@InSet(setName)` 装饰器将系统归入逻辑分组
- 基于拓扑排序自动解析执行顺序,检测循环依赖
- **EpochManager 变更检测**: 帧级变更追踪机制 (#304)
- 跟踪组件添加/修改时间戳epoch
- 支持查询"自上次检查以来变化的组件"
- 适用于脏检测、增量更新等优化场景
- **CompiledQuery 编译查询**: 预编译类型安全查询 (#304)
- 编译时生成优化的查询逻辑,减少运行时开销
- 完整的 TypeScript 类型推断支持
- 支持 `With``Without``Changed` 等查询条件组合
- **PluginServiceRegistry**: 类型安全的插件服务注册表 (#300)
- 通过 `Core.pluginServices` 访问
- 支持 `ServiceToken<T>` 模式获取服务
- **组件自动注册**: `@ECSComponent` 装饰器增强 (#302)
- 装饰器现在自动注册到 `ComponentRegistry`
- 解决 `Decorators ↔ ComponentRegistry` 循环依赖
- 新建 `ComponentTypeUtils.ts` 作为底层无依赖模块
### API Changes
- `EntitySystem` 添加 `getBefore()` / `getAfter()` / `getSets()` getter 方法
- `Entity` 添加 `markDirty()` 辅助方法用于手动触发变更检测
- `IScene` 添加 `epochManager` 属性
- `CommandBuffer.pendingCount` 修正为返回实际操作数(而非实体数)
### Documentation
- 更新系统调度文档,添加声明式依赖配置章节
- 更新实体查询文档,添加编译查询使用说明
---
## v2.3.2 (2025-12-08)
### Features
- **微信小游戏 Worker 支持**: 添加对微信小游戏平台 Worker 的完整支持 (#297)
- 新增 `workerScriptPath` 配置项,支持预编译 Worker 脚本路径
- 修复微信小游戏 Worker 消息格式差异(`res` 直接是数据,无需 `.data`
- 适用于微信小游戏等不支持动态脚本的平台
### New Package
- **@esengine/worker-generator** `v1.0.2`: CLI 工具,从 `WorkerEntitySystem` 子类自动生成 Worker 文件
- 自动扫描并提取 `workerProcess` 方法体
- 支持 `--wechat` 模式,使用 TypeScript 编译器转换为 ES5 语法
- 读取代码中的 `workerScriptPath` 配置,生成到指定路径
- 生成 `worker-mapping.json` 映射文件
### Documentation
- 更新 Worker 系统文档,添加微信小游戏支持章节
- 新增英文版 Worker 系统文档 (`docs/en/guide/worker-system.md`)
---
## v2.3.1 (2025-12-07)
### Bug Fixes
- **类型导出修复**: 修复 v2.3.0 中的类型导出问题
- 解决 `ServiceToken` 跨包类型兼容性问题
- 修复 `editor-app``behavior-tree-editor` 中的类型引用
---
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。
>
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
### Features
- **持久化实体**: 添加实体跨场景迁移支持 (#285)
- 新增 `EEntityLifecyclePolicy` 枚举(`SceneLocal`/`Persistent`
- Entity 添加 `setPersistent()``setSceneLocal()``isPersistent` API
- Scene 添加 `findPersistentEntities()``extractPersistentEntities()``receiveMigratedEntities()`
- `SceneManager.setScene()` 自动处理持久化实体迁移
- 适用场景:全局管理器、玩家角色、跨场景状态保持
- **CommandBuffer 延迟命令系统**: 在帧末统一执行实体操作 (#281)
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 `commands` 属性
- 避免在迭代过程中修改实体列表,防止迭代问题
- Scene 在 `lateUpdate` 后自动刷新所有命令缓冲区
### Performance
- **ReactiveQuery 快照优化**: 优化实体查询迭代性能 (#281)
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照
---
## v2.2.21 (2025-12-05)
### Bug Fixes
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
### Performance
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
- 使用脏实体集合代替每帧遍历所有实体
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
---
## v2.2.20 (2025-12-04)
### Bug Fixes
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
---
## v2.2.19 (2025-12-03)
### Features
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
- 确保相同优先级的系统按添加顺序执行
- **模块配置**: 添加 `module.json` 配置文件 (#256)
- 定义模块 ID、名称、版本等元信息
- 支持模块依赖声明和导出配置
---
## v2.2.18 (2025-11-30)
### Features
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
- `ProfilerSDK`: 统一的性能分析接口
- 手动采样标记 (`beginSample`/`endSample`)
- 自动作用域测量 (`measure`/`measureAsync`)
- 调用层级追踪和调用图生成
- 计数器和仪表支持
- `AdvancedProfilerCollector`: 高级性能数据收集器
- 帧时间统计和历史记录
- 内存快照和 GC 检测
- 长任务检测 (Long Task API)
- 性能报告生成
- `DebugManager`: 调试管理器
- 统一的调试工具入口
- 性能分析器集成
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
- 支持更多序列化选项配置
### Improvements
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
- 覆盖实体查询、缓存、生命周期等场景
- **Matcher 增强**: 优化匹配器功能 (#240)
- 改进匹配逻辑和性能
---
## v2.2.17 (2025-11-28)
### Features
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
- 支持通过名称注册和查询组件类型
- 添加组件掩码缓存优化性能
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
- 支持更灵活的序列化配置
- 改进嵌套对象序列化
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
- `onSceneStart()`: 场景开始时调用
- `onSceneStop()`: 场景停止时调用
---
## v2.2.16 (2025-11-27)
### Features
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
- 支持 `Symbol.for()` 模式的服务标识
- 新增 `@InjectProperty` 属性注入装饰器
- 改进服务解析和生命周期管理
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
- 支持更多组件类型的序列化
- 改进反序列化错误处理
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
- 支持 `@range``@slider` 等编辑器提示
- 支持 `@dropdown``@color` 等 UI 类型
- 支持 `@asset` 资源引用类型
### Improvements
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
---
## 版本说明
- **主版本号**: 重大不兼容更新
- **次版本号**: 新功能添加(向后兼容)
- **修订版本号**: Bug 修复和小改进
## 相关链接
- [GitHub Releases](https://github.com/esengine/esengine/releases)
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
- [文档首页](./index.md)

291
docs/en/changelog.md Normal file
View File

@@ -0,0 +1,291 @@
# Changelog
This document records the version update history of the `@esengine/ecs-framework` core library.
---
## v2.4.2 (2025-12-25)
### Features
- **IncrementalSerializer Entity Filter**: Incremental serialization supports `entityFilter` option (#335)
- Filter entities by condition when creating snapshots
- Support custom filter logic by tag, component type, etc.
- Suitable for scenarios that only sync partial entities (e.g., only sync players)
```typescript
// Only snapshot player entities
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.tag === PLAYER_TAG
});
// Only snapshot entities with specific component
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
});
```
### Refactor
- Optimize `PlatformWorkerPool` code style, extract as standalone module (#335)
- Optimize `WorkerEntitySystem` implementation, improve code structure (#334)
- Code standardization and dependency cleanup (#317)
- Code structure optimization, add `GlobalTypes.ts` for unified type definitions (#316)
---
## v2.4.1 (2025-12-23)
### Bug Fixes
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
- Fix Cocos Creator compatibility issue, more complete type exports
### Documentation
- Add `Core.paused` property documentation
---
## v2.4.0 (2025-12-15)
### Features
- **EntityHandle**: Lightweight entity reference abstraction (#304)
- 28-bit index + 20-bit generation design for efficient reuse of destroyed entity slots
- `EntityHandleManager` manages handle lifecycle and validity verification
- Support handle-to-entity conversion with dangling reference detection
- **SystemScheduler**: Declarative system scheduling (#304)
- New `@Stage(name)` decorator to specify system execution stage
- New `@Before(SystemClass)` / `@After(SystemClass)` decorators to declare dependencies
- New `@InSet(setName)` decorator to group systems logically
- Automatic execution order resolution via topological sort with cycle detection
- **EpochManager**: Frame-level change detection mechanism (#304)
- Track component add/modify timestamps (epochs)
- Support querying "components changed since last check"
- Suitable for dirty checking, incremental updates, and other optimization scenarios
- **CompiledQuery**: Pre-compiled type-safe queries (#304)
- Compile-time generated optimized query logic, reducing runtime overhead
- Full TypeScript type inference support
- Support `With`, `Without`, `Changed` and other query condition combinations
- **PluginServiceRegistry**: Type-safe plugin service registry (#300)
- Accessible via `Core.pluginServices`
- Support `ServiceToken<T>` pattern for service retrieval
- **Component Auto-Registration**: `@ECSComponent` decorator enhancement (#302)
- Decorator now automatically registers to `ComponentRegistry`
- Resolved `Decorators ↔ ComponentRegistry` circular dependency
- New `ComponentTypeUtils.ts` as low-level dependency-free module
### API Changes
- `EntitySystem` adds `getBefore()` / `getAfter()` / `getSets()` getter methods
- `Entity` adds `markDirty()` helper method for manual change detection triggering
- `IScene` adds `epochManager` property
- `CommandBuffer.pendingCount` corrected to return actual operation count (not entity count)
### Documentation
- Updated system scheduling documentation with declarative dependency configuration
- Updated entity query documentation with compiled query usage
---
## v2.3.2 (2025-12-08)
### Features
- **WeChat Mini Game Worker Support**: Add complete Worker support for WeChat Mini Game platform (#297)
- New `workerScriptPath` config option for pre-compiled Worker script path
- Fix WeChat Mini Game Worker message format difference (`res` is data directly, no `.data` wrapper)
- Applicable to WeChat Mini Game and other platforms that don't support dynamic scripts
### New Package
- **@esengine/worker-generator** `v1.0.2`: CLI tool to auto-generate Worker files from `WorkerEntitySystem` subclasses
- Automatically scan and extract `workerProcess` method body
- Support `--wechat` mode, use TypeScript compiler to convert to ES5 syntax
- Read `workerScriptPath` config from code, generate to specified path
- Generate `worker-mapping.json` mapping file
### Documentation
- Updated Worker system documentation with WeChat Mini Game support section
- Added English Worker system documentation (`docs/en/guide/worker-system.md`)
---
## v2.3.1 (2025-12-07)
### Bug Fixes
- **Type export fix**: Fix type export issues in v2.3.0
- Resolve `ServiceToken` cross-package type compatibility issues
- Fix type references in `editor-app` and `behavior-tree-editor`
---
## v2.3.0 (2025-12-06) ⚠️ DEPRECATED
> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later.
### Features
- **Persistent Entity**: Add entity cross-scene migration support (#285)
- New `EEntityLifecyclePolicy` enum (`SceneLocal`/`Persistent`)
- Entity adds `setPersistent()`, `setSceneLocal()`, `isPersistent` API
- Scene adds `findPersistentEntities()`, `extractPersistentEntities()`, `receiveMigratedEntities()`
- `SceneManager.setScene()` automatically handles persistent entity migration
- Use cases: global managers, player characters, cross-scene state persistence
- **CommandBuffer Deferred Command System**: Execute entity operations uniformly at end of frame (#281)
- Support deferred add/remove components, destroy entities, set entity active state
- Each system has its own `commands` property
- Avoid modifying entity list during iteration, preventing iteration issues
- Scene automatically flushes all command buffers after `lateUpdate`
### Performance
- **ReactiveQuery Snapshot Optimization**: Optimize entity query iteration performance (#281)
- Add snapshot mechanism to avoid copying arrays every frame
- Only create new snapshots when entity list changes
- Multiple systems share the same snapshot in static scenes
---
## v2.2.21 (2025-12-05)
### Bug Fixes
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
### Performance
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
- Use dirty entity set instead of iterating all entities
- Static scene `process()` optimized from O(n) to O(1)
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
---
## v2.2.20 (2025-12-04)
### Bug Fixes
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
- System initialization now triggers `onAdded` callback for existing matching entities
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
---
## v2.2.19 (2025-12-03)
### Features
- **System stable sorting**: Add stable sorting support for systems (#257)
- Added `addOrder` property for stable sorting when `updateOrder` is the same
- Ensures systems with same priority execute in add order
- **Module configuration**: Add `module.json` configuration file (#256)
- Define module ID, name, version and other metadata
- Support module dependency declaration and export configuration
---
## v2.2.18 (2025-11-30)
### Features
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
- `ProfilerSDK`: Unified performance analysis interface
- Manual sampling markers (`beginSample`/`endSample`)
- Automatic scope measurement (`measure`/`measureAsync`)
- Call hierarchy tracking and call graph generation
- Counter and gauge support
- `AdvancedProfilerCollector`: Advanced performance data collector
- Frame time statistics and history
- Memory snapshots and GC detection
- Long task detection (Long Task API)
- Performance report generation
- `DebugManager`: Debug manager
- Unified debug tool entry point
- Profiler integration
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
- Support more serialization option configurations
### Improvements
- **EntitySystem test coverage**: Add complete system test cases (#240)
- Cover entity query, cache, lifecycle scenarios
- **Matcher enhancement**: Optimize matcher functionality (#240)
- Improved matching logic and performance
---
## v2.2.17 (2025-11-28)
### Features
- **ComponentRegistry enhancement**: Add new component registry features (#244)
- Support registering and querying component types by name
- Add component mask caching for performance optimization
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
- Support more flexible serialization configuration
- Improved nested object serialization
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
- `onSceneStart()`: Called when scene starts
- `onSceneStop()`: Called when scene stops
---
## v2.2.16 (2025-11-27)
### Features
- **Component lifecycle**: Add component lifecycle callback support (#237)
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
- **ServiceContainer enhancement**: Improve service container functionality (#237)
- Support `Symbol.for()` pattern for service identifiers
- Added `@InjectProperty` property injection decorator
- Improved service resolution and lifecycle management
- **SceneSerializer enhancement**: New scene serializer features (#237)
- Support serialization of more component types
- Improved deserialization error handling
- **Property decorator extension**: Extend `@serialize` decorator (#238)
- Support `@range`, `@slider` and other editor hints
- Support `@dropdown`, `@color` and other UI types
- Support `@asset` resource reference type
### Improvements
- **Matcher tests**: Add Matcher test cases (#240)
- **EntitySystem tests**: Add complete entity system test coverage (#240)
---
## Version Notes
- **Major version**: Breaking changes
- **Minor version**: New features (backward compatible)
- **Patch version**: Bug fixes and improvements
## Related Links
- [GitHub Releases](https://github.com/esengine/esengine/releases)
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
- [Documentation Home](./index.md)

444
docs/en/guide/entity.md Normal file
View File

@@ -0,0 +1,444 @@
# Entity
In ECS architecture, an Entity is the basic object in the game world. An entity itself does not contain game logic or data - it's just a container that combines different components to achieve various functionalities.
## Basic Concepts
An entity is a lightweight object mainly used for:
- Serving as a container for components
- Providing a unique identifier (ID)
- Managing component lifecycle
::: tip About Parent-Child Hierarchy
Parent-child hierarchy relationships between entities are managed through `HierarchyComponent` and `HierarchySystem`, not built-in Entity properties. This design follows ECS composition principles - only entities that need hierarchy relationships add this component.
See [Hierarchy System](./hierarchy.md) documentation.
:::
## Creating Entities
**Important: Entities must be created through Scene, manual creation is not supported!**
Entities must be created through the scene's `createEntity()` method to ensure:
- Entity is properly added to the scene's entity management system
- Entity is added to the query system for system use
- Entity gets the correct scene reference
- Related lifecycle events are triggered
```typescript
// Correct way: create entity through scene
const player = scene.createEntity("Player");
// Wrong way: manually create entity
// const entity = new Entity("MyEntity", 1); // System cannot manage such entities
```
## Adding Components
Entities gain functionality by adding components:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';
// Define 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;
}
}
// Define health component
@ECSComponent('Health')
class Health extends Component {
current: number = 100;
max: number = 100;
constructor(max: number = 100) {
super();
this.max = max;
this.current = max;
}
}
// Add components to entity
const player = scene.createEntity("Player");
player.addComponent(new Position(100, 200));
player.addComponent(new Health(150));
```
## Getting Components
```typescript
// Get component (pass component class, not instance)
const position = player.getComponent(Position); // Returns Position | null
const health = player.getComponent(Health); // Returns Health | null
// Check if component exists
if (position) {
console.log(`Player position: x=${position.x}, y=${position.y}`);
}
// Check if entity has a component
if (player.hasComponent(Position)) {
console.log("Player has position component");
}
// Get all component instances (read-only property)
const allComponents = player.components; // readonly Component[]
// Get all components of specified type (supports multiple components of same type)
const allHealthComponents = player.getComponents(Health); // Health[]
// Get or create component (creates automatically if not exists)
const position = player.getOrCreateComponent(Position, 0, 0); // Pass constructor arguments
const health = player.getOrCreateComponent(Health, 100); // Returns existing if present, creates new if not
```
## Removing Components
```typescript
// Method 1: Remove by component type
const removedHealth = player.removeComponentByType(Health);
if (removedHealth) {
console.log("Health component removed");
}
// Method 2: Remove by component instance
const healthComponent = player.getComponent(Health);
if (healthComponent) {
player.removeComponent(healthComponent);
}
// Batch remove multiple component types
const removedComponents = player.removeComponentsByTypes([Position, Health]);
// Check if component was removed
if (!player.hasComponent(Health)) {
console.log("Health component has been removed");
}
```
## Finding Entities
Scene provides multiple ways to find entities:
### Find by Name
```typescript
// Find single entity
const player = scene.findEntity("Player");
// Or use alias method
const player2 = scene.getEntityByName("Player");
if (player) {
console.log("Found player entity");
}
```
### Find by ID
```typescript
// Find by entity ID
const entity = scene.findEntityById(123);
```
### Find by Tag
Entities support a tag system for quick categorization and lookup:
```typescript
// Set tags
player.tag = 1; // Player tag
enemy.tag = 2; // Enemy tag
// Find all entities by tag
const players = scene.findEntitiesByTag(1);
const enemies = scene.findEntitiesByTag(2);
// Or use alias method
const allPlayers = scene.getEntitiesByTag(1);
```
## Entity Lifecycle
```typescript
// Destroy entity
player.destroy();
// Check if entity is destroyed
if (player.isDestroyed) {
console.log("Entity has been destroyed");
}
```
## Entity Events
Component changes on entities trigger events:
```typescript
// Listen for component added event
scene.eventSystem.on('component:added', (data) => {
console.log('Component added:', data);
});
// Listen for entity created event
scene.eventSystem.on('entity:created', (data) => {
console.log('Entity created:', data.entityName);
});
```
## Performance Optimization
### Batch Entity Creation
The framework provides high-performance batch creation methods:
```typescript
// Batch create 100 bullet entities (high-performance version)
const bullets = scene.createEntities(100, "Bullet");
// Add components to each bullet
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(Math.random() * 800, Math.random() * 600));
bullet.addComponent(new Velocity(Math.random() * 100 - 50, Math.random() * 100 - 50));
});
```
`createEntities()` method will:
- Batch allocate entity IDs
- Batch add to entity list
- Optimize query system updates
- Reduce system cache clearing times
## Best Practices
### 1. Appropriate Component Granularity
```typescript
// Good practice: single-purpose components
@ECSComponent('Position')
class Position extends Component {
x: number = 0;
y: number = 0;
}
@ECSComponent('Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
}
// Avoid: overly complex components
@ECSComponent('Player')
class Player extends Component {
// Avoid including too many unrelated properties in one component
x: number;
y: number;
health: number;
inventory: Item[];
skills: Skill[];
}
```
### 2. Use Decorators
Always use `@ECSComponent` decorator:
```typescript
@ECSComponent('Transform')
class Transform extends Component {
// Component implementation
}
```
### 3. Proper Naming
```typescript
// Clear entity naming
const mainCharacter = scene.createEntity("MainCharacter");
const enemy1 = scene.createEntity("Goblin_001");
const collectible = scene.createEntity("HealthPotion");
```
### 4. Timely Cleanup
```typescript
// Destroy entities that are no longer needed
if (enemy.getComponent(Health).current <= 0) {
enemy.destroy();
}
```
## Debugging Entities
The framework provides debugging features to help development:
```typescript
// Get entity debug info
const debugInfo = entity.getDebugInfo();
console.log('Entity info:', debugInfo);
// List all components of entity
entity.components.forEach(component => {
console.log('Component:', component.constructor.name);
});
```
Entities are one of the core concepts in ECS architecture. Understanding how to use entities correctly will help you build efficient, maintainable game code.
## Entity Handle (EntityHandle)
Entity handles provide a safe way to reference entities, solving the "referencing destroyed entity" problem.
### Problem Scenario
Suppose your AI system needs to track a target enemy:
```typescript
// Wrong approach: directly store entity reference
class AISystem extends EntitySystem {
private targetEnemy: Entity | null = null;
setTarget(enemy: Entity) {
this.targetEnemy = enemy;
}
process() {
if (this.targetEnemy) {
// Dangerous! Enemy might be destroyed, but reference still exists
// Worse: this memory location might be reused by a new entity
const health = this.targetEnemy.getComponent(Health);
// Might operate on the wrong entity!
}
}
}
```
### Correct Approach Using Handles
Each entity is automatically assigned a handle when created, accessible via `entity.handle`:
```typescript
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
class AISystem extends EntitySystem {
// Store handle instead of entity reference
private targetHandle: EntityHandle = NULL_HANDLE;
setTarget(enemy: Entity) {
// Save enemy's handle
this.targetHandle = enemy.handle;
}
process() {
if (!isValidHandle(this.targetHandle)) {
return; // No target
}
// Get entity through handle (automatically checks validity)
const enemy = this.scene.findEntityByHandle(this.targetHandle);
if (!enemy) {
// Enemy was destroyed, clear reference
this.targetHandle = NULL_HANDLE;
return;
}
// Safe operation
const health = enemy.getComponent(Health);
}
}
```
### Complete Example: Skill Target Locking
```typescript
import {
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
} from '@esengine/ecs-framework';
@ECSSystem('SkillTargeting')
class SkillTargetingSystem extends EntitySystem {
// Store handles for multiple targets
private lockedTargets: Map<Entity, EntityHandle> = new Map();
// Lock target
lockTarget(caster: Entity, target: Entity) {
this.lockedTargets.set(caster, target.handle);
}
// Get locked target
getLockedTarget(caster: Entity): Entity | null {
const handle = this.lockedTargets.get(caster);
if (!handle || !isValidHandle(handle)) {
return null;
}
// findEntityByHandle checks if handle is valid
const target = this.scene.findEntityByHandle(handle);
if (!target) {
// Target died, clear lock
this.lockedTargets.delete(caster);
}
return target;
}
// Cast skill
castSkill(caster: Entity) {
const target = this.getLockedTarget(caster);
if (!target) {
console.log('Target lost, skill cancelled');
return;
}
// Safely deal damage to target
const health = target.getComponent(Health);
if (health) {
health.current -= 10;
}
}
}
```
### Handle vs Entity Reference
| Scenario | Recommended Approach |
|----------|---------------------|
| Temporary use within same frame | Use `Entity` reference directly |
| Cross-frame storage (e.g., AI target, skill target) | Use `EntityHandle` |
| Needs serialization | Use `EntityHandle` (numeric type) |
| Network synchronization | Use `EntityHandle` (can be transmitted directly) |
### API Quick Reference
```typescript
// Get entity's handle
const handle = entity.handle;
// Check if handle is non-null
if (isValidHandle(handle)) { ... }
// Get entity through handle (automatically checks validity)
const entity = scene.findEntityByHandle(handle);
// Check if entity corresponding to handle is alive
const alive = scene.handleManager.isAlive(handle);
// Null handle constant
const emptyHandle = NULL_HANDLE;
```
## Next Steps
- Learn about [Hierarchy System](./hierarchy.md) to establish parent-child relationships
- Learn about [Component System](./component.md) to add functionality to entities
- Learn about [Scene Management](./scene.md) to organize and manage entities

View File

@@ -0,0 +1,441 @@
# Quick Start
This guide will help you get started with ECS Framework, from installation to creating your first ECS application.
## Installation
### Using CLI (Recommended)
The easiest way to add ECS to your existing project:
```bash
# In your project directory
npx @esengine/cli init
```
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code, including:
- `ECSManager` component/script - Manages ECS lifecycle
- Example components and systems - Helps you get started quickly
- Automatic dependency installation
### Manual NPM Installation
If you prefer manual configuration:
```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 3.x Engine Integration
Using `Laya.Script` component to manage ECS lifecycle is recommended:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
const { regClass } = Laya;
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new GameScene();
onAwake(): void {
// Initialize ECS
Core.create({ debug: true });
Core.setScene(this.ecsScene);
}
onUpdate(): void {
// Auto-updates global services and scene
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
// Cleanup resources
Core.destroy();
}
}
```
In Laya IDE, attach the `ECSManager` script to a node in your 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.

View File

@@ -0,0 +1,360 @@
# Persistent Entity
> **Version**: v2.3.0+
Persistent Entity is a special type of entity that automatically migrates to the new scene during scene transitions. It is suitable for game objects that need to maintain state across scenes, such as players, game managers, audio managers, etc.
## Basic Concepts
In the ECS framework, entities have two lifecycle policies:
| Policy | Description | Default |
|--------|-------------|---------|
| `SceneLocal` | Scene-local entity, destroyed when scene changes | ✓ |
| `Persistent` | Persistent entity, automatically migrates during scene transitions | |
## Quick Start
### Creating a Persistent Entity
```typescript
import { Scene } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// Create a persistent player entity
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(100, 200));
player.addComponent(new PlayerData('Hero', 500));
// Create a normal enemy entity (destroyed when scene changes)
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(300, 200));
enemy.addComponent(new EnemyAI());
}
}
```
### Behavior During Scene Transitions
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// Initial scene
class Level1Scene extends Scene {
protected initialize(): void {
// Player - persistent, will migrate to the next scene
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(0, 0));
player.addComponent(new Health(100));
// Enemy - scene-local, destroyed when scene changes
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(100, 100));
}
}
// Target scene
class Level2Scene extends Scene {
protected initialize(): void {
// New enemy
const enemy = this.createEntity('Boss');
enemy.addComponent(new Position(200, 200));
}
public onStart(): void {
// Player has automatically migrated to this scene
const player = this.findEntity('Player');
console.log(player !== null); // true
// Position and health data are fully preserved
const position = player?.getComponent(Position);
const health = player?.getComponent(Health);
console.log(position?.x, position?.y); // 0, 0
console.log(health?.value); // 100
}
}
// Switch scenes
Core.create({ debug: true });
Core.setScene(new Level1Scene());
// Later switch to Level2
Core.loadScene(new Level2Scene());
// Player entity migrates automatically, Enemy entity is destroyed
```
## API Reference
### Entity Methods
#### setPersistent()
Marks the entity as persistent, preventing destruction during scene transitions.
```typescript
public setPersistent(): this
```
**Returns**: Returns the entity itself for method chaining
**Example**:
```typescript
const player = scene.createEntity('Player')
.setPersistent();
player.addComponent(new Position(100, 200));
```
#### setSceneLocal()
Restores the entity to scene-local policy (default).
```typescript
public setSceneLocal(): this
```
**Returns**: Returns the entity itself for method chaining
**Example**:
```typescript
// Dynamically cancel persistence
player.setSceneLocal();
```
#### isPersistent
Checks if the entity is persistent.
```typescript
public get isPersistent(): boolean
```
**Example**:
```typescript
if (entity.isPersistent) {
console.log('This is a persistent entity');
}
```
#### lifecyclePolicy
Gets the entity's lifecycle policy.
```typescript
public get lifecyclePolicy(): EEntityLifecyclePolicy
```
**Example**:
```typescript
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
console.log('Persistent entity');
}
```
### Scene Methods
#### findPersistentEntities()
Finds all persistent entities in the scene.
```typescript
public findPersistentEntities(): Entity[]
```
**Returns**: Array of persistent entities
**Example**:
```typescript
const persistentEntities = scene.findPersistentEntities();
console.log(`Scene has ${persistentEntities.length} persistent entities`);
```
#### extractPersistentEntities()
Extracts and removes all persistent entities from the scene (typically called internally by the framework).
```typescript
public extractPersistentEntities(): Entity[]
```
**Returns**: Array of extracted persistent entities
#### receiveMigratedEntities()
Receives migrated entities (typically called internally by the framework).
```typescript
public receiveMigratedEntities(entities: Entity[]): void
```
**Parameters**:
- `entities` - Array of entities to receive
## Use Cases
### 1. Player Entity Across Levels
```typescript
class PlayerSetupScene extends Scene {
protected initialize(): void {
// Player maintains state across all levels
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Transform(0, 0));
player.addComponent(new Health(100));
player.addComponent(new Inventory());
player.addComponent(new PlayerStats());
}
}
class Level1 extends Scene { /* ... */ }
class Level2 extends Scene { /* ... */ }
class Level3 extends Scene { /* ... */ }
// Player entity automatically migrates between all levels
Core.setScene(new PlayerSetupScene());
// ... game progresses
Core.loadScene(new Level1());
// ... level complete
Core.loadScene(new Level2());
// Player data (health, inventory, stats) fully preserved
```
### 2. Global Managers
```typescript
class BootstrapScene extends Scene {
protected initialize(): void {
// Audio manager - persists across scenes
const audioManager = this.createEntity('AudioManager').setPersistent();
audioManager.addComponent(new AudioController());
// Achievement manager - persists across scenes
const achievementManager = this.createEntity('AchievementManager').setPersistent();
achievementManager.addComponent(new AchievementTracker());
// Game settings - persists across scenes
const settings = this.createEntity('GameSettings').setPersistent();
settings.addComponent(new SettingsData());
}
}
```
### 3. Dynamically Toggling Persistence
```typescript
class GameScene extends Scene {
protected initialize(): void {
// Initially created as a normal entity
const companion = this.createEntity('Companion');
companion.addComponent(new Transform(0, 0));
companion.addComponent(new CompanionAI());
// Listen for recruitment event
this.eventSystem.on('companion:recruited', () => {
// After recruitment, become persistent
companion.setPersistent();
console.log('Companion joined the party, will follow player across scenes');
});
// Listen for dismissal event
this.eventSystem.on('companion:dismissed', () => {
// After dismissal, restore to scene-local
companion.setSceneLocal();
console.log('Companion left the party, will no longer persist across scenes');
});
}
}
```
## Best Practices
### 1. Clearly Identify Persistent Entities
```typescript
// Recommended: Mark immediately when creating
const player = this.createEntity('Player').setPersistent();
// Not recommended: Marking after creation (easy to forget)
const player = this.createEntity('Player');
// ... lots of code ...
player.setPersistent(); // Easy to forget
```
### 2. Use Persistence Appropriately
```typescript
// ✓ Entities suitable for persistence
const player = this.createEntity('Player').setPersistent(); // Player
const gameManager = this.createEntity('GameManager').setPersistent(); // Global manager
const audioManager = this.createEntity('AudioManager').setPersistent(); // Audio system
// ✗ Entities that should NOT be persistent
const bullet = this.createEntity('Bullet'); // Temporary objects
const enemy = this.createEntity('Enemy'); // Level-specific enemies
const particle = this.createEntity('Particle'); // Effect particles
```
### 3. Check Migrated Entities
```typescript
class NewScene extends Scene {
public onStart(): void {
// Check if expected persistent entities exist
const player = this.findEntity('Player');
if (!player) {
console.error('Player entity did not migrate correctly!');
// Handle error case
}
}
}
```
### 4. Avoid Circular References
```typescript
// ✗ Avoid: Persistent entity referencing scene-local entity
class BadScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// Dangerous: player is persistent but enemy is not
// After scene change, enemy is destroyed, reference becomes invalid
player.addComponent(new TargetComponent(enemy));
}
}
// ✓ Recommended: Use ID references or event system
class GoodScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// Store ID instead of direct reference
player.addComponent(new TargetComponent(enemy.id));
// Or use event system for communication
}
}
```
## Important Notes
1. **Destroyed entities will not migrate**: If an entity is destroyed before scene transition, it will not migrate even if marked as persistent.
2. **Component data is fully preserved**: All components and their state are preserved during migration.
3. **Scene reference is updated**: After migration, the entity's `scene` property will point to the new scene.
4. **Query system is updated**: Migrated entities are automatically registered in the new scene's query system.
5. **Delayed transitions also work**: Persistent entities migrate when using `Core.loadScene()` for delayed transitions as well.
## Related Documentation
- [Scene](./scene) - Learn the basics of scenes
- [SceneManager](./scene-manager) - Learn about scene transitions
- [WorldManager](./world-manager) - Learn about multi-world management

View File

@@ -0,0 +1,436 @@
# SceneManager
SceneManager is a lightweight scene manager provided by ECS Framework, suitable for 95% of game applications. It provides a simple and intuitive API with support for scene transitions and delayed loading.
## Use Cases
SceneManager is suitable for:
- Single-player games
- Simple multiplayer games
- Mobile games
- Games requiring scene transitions (menu, game, pause, etc.)
- Projects that don't need multi-World isolation
## Features
- Lightweight, zero extra overhead
- Simple and intuitive API
- Supports delayed scene transitions (avoids switching mid-frame)
- Automatic ECS fluent API management
- Automatic scene lifecycle handling
- Integrated with Core, auto-updated
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+)
## Basic Usage
### Recommended: Using Core's Static Methods
This is the simplest and recommended approach, suitable for most applications:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 1. Initialize Core
Core.create({ debug: true });
// 2. Create and set scene
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// Add systems
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
// Create initial entities
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Health(100));
}
public onStart(): void {
console.log("Game scene started");
}
}
// 3. Set scene
Core.setScene(new GameScene());
// 4. Game loop (Core.update automatically updates the scene)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Automatically updates all services and scenes
}
// Laya engine integration
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime);
});
// Cocos Creator integration
update(deltaTime: number) {
Core.update(deltaTime);
}
```
### Advanced: Using SceneManager Directly
If you need more control, you can use SceneManager directly:
```typescript
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
// Initialize Core
Core.create({ debug: true });
// Get SceneManager (already auto-created and registered by Core)
const sceneManager = Core.services.resolve(SceneManager);
// Set scene
const gameScene = new GameScene();
sceneManager.setScene(gameScene);
// Game loop (still use Core.update)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Core automatically calls sceneManager.update()
}
```
**Important**: Regardless of which approach you use, you should only call `Core.update()` in the game loop. It automatically updates SceneManager and scenes. You don't need to manually call `sceneManager.update()`.
## Scene Transitions
### Immediate Transition
Use `Core.setScene()` or `sceneManager.setScene()` to immediately switch scenes:
```typescript
// Method 1: Using Core (recommended)
Core.setScene(new MenuScene());
// Method 2: Using SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new MenuScene());
```
### Delayed Transition
Use `Core.loadScene()` or `sceneManager.loadScene()` for delayed scene transition, which takes effect on the next frame:
```typescript
// Method 1: Using Core (recommended)
Core.loadScene(new GameOverScene());
// Method 2: Using SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.loadScene(new GameOverScene());
```
When switching scenes from within a System, use delayed transitions:
```typescript
class GameOverSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
const player = entities.find(e => e.name === 'Player');
const health = player?.getComponent(Health);
if (health && health.value <= 0) {
// Delayed transition to game over scene (takes effect next frame)
Core.loadScene(new GameOverScene());
// Current frame continues execution, won't interrupt current system processing
}
}
}
```
## API Reference
### Core Static Methods (Recommended)
#### Core.setScene()
Immediately switch scenes.
```typescript
public static setScene<T extends IScene>(scene: T): T
```
**Parameters**:
- `scene` - The scene instance to set
**Returns**:
- Returns the set scene instance
**Example**:
```typescript
const gameScene = Core.setScene(new GameScene());
console.log(gameScene.name);
```
#### Core.loadScene()
Delayed scene loading (switches on next frame).
```typescript
public static loadScene<T extends IScene>(scene: T): void
```
**Parameters**:
- `scene` - The scene instance to load
**Example**:
```typescript
Core.loadScene(new GameOverScene());
```
#### Core.scene
Get the currently active scene.
```typescript
public static get scene(): IScene | null
```
**Returns**:
- Current scene instance, or null if no scene
**Example**:
```typescript
const currentScene = Core.scene;
if (currentScene) {
console.log(`Current scene: ${currentScene.name}`);
}
```
### SceneManager Methods (Advanced)
If you need to use SceneManager directly, get it through the service container:
```typescript
const sceneManager = Core.services.resolve(SceneManager);
```
#### setScene()
Immediately switch scenes.
```typescript
public setScene<T extends IScene>(scene: T): T
```
#### loadScene()
Delayed scene loading.
```typescript
public loadScene<T extends IScene>(scene: T): void
```
#### currentScene
Get the current scene.
```typescript
public get currentScene(): IScene | null
```
#### hasScene
Check if there's an active scene.
```typescript
public get hasScene(): boolean
```
#### hasPendingScene
Check if there's a pending scene transition.
```typescript
public get hasPendingScene(): boolean
```
## Best Practices
### 1. Use Core's Static Methods
```typescript
// Recommended: Use Core's static methods
Core.setScene(new GameScene());
Core.loadScene(new MenuScene());
const currentScene = Core.scene;
// Not recommended: Don't directly use SceneManager unless you have special needs
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new GameScene());
```
### 2. Only Call Core.update()
```typescript
// Correct: Only call Core.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Automatically updates all services and scenes
}
// Incorrect: Don't manually call sceneManager.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
sceneManager.update(); // Duplicate update, will cause issues!
}
```
### 3. Use Delayed Transitions to Avoid Issues
When switching scenes from within a System, use `loadScene()` instead of `setScene()`:
```typescript
// Recommended: Delayed transition
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.loadScene(new GameOverScene());
// Current frame continues processing other entities
}
}
}
}
// Not recommended: Immediate transition may cause issues
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.setScene(new GameOverScene());
// Scene switches immediately, other entities in current frame may not process correctly
}
}
}
}
```
### 4. Scene Responsibility Separation
Each scene should be responsible for only one specific game state:
```typescript
// Good design - clear responsibilities
class MenuScene extends Scene {
// Only handles menu-related logic
}
class GameScene extends Scene {
// Only handles gameplay logic
}
class PauseScene extends Scene {
// Only handles pause screen logic
}
// Avoid this design - mixed responsibilities
class MegaScene extends Scene {
// Contains menu, game, pause, and all other logic
}
```
### 5. Resource Management
Clean up resources in the scene's `unload()` method:
```typescript
class GameScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
this.textures.set('player', loadTexture('player.png'));
this.sounds.set('bgm', loadSound('bgm.mp3'));
}
public unload(): void {
// Cleanup resources
this.textures.clear();
this.sounds.clear();
console.log('Scene resources cleaned up');
}
}
```
### 6. Event-Driven Scene Transitions
Use the event system to trigger scene transitions, keeping code decoupled:
```typescript
class GameScene extends Scene {
protected initialize(): void {
// Listen to scene transition events
this.eventSystem.on('goto:menu', () => {
Core.loadScene(new MenuScene());
});
this.eventSystem.on('goto:gameover', (data) => {
Core.loadScene(new GameOverScene());
});
}
}
// Trigger events in System
class GameLogicSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
if (levelComplete) {
this.scene.eventSystem.emitSync('goto:gameover', {
score: 1000,
level: 5
});
}
}
}
```
## Architecture Overview
SceneManager's position in ECS Framework:
```
Core (Global Services)
└── SceneManager (Scene Management, auto-updated)
└── Scene (Current Scene)
├── EntitySystem (Systems)
├── Entity (Entities)
└── Component (Components)
```
## Comparison with WorldManager
| Feature | SceneManager | WorldManager |
|---------|--------------|--------------|
| Use Case | 95% of game applications | Advanced multi-world isolation scenarios |
| Complexity | Simple | Complex |
| Scene Count | Single scene (switchable) | Multiple Worlds, each with multiple scenes |
| Performance Overhead | Minimal | Higher |
| Usage | `Core.setScene()` | `worldManager.createWorld()` |
**When to use SceneManager**:
- Single-player games
- Simple multiplayer games
- Mobile games
- Scenes that need transitions but don't need to run simultaneously
**When to use WorldManager**:
- MMO game servers (one World per room)
- Game lobby systems (complete isolation per game room)
- Need to run multiple completely independent game instances
## Related Documentation
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.

364
docs/en/guide/scene.md Normal file
View File

@@ -0,0 +1,364 @@
# Scene Management
In the ECS architecture, a Scene is a container for the game world, responsible for managing the lifecycle of entities, systems, and components. Scenes provide a complete ECS runtime environment.
## Basic Concepts
Scene is the core container of the ECS framework, providing:
- Entity creation, management, and destruction
- System registration and execution scheduling
- Component storage and querying
- Event system support
- Performance monitoring and debugging information
## Scene Management Options
ECS Framework provides two scene management approaches:
1. **[SceneManager](./scene-manager)** - Suitable for 95% of game applications
- Single-player games, simple multiplayer games, mobile games
- Lightweight, simple and intuitive API
- Supports scene transitions
2. **[WorldManager](./world-manager)** - Suitable for advanced multi-world isolation scenarios
- MMO game servers, game room systems
- Multi-World management, each World can contain multiple scenes
- Completely isolated independent environments
This document focuses on the usage of the Scene class itself. For detailed information about scene managers, please refer to the corresponding documentation.
## Creating a Scene
### Inheriting the Scene Class
**Recommended: Inherit the Scene class to create custom scenes**
```typescript
import { Scene, EntitySystem } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// Set scene name
this.name = "GameScene";
// Add systems
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
this.addSystem(new PhysicsSystem());
// Create initial entities
this.createInitialEntities();
}
private createInitialEntities(): void {
// Create player
const player = this.createEntity("Player");
player.addComponent(new Position(400, 300));
player.addComponent(new Health(100));
player.addComponent(new PlayerController());
// Create enemies
for (let i = 0; i < 5; i++) {
const enemy = this.createEntity(`Enemy_${i}`);
enemy.addComponent(new Position(Math.random() * 800, Math.random() * 600));
enemy.addComponent(new Health(50));
enemy.addComponent(new EnemyAI());
}
}
public onStart(): void {
console.log("Game scene started");
// Logic when scene starts
}
public unload(): void {
console.log("Game scene unloaded");
// Cleanup logic when scene unloads
}
}
```
### Using Scene Configuration
```typescript
import { ISceneConfig } from '@esengine/ecs-framework';
const config: ISceneConfig = {
name: "MainGame",
enableEntityDirectUpdate: false
};
class ConfiguredScene extends Scene {
constructor() {
super(config);
}
}
```
## Scene Lifecycle
Scene provides complete lifecycle management:
```typescript
class ExampleScene extends Scene {
protected initialize(): void {
// Scene initialization: setup systems and initial entities
console.log("Scene initializing");
}
public onStart(): void {
// Scene starts running: game logic begins execution
console.log("Scene starting");
}
public unload(): void {
// Scene unloading: cleanup resources
console.log("Scene unloading");
}
}
// Using scenes (lifecycle automatically managed by framework)
const scene = new ExampleScene();
// Scene's initialize(), begin(), update(), end() are automatically called by the framework
```
**Lifecycle Methods**:
1. `initialize()` - Scene initialization, setup systems and initial entities
2. `begin()` / `onStart()` - Scene starts running
3. `update()` - Per-frame update (called by scene manager)
4. `end()` / `unload()` - Scene unloading, cleanup resources
## Entity Management
### Creating Entities
```typescript
class EntityScene extends Scene {
createGameEntities(): void {
// Create single entity
const player = this.createEntity("Player");
// Batch create entities (high performance)
const bullets = this.createEntities(100, "Bullet");
// Add components to batch-created entities
bullets.forEach((bullet, index) => {
bullet.addComponent(new Position(index * 10, 100));
bullet.addComponent(new Velocity(Math.random() * 200 - 100, -300));
});
}
}
```
### Finding Entities
```typescript
class SearchScene extends Scene {
findEntities(): void {
// Find by name
const player = this.findEntity("Player");
const player2 = this.getEntityByName("Player"); // Alias method
// Find by ID
const entity = this.findEntityById(123);
// Find by tag
const enemies = this.findEntitiesByTag(2);
const enemies2 = this.getEntitiesByTag(2); // Alias method
if (player) {
console.log(`Found player: ${player.name}`);
}
console.log(`Found ${enemies.length} enemies`);
}
}
```
### Destroying Entities
```typescript
class DestroyScene extends Scene {
cleanupEntities(): void {
// Destroy all entities
this.destroyAllEntities();
// Single entity destruction through the entity itself
const enemy = this.findEntity("Enemy_1");
if (enemy) {
enemy.destroy(); // Entity is automatically removed from the scene
}
}
}
```
## System Management
### Adding and Removing Systems
```typescript
class SystemScene extends Scene {
protected initialize(): void {
// Add systems
const movementSystem = new MovementSystem();
this.addSystem(movementSystem);
// Set system update order
movementSystem.updateOrder = 1;
// Add more systems
this.addSystem(new PhysicsSystem());
this.addSystem(new RenderSystem());
}
public removeUnnecessarySystems(): void {
// Get system
const physicsSystem = this.getEntityProcessor(PhysicsSystem);
// Remove system
if (physicsSystem) {
this.removeSystem(physicsSystem);
}
}
}
```
## Event System
Scene has a built-in type-safe event system:
```typescript
class EventScene extends Scene {
protected initialize(): void {
// Listen to events
this.eventSystem.on('player_died', this.onPlayerDied.bind(this));
this.eventSystem.on('enemy_spawned', this.onEnemySpawned.bind(this));
this.eventSystem.on('level_complete', this.onLevelComplete.bind(this));
}
private onPlayerDied(data: any): void {
console.log('Player died event');
// Handle player death
}
private onEnemySpawned(data: any): void {
console.log('Enemy spawned event');
// Handle enemy spawn
}
private onLevelComplete(data: any): void {
console.log('Level complete event');
// Handle level completion
}
public triggerGameEvent(): void {
// Send event (synchronous)
this.eventSystem.emitSync('custom_event', {
message: "This is a custom event",
timestamp: Date.now()
});
// Send event (asynchronous)
this.eventSystem.emit('async_event', {
data: "Async event data"
});
}
}
```
## Best Practices
### 1. Scene Responsibility Separation
```typescript
// Good scene design - clear responsibilities
class MenuScene extends Scene {
// Only handles menu-related logic
}
class GameScene extends Scene {
// Only handles gameplay logic
}
class InventoryScene extends Scene {
// Only handles inventory logic
}
// Avoid this design - mixed responsibilities
class MegaScene extends Scene {
// Contains menu, game, inventory, and all other logic
}
```
### 2. Proper System Organization
```typescript
class OrganizedScene extends Scene {
protected initialize(): void {
// Add systems by function and dependencies
this.addInputSystems();
this.addLogicSystems();
this.addRenderSystems();
}
private addInputSystems(): void {
this.addSystem(new InputSystem());
}
private addLogicSystems(): void {
this.addSystem(new MovementSystem());
this.addSystem(new PhysicsSystem());
this.addSystem(new CollisionSystem());
}
private addRenderSystems(): void {
this.addSystem(new RenderSystem());
this.addSystem(new UISystem());
}
}
```
### 3. Resource Management
```typescript
class ResourceScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
// Load resources needed by the scene
this.textures.set('player', this.loadTexture('player.png'));
this.sounds.set('bgm', this.loadSound('bgm.mp3'));
}
public unload(): void {
// Cleanup resources
this.textures.clear();
this.sounds.clear();
console.log('Scene resources cleaned up');
}
private loadTexture(path: string): any {
// Load texture
return null;
}
private loadSound(path: string): any {
// Load sound
return null;
}
}
```
## Next Steps
- Learn about [SceneManager](./scene-manager) - Simple scene management for most games
- Learn about [WorldManager](./world-manager) - For scenarios requiring multi-world isolation
- Learn about [Persistent Entity](./persistent-entity) - Keep entities across scene transitions (v2.3.0+)
Scene is the core container of the ECS framework. Proper scene management makes your game architecture clearer, more modular, and easier to maintain.

1161
docs/en/guide/system.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,402 @@
# Time and Timer System
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
## Time Class
The Time class is the core of the framework's time management, providing all game time-related functionality.
### Basic Time Properties
```typescript
import { Time } from '@esengine/ecs-framework';
class GameSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
// Get frame time (seconds)
const deltaTime = Time.deltaTime;
// Get unscaled frame time
const unscaledDelta = Time.unscaledDeltaTime;
// Get total game time
const totalTime = Time.totalTime;
// Get current frame count
const frameCount = Time.frameCount;
console.log(`Frame ${frameCount}, delta: ${deltaTime}s, total: ${totalTime}s`);
}
}
```
### Game Pause
The framework provides two pause methods for different scenarios:
#### Core.paused (Recommended)
`Core.paused` is a **true pause** - when set, the entire game loop stops:
```typescript
import { Core } from '@esengine/ecs-framework';
class PauseMenuSystem extends EntitySystem {
public pauseGame(): void {
// True pause - all systems stop executing
Core.paused = true;
console.log('Game paused');
}
public resumeGame(): void {
// Resume game
Core.paused = false;
console.log('Game resumed');
}
public togglePause(): void {
Core.paused = !Core.paused;
console.log(Core.paused ? 'Game paused' : 'Game resumed');
}
}
```
#### Time.timeScale = 0
`Time.timeScale = 0` only makes `deltaTime` become 0, **systems still execute**:
```typescript
class SlowMotionSystem extends EntitySystem {
public freezeTime(): void {
// Time freeze - systems still execute, just deltaTime = 0
Time.timeScale = 0;
}
}
```
#### Comparison
| Feature | `Core.paused = true` | `Time.timeScale = 0` |
|---------|---------------------|---------------------|
| System Execution | Completely stopped | Still running |
| CPU Overhead | Zero | Normal overhead |
| Time Updates | Stopped | Continues (deltaTime=0) |
| Timers | Stopped | Continues (but time doesn't advance) |
| Use Cases | Pause menu, game pause | Slow motion, bullet time effects |
**Recommendations**:
- Pause menu, true game pause → Use `Core.paused = true`
- Slow motion, bullet time effects → Use `Time.timeScale`
### Time Scaling
The Time class supports time scaling for slow motion, fast forward, and other effects:
```typescript
class TimeControlSystem extends EntitySystem {
public enableSlowMotion(): void {
// Set to slow motion (50% speed)
Time.timeScale = 0.5;
console.log('Slow motion enabled');
}
public enableFastForward(): void {
// Set to fast forward (200% speed)
Time.timeScale = 2.0;
console.log('Fast forward enabled');
}
public enableBulletTime(): void {
// Bullet time effect (10% speed)
Time.timeScale = 0.1;
console.log('Bullet time enabled');
}
public resumeNormalSpeed(): void {
// Resume normal speed
Time.timeScale = 1.0;
console.log('Normal speed resumed');
}
protected process(entities: readonly Entity[]): void {
// deltaTime is affected by timeScale
const scaledDelta = Time.deltaTime; // Affected by time scale
const realDelta = Time.unscaledDeltaTime; // Not affected by time scale
for (const entity of entities) {
const movement = entity.getComponent(Movement);
if (movement) {
// Use scaled time for game logic updates
movement.update(scaledDelta);
}
const ui = entity.getComponent(UIComponent);
if (ui) {
// UI animations use real time, not affected by game time scale
ui.update(realDelta);
}
}
}
}
```
### Time Check Utilities
```typescript
class CooldownSystem extends EntitySystem {
private lastAttackTime = 0;
private lastSpawnTime = 0;
constructor() {
super(Matcher.all(Weapon));
}
protected process(entities: readonly Entity[]): void {
// Check attack cooldown
if (Time.checkEvery(1.5, this.lastAttackTime)) {
this.performAttack();
this.lastAttackTime = Time.totalTime;
}
// Check spawn interval
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
this.spawnEnemy();
this.lastSpawnTime = Time.totalTime;
}
}
private performAttack(): void {
console.log('Performing attack!');
}
private spawnEnemy(): void {
console.log('Spawning enemy!');
}
}
```
## Core.schedule Timer System
Core provides powerful timer scheduling functionality for creating one-time or repeating timers.
### Basic Timer Usage
```typescript
import { Core } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// Create one-time timers
this.createOneTimeTimers();
// Create repeating timers
this.createRepeatingTimers();
// Create timers with context
this.createContextTimers();
}
private createOneTimeTimers(): void {
// Execute once after 2 seconds
Core.schedule(2.0, false, null, (timer) => {
console.log('Executed after 2 second delay');
});
// Show tip after 5 seconds
Core.schedule(5.0, false, this, (timer) => {
const scene = timer.getContext<GameScene>();
scene.showTip('Game tip: 5 seconds have passed!');
});
}
private createRepeatingTimers(): void {
// Execute every second
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
console.log(`Game heartbeat - Total time: ${Time.totalTime.toFixed(1)}s`);
});
// Save timer reference for later control
this.saveTimerReference(heartbeatTimer);
}
private createContextTimers(): void {
const gameData = { score: 0, level: 1 };
// Add score every 2 seconds
Core.schedule(2.0, true, gameData, (timer) => {
const data = timer.getContext<typeof gameData>();
data.score += 10;
console.log(`Score increased! Current score: ${data.score}`);
});
}
private saveTimerReference(timer: any): void {
// Can stop timer later
setTimeout(() => {
timer.stop();
console.log('Timer stopped');
}, 10000); // Stop after 10 seconds
}
private showTip(message: string): void {
console.log('Tip:', message);
}
}
```
### Timer Control
```typescript
class TimerControlExample {
private attackTimer: any;
private spawnerTimer: any;
public startCombat(): void {
// Start attack timer
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
const self = timer.getContext<TimerControlExample>();
self.performAttack();
});
// Start enemy spawn timer
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
this.spawnEnemy();
});
}
public stopCombat(): void {
// Stop all combat-related timers
if (this.attackTimer) {
this.attackTimer.stop();
console.log('Attack timer stopped');
}
if (this.spawnerTimer) {
this.spawnerTimer.stop();
console.log('Spawn timer stopped');
}
}
public resetAttackTimer(): void {
// Reset attack timer
if (this.attackTimer) {
this.attackTimer.reset();
console.log('Attack timer reset');
}
}
private performAttack(): void {
console.log('Performing attack');
}
private spawnEnemy(): void {
console.log('Spawning enemy');
}
}
```
## Best Practices
### 1. Use Appropriate Time Types
```typescript
class MovementSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const movement = entity.getComponent(Movement);
// Use scaled time for game logic
movement.position.x += movement.velocity.x * Time.deltaTime;
// Use real time for UI animations (not affected by game pause)
const ui = entity.getComponent(UIAnimation);
if (ui) {
ui.update(Time.unscaledDeltaTime);
}
}
}
}
```
### 2. Timer Management
```typescript
class TimerManager {
private timers: any[] = [];
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
const timer = Core.schedule(duration, repeats, null, callback);
this.timers.push(timer);
return timer;
}
public stopAllTimers(): void {
for (const timer of this.timers) {
timer.stop();
}
this.timers = [];
}
public cleanupCompletedTimers(): void {
this.timers = this.timers.filter(timer => !timer.isDone);
}
}
```
### 3. Avoid Too Many Timers
```typescript
// Avoid: Creating a timer for each entity
class BadExample extends EntitySystem {
protected onAdded(entity: Entity): void {
Core.schedule(1.0, true, entity, (timer) => {
// One timer per entity - poor performance
});
}
}
// Recommended: Manage time uniformly in the system
class GoodExample extends EntitySystem {
private lastUpdateTime = 0;
protected process(entities: readonly Entity[]): void {
// Execute logic once per second
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
this.processAllEntities(entities);
this.lastUpdateTime = Time.totalTime;
}
}
private processAllEntities(entities: readonly Entity[]): void {
// Batch process all entities
}
}
```
### 4. Timer Context Usage
```typescript
interface TimerContext {
entityId: number;
duration: number;
onComplete: () => void;
}
class ContextualTimerExample {
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
const context: TimerContext = {
entityId,
duration,
onComplete
};
Core.schedule(duration, false, context, (timer) => {
const ctx = timer.getContext<TimerContext>();
console.log(`Timer for entity ${ctx.entityId} completed`);
ctx.onComplete();
});
}
}
```
The time and timer system is an essential tool in game development. Using these features correctly will make your game logic more precise and controllable.

View File

@@ -0,0 +1,570 @@
# Worker System
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
## Core Features
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
- **Type Safety**: Full TypeScript support and type checking
## Basic Usage
### Simple Physics System Example
```typescript
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true, // Enable Worker parallel processing
workerCount: 8, // Worker count, auto-limited to hardware capacity
entitiesPerWorker: 100, // Entities per Worker
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
entityDataSize: 7, // Data size per entity
maxEntities: 10000, // Maximum entity count
systemConfig: { // Configuration passed to Worker
gravity: 100,
friction: 0.95
}
});
}
// Data extraction: Convert Entity to serializable data
protected extractEntityData(entity: Entity): PhysicsData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: position.x,
y: position.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass,
radius: physics.radius
};
}
// Worker processing function: Pure function executed in Worker
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
// Apply gravity
entity.vy += config.gravity * deltaTime;
// Update position
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// Apply friction
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
// Apply results: Apply Worker processing results back to Entity
protected applyResult(entity: Entity, result: PhysicsData): void {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
position.x = result.x;
position.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer optimization support
protected getDefaultEntityDataSize(): number {
return 7; // id, x, y, vx, vy, mass, radius
}
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = entityData.id;
this.sharedFloatArray[offset + 1] = entityData.x;
this.sharedFloatArray[offset + 2] = entityData.y;
this.sharedFloatArray[offset + 3] = entityData.vx;
this.sharedFloatArray[offset + 4] = entityData.vy;
this.sharedFloatArray[offset + 5] = entityData.mass;
this.sharedFloatArray[offset + 6] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
vx: this.sharedFloatArray[offset + 3],
vy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6]
};
}
}
```
## Configuration Options
The Worker system supports rich configuration options:
```typescript
interface WorkerSystemConfig {
/** Enable Worker parallel processing */
enableWorker?: boolean;
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
workerCount?: number;
/** Entities per Worker for load distribution control */
entitiesPerWorker?: number;
/** System configuration data passed to Worker */
systemConfig?: any;
/** Enable SharedArrayBuffer optimization */
useSharedArrayBuffer?: boolean;
/** Float32 count per entity in SharedArrayBuffer */
entityDataSize?: number;
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
maxEntities?: number;
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
workerScriptPath?: string;
}
```
### Configuration Recommendations
```typescript
constructor() {
super(matcher, {
// Decide based on task complexity
enableWorker: this.shouldUseWorker(),
// Worker count: System auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
// Entities per Worker (optional)
entitiesPerWorker: 200, // Precise load distribution control
// Enable SharedArrayBuffer for many simple calculations
useSharedArrayBuffer: this.entityCount > 1000,
// Set according to actual data structure
entityDataSize: 8, // Ensure it matches data structure
// Estimated maximum entity count
maxEntities: 10000,
// Global configuration passed to Worker
systemConfig: {
gravity: 9.8,
friction: 0.95,
worldBounds: { width: 1920, height: 1080 }
}
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get system info
getSystemInfo() {
const info = this.getWorkerInfo();
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
console.log(`Current mode: ${info.currentMode}`);
}
```
## Processing Modes
The Worker system supports two processing modes:
### 1. Traditional Worker Mode
Data is serialized and passed between main thread and Workers:
```typescript
// Suitable for: Complex computation logic, moderate entity count
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: false, // Use traditional mode
workerCount: 2
});
}
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
// Complex algorithm logic
return entities.map(entity => {
// AI decisions, pathfinding, etc.
return this.complexAILogic(entity, deltaTime);
});
}
```
### 2. SharedArrayBuffer Mode
Zero-copy data sharing, suitable for many simple calculations:
```typescript
// Suitable for: Many entities with simple calculations
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: true, // Enable shared memory
entityDataSize: 6,
maxEntities: 10000
});
}
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
const entitySize = 6;
for (let i = startIndex; i < endIndex; i++) {
const offset = i * entitySize;
// Read data
let x = sharedFloatArray[offset];
let y = sharedFloatArray[offset + 1];
let vx = sharedFloatArray[offset + 2];
let vy = sharedFloatArray[offset + 3];
// Physics calculations
vy += config.gravity * deltaTime;
x += vx * deltaTime;
y += vy * deltaTime;
// Write back data
sharedFloatArray[offset] = x;
sharedFloatArray[offset + 1] = y;
sharedFloatArray[offset + 2] = vx;
sharedFloatArray[offset + 3] = vy;
}
};
}
```
## Use Cases
The Worker system is particularly suitable for:
### 1. Physics Simulation
- **Gravity systems**: Gravity calculations for many entities
- **Collision detection**: Complex collision algorithms
- **Fluid simulation**: Particle fluid systems
- **Cloth simulation**: Vertex physics calculations
### 2. AI Computation
- **Pathfinding**: A*, Dijkstra algorithms
- **Behavior trees**: Complex AI decision logic
- **Swarm intelligence**: Boid, fish school algorithms
- **Neural networks**: Simple AI inference
### 3. Data Processing
- **Bulk entity updates**: State machines, lifecycle management
- **Statistical calculations**: Game data analysis
- **Image processing**: Texture generation, effect calculations
- **Audio processing**: Sound synthesis, spectrum analysis
## Best Practices
### 1. Worker Function Requirements
```typescript
// Recommended: Worker processing function is a pure function
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
// Only use parameters and standard JavaScript APIs
return entities.map(entity => {
// Pure computation logic, no external state dependencies
entity.y += entity.velocity * deltaTime;
return entity;
});
}
// Avoid: Using external references in Worker function
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
// this and external variables are not available in Worker
return entities.map(entity => {
entity.y += this.someProperty; // Error
return entity;
});
}
```
### 2. Data Design
```typescript
// Recommended: Reasonable data design
interface SimplePhysicsData {
x: number;
y: number;
vx: number;
vy: number;
// Keep data structure simple for easy serialization
}
// Avoid: Complex nested objects
interface ComplexData {
transform: {
position: { x: number; y: number };
rotation: { angle: number };
};
// Complex nested structures increase serialization overhead
}
```
### 3. Worker Count Control
```typescript
// Recommended: Flexible Worker configuration
constructor() {
super(matcher, {
// Specify needed Worker count, system auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers
entitiesPerWorker: 100, // 100 entities per Worker
enableWorker: this.shouldUseWorker(), // Conditional enable
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get actual Worker info
checkWorkerConfiguration() {
const info = this.getWorkerInfo();
console.log(`Requested Workers: 8`);
console.log(`Actual Workers: ${info.workerCount}`);
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
}
```
### 4. Performance Monitoring
```typescript
// Recommended: Performance monitoring
public getPerformanceMetrics(): WorkerPerformanceMetrics {
return {
...this.getWorkerInfo(),
entityCount: this.entities.length,
averageProcessTime: this.getAverageProcessTime(),
workerUtilization: this.getWorkerUtilization()
};
}
```
## Performance Optimization Tips
### 1. Compute Intensity Assessment
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
### 2. Data Transfer Optimization
- Use SharedArrayBuffer to reduce serialization overhead
- Keep data structures simple and flat
- Avoid frequent large data transfers
### 3. Degradation Strategy
Always provide main thread fallback to ensure normal operation in environments without Worker support.
### 4. Memory Management
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
### 5. Load Balancing
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
## WeChat Mini Game Support
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
### WeChat Mini Game Worker Limitations
| Feature | Browser | WeChat Mini Game |
|---------|---------|------------------|
| Dynamic scripts (Blob URL) | Supported | Not supported |
| Worker count | Multiple | Maximum 1 |
| Script source | Any | Must be in code package |
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
### Using Worker Generator CLI
#### 1. Install the Tool
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. Configure workerScriptPath
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// Physics calculation logic
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... other methods
}
```
#### 3. Generate Worker Files
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
```bash
# Basic usage
npx esengine-worker-gen --src ./src --wechat
# Full options
npx esengine-worker-gen \
--src ./src \ # Source directory
--wechat \ # Generate WeChat Mini Game compatible code
--mapping \ # Generate worker-mapping.json
--verbose # Verbose output
```
The CLI tool will:
1. Scan source directory for all `WorkerEntitySystem` subclasses
2. Read each class's `workerScriptPath` configuration
3. Extract `workerProcess` method body
4. Convert to ES5 syntax (WeChat Mini Game compatible)
5. Generate to configured path
#### 4. Configure game.json
Configure workers directory in WeChat Mini Game's `game.json`:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. Project Structure
```
your-game/
├── game.js
├── game.json # Configure "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # Auto-generated
└── worker-mapping.json # Auto-generated
```
### Temporarily Disabling Workers
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
#### Method 1: Configuration Disable
```typescript
constructor() {
super(matcher, {
enableWorker: false, // Disable Worker, use main thread processing
// ...
});
}
```
#### Method 2: Platform Adapter Disable
Return Worker not supported in custom platform adapter:
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // Return false to disable Worker
}
// ...
}
```
### Important Notes
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
```typescript
// Correct: Only use parameters
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// Wrong: Using this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Cannot access this in Worker
return e;
});
}
```
3. **Pass configuration data via `systemConfig`**, not class properties
4. **Developer tool warnings can be ignored**:
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
## Online Demo
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
The demo showcases:
- Multi-threaded physics computation
- Real-time performance comparison
- SharedArrayBuffer optimization
- Parallel processing of many entities
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.

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

@@ -0,0 +1,404 @@
# Blueprint Visual Scripting
`@esengine/blueprint` provides a full-featured visual scripting system supporting node-based programming, event-driven execution, and blueprint composition.
## Installation
```bash
npm install @esengine/blueprint
```
## Quick Start
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
} from '@esengine/blueprint';
// Create blueprint system
const blueprintSystem = createBlueprintSystem(scene);
// Load blueprint asset
const blueprint = await loadBlueprintAsset('player.bp');
// Create blueprint component data
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
// Update in game loop
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
}
```
## Core Concepts
### Blueprint Asset Structure
Blueprints are saved as `.bp` files:
```typescript
interface BlueprintAsset {
version: number; // Format version
type: 'blueprint'; // Asset type
metadata: BlueprintMetadata; // Metadata
variables: BlueprintVariable[]; // Variable definitions
nodes: BlueprintNode[]; // Node instances
connections: BlueprintConnection[]; // Connections
}
```
### Node Categories
| Category | Description | Color |
|----------|-------------|-------|
| `event` | Event nodes (entry points) | Red |
| `flow` | Flow control | Gray |
| `entity` | Entity operations | Blue |
| `component` | Component access | Cyan |
| `math` | Math operations | Green |
| `logic` | Logic operations | Red |
| `variable` | Variable access | Purple |
| `time` | Time utilities | Cyan |
| `debug` | Debug utilities | Gray |
### Pin Types
Nodes connect through pins:
```typescript
interface BlueprintPinDefinition {
name: string; // Pin name
type: PinDataType; // Data type
direction: 'input' | 'output';
isExec?: boolean; // Execution pin
defaultValue?: unknown;
}
type PinDataType =
| 'exec' // Execution flow
| 'boolean' // Boolean
| 'number' // Number
| 'string' // String
| 'vector2' // 2D vector
| 'vector3' // 3D vector
| 'entity' // Entity reference
| 'component' // Component reference
| 'any'; // Any type
```
### Variable Scopes
```typescript
type VariableScope =
| 'local' // Per execution
| 'instance' // Per entity
| 'global'; // Shared globally
```
## Virtual Machine API
### BlueprintVM
The virtual machine executes blueprint graphs:
```typescript
import { BlueprintVM } from '@esengine/blueprint';
const vm = new BlueprintVM(blueprintAsset, entity, scene);
vm.start(); // Start (triggers BeginPlay)
vm.tick(deltaTime); // Update (triggers Tick)
vm.stop(); // Stop (triggers EndPlay)
vm.pause();
vm.resume();
// Trigger events
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });
// Debug mode
vm.debug = true;
```
### Execution Context
```typescript
interface ExecutionContext {
blueprint: BlueprintAsset;
entity: Entity;
scene: IScene;
deltaTime: number;
time: number;
getInput<T>(nodeId: string, pinName: string): T;
setOutput(nodeId: string, pinName: string, value: unknown): void;
getVariable<T>(name: string): T;
setVariable(name: string, value: unknown): void;
}
```
### Execution Result
```typescript
interface ExecutionResult {
outputs?: Record<string, unknown>; // Output values
nextExec?: string | null; // Next exec pin
delay?: number; // Delay execution (ms)
yield?: boolean; // Pause until next frame
error?: string; // Error message
}
```
## Custom Nodes
### Define Node Template
```typescript
import { BlueprintNodeTemplate } from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'A custom node example',
keywords: ['custom', 'example'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
```
### Implement Node Executor
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const value = context.getInput<number>(node.id, 'value');
const result = value * 2;
return {
outputs: { result },
nextExec: 'exec'
};
}
}
```
### Registration Methods
```typescript
// Method 1: Decorator
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }
// Method 2: Manual registration
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
```
## Node Registry
```typescript
import { NodeRegistry } from '@esengine/blueprint';
const registry = NodeRegistry.instance;
const allTemplates = registry.getAllTemplates();
const mathNodes = registry.getTemplatesByCategory('math');
const results = registry.searchTemplates('add');
if (registry.has('MyCustomNode')) { ... }
```
## Built-in Nodes
### Event Nodes
| Node | Description |
|------|-------------|
| `EventBeginPlay` | Triggered on blueprint start |
| `EventTick` | Triggered every frame |
| `EventEndPlay` | Triggered on blueprint stop |
| `EventCollision` | Triggered on collision |
| `EventInput` | Triggered on input |
| `EventTimer` | Triggered by timer |
### Time Nodes
| Node | Description |
|------|-------------|
| `Delay` | Delay execution |
| `GetDeltaTime` | Get frame delta |
| `GetTime` | Get total runtime |
### Math Nodes
| Node | Description |
|------|-------------|
| `Add`, `Subtract`, `Multiply`, `Divide` | Basic operations |
| `Abs`, `Clamp`, `Lerp`, `Min`, `Max` | Utility functions |
### Debug Nodes
| Node | Description |
|------|-------------|
| `Print` | Print to console |
## Blueprint Composition
### Blueprint Fragments
Encapsulate reusable logic as fragments:
```typescript
import { createFragment } from '@esengine/blueprint';
const healthFragment = createFragment('HealthSystem', {
inputs: [
{ name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
],
outputs: [
{ name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
],
graph: { nodes: [...], connections: [...], variables: [...] }
});
```
### Compose Blueprints
```typescript
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
// Register fragments
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);
// Create composer
const composer = createComposer('PlayerBlueprint');
// Add fragments to slots
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
// Connect slots
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
// Validate
const validation = composer.validate();
if (!validation.isValid) {
console.error(validation.errors);
}
// Compile to blueprint
const blueprint = composer.compile();
```
## Trigger System
### Define Trigger Conditions
```typescript
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
const lowHealthCondition: TriggerCondition = {
type: 'comparison',
left: { type: 'variable', name: 'health' },
operator: '<',
right: { type: 'constant', value: 20 }
};
```
### Use Trigger Dispatcher
```typescript
const dispatcher = new TriggerDispatcher();
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
context.triggerEvent('OnLowHealth');
});
dispatcher.evaluate(context);
```
## ECS Integration
### Using Blueprint System
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
this.blueprintSystem.process(this.entities, dt);
}
}
```
### Triggering Blueprint Events
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
```
## Serialization
### Save Blueprint
```typescript
import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void {
if (!validateBlueprintAsset(blueprint)) {
throw new Error('Invalid blueprint structure');
}
const json = JSON.stringify(blueprint, null, 2);
fs.writeFileSync(path, json);
}
```
### Load Blueprint
```typescript
async function loadBlueprint(path: string): Promise<BlueprintAsset> {
const json = await fs.readFile(path, 'utf-8');
const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) {
throw new Error('Invalid blueprint file');
}
return asset;
}
```
## Best Practices
1. **Use fragments for reusable logic**
2. **Choose appropriate variable scopes**
- `local`: Temporary calculations
- `instance`: Entity state (e.g., health)
- `global`: Game-wide state
3. **Avoid infinite loops** - VM has max steps per frame (default 1000)
4. **Debug techniques**
- Enable `vm.debug = true` for execution logs
- Use Print nodes for intermediate values
5. **Performance optimization**
- Pure nodes (`isPure: true`) cache outputs
- Avoid heavy computation in Tick

View File

@@ -0,0 +1,316 @@
# State Machine (FSM)
`@esengine/fsm` provides a type-safe finite state machine implementation for characters, AI, or any scenario requiring state management.
## Installation
```bash
npm install @esengine/fsm
```
## Quick Start
```typescript
import { createStateMachine } from '@esengine/fsm';
// Define state types
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
// Create state machine
const fsm = createStateMachine<PlayerState>('idle');
// Define states with callbacks
fsm.defineState('idle', {
onEnter: (ctx, from) => console.log(`Entered idle from ${from}`),
onExit: (ctx, to) => console.log(`Exiting idle to ${to}`),
onUpdate: (ctx, dt) => { /* Update every frame */ }
});
fsm.defineState('walk', {
onEnter: () => console.log('Started walking')
});
// Manual transition
fsm.transition('walk');
console.log(fsm.current); // 'walk'
```
## Core Concepts
### State Configuration
Each state can be configured with the following callbacks:
```typescript
interface StateConfig<TState, TContext> {
name: TState; // State name
onEnter?: (context: TContext, from: TState | null) => void; // Enter callback
onExit?: (context: TContext, to: TState) => void; // Exit callback
onUpdate?: (context: TContext, deltaTime: number) => void; // Update callback
tags?: string[]; // State tags
metadata?: Record<string, unknown>; // Metadata
}
```
### Transition Conditions
Define conditional state transitions:
```typescript
interface Context {
isMoving: boolean;
isRunning: boolean;
isGrounded: boolean;
}
const fsm = createStateMachine<PlayerState, Context>('idle', {
context: { isMoving: false, isRunning: false, isGrounded: true }
});
// Define transition conditions
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsm.defineTransition('walk', 'run', (ctx) => ctx.isRunning);
fsm.defineTransition('walk', 'idle', (ctx) => !ctx.isMoving);
// Automatically evaluate and execute matching transitions
fsm.evaluateTransitions();
```
### Transition Priority
When multiple transitions are valid, higher priority executes first:
```typescript
// Higher priority number = higher priority
fsm.defineTransition('idle', 'attack', (ctx) => ctx.isAttacking, 10);
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
// If both conditions are met, 'attack' (priority 10) is tried first
```
## API Reference
### createStateMachine
```typescript
function createStateMachine<TState extends string, TContext = unknown>(
initialState: TState,
options?: StateMachineOptions<TContext>
): IStateMachine<TState, TContext>
```
**Parameters:**
- `initialState` - Initial state
- `options.context` - Context object, accessible in callbacks
- `options.maxHistorySize` - Maximum history entries (default 100)
- `options.enableHistory` - Enable history tracking (default true)
### State Machine Properties
| Property | Type | Description |
|----------|------|-------------|
| `current` | `TState` | Current state |
| `previous` | `TState \| null` | Previous state |
| `context` | `TContext` | Context object |
| `isTransitioning` | `boolean` | Whether currently transitioning |
| `currentStateDuration` | `number` | Current state duration (ms) |
### State Machine Methods
#### State Definition
```typescript
// Define state
fsm.defineState('idle', {
onEnter: (ctx, from) => {},
onExit: (ctx, to) => {},
onUpdate: (ctx, dt) => {}
});
// Check if state exists
fsm.hasState('idle'); // true
// Get state configuration
fsm.getStateConfig('idle');
// Get all states
fsm.getStates(); // ['idle', 'walk', ...]
```
#### Transition Operations
```typescript
// Define transition
fsm.defineTransition('idle', 'walk', condition, priority);
// Remove transition
fsm.removeTransition('idle', 'walk');
// Get transitions from state
fsm.getTransitionsFrom('idle');
// Check if transition is possible
fsm.canTransition('walk'); // true/false
// Manual transition
fsm.transition('walk');
// Force transition (ignore conditions)
fsm.transition('walk', true);
// Auto-evaluate transition conditions
fsm.evaluateTransitions();
```
#### Lifecycle
```typescript
// Update state machine (calls current state's onUpdate)
fsm.update(deltaTime);
// Reset state machine
fsm.reset(); // Reset to current state
fsm.reset('idle'); // Reset to specified state
```
#### Event Listeners
```typescript
// Listen to entering specific state
const unsubscribe = fsm.onEnter('walk', (from) => {
console.log(`Entered walk from ${from}`);
});
// Listen to exiting specific state
fsm.onExit('walk', (to) => {
console.log(`Exiting walk to ${to}`);
});
// Listen to any state change
fsm.onChange((event) => {
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
});
// Unsubscribe
unsubscribe();
```
#### Debugging
```typescript
// Get state history
const history = fsm.getHistory();
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
// Clear history
fsm.clearHistory();
// Get debug info
const info = fsm.getDebugInfo();
// { current, previous, duration, stateCount, transitionCount, historySize }
```
## Practical Examples
### Character State Machine
```typescript
import { createStateMachine } from '@esengine/fsm';
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
interface CharacterContext {
velocity: { x: number; y: number };
isGrounded: boolean;
isAttacking: boolean;
speed: number;
}
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
context: {
velocity: { x: 0, y: 0 },
isGrounded: true,
isAttacking: false,
speed: 0
}
});
// Define states
characterFSM.defineState('idle', {
onEnter: (ctx) => { ctx.speed = 0; }
});
characterFSM.defineState('walk', {
onEnter: (ctx) => { ctx.speed = 100; }
});
characterFSM.defineState('run', {
onEnter: (ctx) => { ctx.speed = 200; }
});
// Define transitions
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
// Jump has highest priority
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
// Game loop usage
function gameUpdate(dt: number) {
// Update context
characterFSM.context.velocity.x = getInputVelocity();
characterFSM.context.isGrounded = checkGrounded();
// Evaluate transitions
characterFSM.evaluateTransitions();
// Update current state
characterFSM.update(dt);
}
```
### ECS Integration
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
// State machine component
class FSMComponent extends Component {
fsm: IStateMachine<string>;
constructor(initialState: string) {
super();
this.fsm = createStateMachine(initialState);
}
}
// State machine system
class FSMSystem extends EntitySystem {
constructor() {
super(Matcher.all(FSMComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const fsmComp = entity.getComponent(FSMComponent);
fsmComp.fsm.evaluateTransitions();
fsmComp.fsm.update(dt);
}
}
```
## Blueprint Nodes
The FSM module provides blueprint nodes for visual scripting:
- `GetCurrentState` - Get current state
- `TransitionTo` - Transition to specified state
- `CanTransition` - Check if transition is possible
- `IsInState` - Check if in specified state
- `WasInState` - Check if was ever in specified state
- `GetStateDuration` - Get state duration
- `EvaluateTransitions` - Evaluate transition conditions
- `ResetStateMachine` - Reset state machine

54
docs/en/modules/index.md Normal file
View File

@@ -0,0 +1,54 @@
# Modules
ESEngine provides a rich set of modules that can be imported as needed.
## Module List
### AI Modules
| Module | Package | Description |
|--------|---------|-------------|
| [Behavior Tree](/en/modules/behavior-tree/) | `@esengine/behavior-tree` | AI behavior tree with visual editor |
| [State Machine](/en/modules/fsm/) | `@esengine/fsm` | Finite state machine for character/AI states |
### Gameplay
| Module | Package | Description |
|--------|---------|-------------|
| [Timer](/en/modules/timer/) | `@esengine/timer` | Timer and cooldown system |
| [Spatial](/en/modules/spatial/) | `@esengine/spatial` | Spatial queries, AOI management |
| [Pathfinding](/en/modules/pathfinding/) | `@esengine/pathfinding` | A* pathfinding, NavMesh navigation |
### Tools
| Module | Package | Description |
|--------|---------|-------------|
| [Blueprint](/en/modules/blueprint/) | `@esengine/blueprint` | Visual scripting system |
| [Procgen](/en/modules/procgen/) | `@esengine/procgen` | Noise functions, random utilities |
### Network
| Module | Package | Description |
|--------|---------|-------------|
| [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking |
## Installation
All modules can be installed independently:
```bash
# Install a single module
npm install @esengine/behavior-tree
# Or use CLI to add to existing project
npx @esengine/cli add behavior-tree
```
## Platform Compatibility
All modules are pure TypeScript and compatible with:
- Cocos Creator 3.x
- Laya 3.x
- Node.js
- Browser

View File

@@ -0,0 +1,727 @@
# Network System
`@esengine/network` provides a TSRPC-based client-server network synchronization solution for multiplayer games, including entity synchronization, input handling, and state interpolation.
## Overview
The network module consists of three packages:
| Package | Description |
|---------|-------------|
| `@esengine/network` | Client-side ECS plugin |
| `@esengine/network-protocols` | Shared protocol definitions |
| `@esengine/network-server` | Server-side implementation |
## Installation
```bash
# Client
npm install @esengine/network
# Server
npm install @esengine/network-server
```
## Quick Setup with CLI
We recommend using ESEngine CLI to quickly create a complete game server project:
```bash
# Create project directory
mkdir my-game-server && cd my-game-server
npm init -y
# Initialize Node.js server with CLI
npx @esengine/cli init -p nodejs
```
The CLI will generate the following project structure:
```
my-game-server/
├── src/
│ ├── index.ts # Entry point
│ ├── server/
│ │ └── GameServer.ts # Network server configuration
│ └── game/
│ ├── Game.ts # ECS game class
│ ├── scenes/
│ │ └── MainScene.ts # Main scene
│ ├── components/ # ECS components
│ │ ├── PositionComponent.ts
│ │ └── VelocityComponent.ts
│ └── systems/ # ECS systems
│ └── MovementSystem.ts
├── tsconfig.json
├── package.json
└── README.md
```
Start the server:
```bash
# Development mode (hot reload)
npm run dev
# Production mode
npm run start
```
## Quick Start
### Client
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
NetworkTransform
} from '@esengine/network';
// Define game scene
class GameScene extends Scene {
initialize(): void {
this.name = 'Game';
// Network systems are automatically added by NetworkPlugin
}
}
// Initialize Core
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
// Install network plugin
const networkPlugin = new NetworkPlugin();
await Core.installPlugin(networkPlugin);
// Register prefab factory
networkPlugin.registerPrefab('player', (scene, spawn) => {
const entity = scene.createEntity(`player_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
entity.addComponent(new NetworkTransform());
return entity;
});
// Connect to server
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
if (success) {
console.log('Connected!');
}
// Game loop
function gameLoop(dt: number) {
Core.update(dt);
}
// Disconnect
await networkPlugin.disconnect();
```
### Server
After creating a server project with CLI, the generated code already configures GameServer:
```typescript
import { GameServer } from '@esengine/network-server';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16,
tickRate: 20
}
});
await server.start();
console.log('Server started on ws://localhost:3000');
```
## Core Concepts
### Architecture
```
Client Server
┌────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │
│ ├─ Service │ │ ├─ Room │
│ ├─ SyncSystem │ │ └─ Players │
│ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │
└────────────────┘
```
### Components
#### NetworkIdentity
Network identity component, required for every networked entity:
```typescript
class NetworkIdentity extends Component {
netId: number; // Network unique ID
ownerId: number; // Owner client ID
bIsLocalPlayer: boolean; // Whether local player
bHasAuthority: boolean; // Whether has control authority
}
```
#### NetworkTransform
Network transform component for position and rotation sync:
```typescript
class NetworkTransform extends Component {
position: { x: number; y: number };
rotation: number;
velocity: { x: number; y: number };
}
```
### Systems
#### NetworkSyncSystem
Handles server state synchronization and interpolation:
- Receives server state snapshots
- Stores states in snapshot buffer
- Performs interpolation for remote entities
#### NetworkSpawnSystem
Handles network entity spawning and despawning:
- Listens for Spawn/Despawn messages
- Creates entities using registered prefab factories
- Manages networked entity lifecycle
#### NetworkInputSystem
Handles local player input sending:
- Collects local player input
- Sends input to server
- Supports movement and action inputs
## API Reference
### NetworkPlugin
```typescript
class NetworkPlugin {
constructor(config: INetworkPluginConfig);
// Install plugin
install(services: ServiceContainer): void;
// Connect to server
connect(playerName: string, roomId?: string): Promise<void>;
// Disconnect
disconnect(): void;
// Register prefab factory
registerPrefab(prefab: string, factory: PrefabFactory): void;
// Properties
readonly localPlayerId: number | null;
readonly isConnected: boolean;
}
```
**Configuration:**
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `serverUrl` | `string` | Yes | WebSocket server URL |
### NetworkService
Network service managing WebSocket connections:
```typescript
class NetworkService {
// Connection state
readonly state: ENetworkState;
readonly isConnected: boolean;
readonly clientId: number | null;
readonly roomId: string | null;
// Connection control
connect(serverUrl: string): Promise<void>;
disconnect(): void;
// Join room
join(playerName: string, roomId?: string): Promise<ResJoin>;
// Send input
sendInput(input: IPlayerInput): void;
// Event callbacks
setCallbacks(callbacks: Partial<INetworkCallbacks>): void;
}
```
**Network state enum:**
```typescript
enum ENetworkState {
Disconnected = 'disconnected',
Connecting = 'connecting',
Connected = 'connected',
Joining = 'joining',
Joined = 'joined'
}
```
**Callbacks interface:**
```typescript
interface INetworkCallbacks {
onConnected?: () => void;
onDisconnected?: () => void;
onJoined?: (clientId: number, roomId: string) => void;
onSync?: (msg: MsgSync) => void;
onSpawn?: (msg: MsgSpawn) => void;
onDespawn?: (msg: MsgDespawn) => void;
}
```
### Prefab Factory
```typescript
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
```
Register prefab factories for network entity creation:
```typescript
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
const entity = scene.createEntity(`enemy_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
entity.addComponent(new NetworkTransform());
entity.addComponent(new EnemyComponent());
return entity;
});
```
### Input System
#### NetworkInputSystem
```typescript
class NetworkInputSystem extends EntitySystem {
// Add movement input
addMoveInput(x: number, y: number): void;
// Add action input
addActionInput(action: string): void;
// Clear input
clearInput(): void;
}
```
Usage example:
```typescript
// Send input via NetworkPlugin (recommended)
networkPlugin.sendMoveInput(0, 1); // Movement
networkPlugin.sendActionInput('jump'); // Action
// Or use inputSystem directly
const inputSystem = networkPlugin.inputSystem;
if (keyboard.isPressed('W')) {
inputSystem.addMoveInput(0, 1);
}
if (keyboard.isPressed('Space')) {
inputSystem.addActionInput('jump');
}
```
## State Synchronization
### Snapshot Buffer
Stores server state snapshots for interpolation:
```typescript
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
const buffer = createSnapshotBuffer<IStateSnapshot>({
maxSnapshots: 30, // Max snapshots
interpolationDelay: 100 // Interpolation delay (ms)
});
// Add snapshot
buffer.addSnapshot({
time: serverTime,
entities: states
});
// Get interpolated state
const interpolated = buffer.getInterpolatedState(clientTime);
```
### Transform Interpolators
#### Linear Interpolator
```typescript
import { createTransformInterpolator } from '@esengine/network';
const interpolator = createTransformInterpolator();
// Add state
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
// Get interpolated result
const state = interpolator.getInterpolatedState(currentTime);
```
#### Hermite Interpolator
Uses Hermite splines for smoother interpolation:
```typescript
import { createHermiteTransformInterpolator } from '@esengine/network';
const interpolator = createHermiteTransformInterpolator({
bufferSize: 10
});
// Add state with velocity
interpolator.addState(time, {
x: 100,
y: 200,
rotation: 0,
vx: 5,
vy: 0
});
// Get smooth interpolated result
const state = interpolator.getInterpolatedState(currentTime);
```
### Client Prediction
Implement client-side prediction with server reconciliation:
```typescript
import { createClientPrediction } from '@esengine/network';
const prediction = createClientPrediction({
maxPredictedInputs: 60,
reconciliationThreshold: 0.1
});
// Predict input
const seq = prediction.predict(inputState, currentState, (state, input) => {
// Apply input to state
return applyInput(state, input);
});
// Server reconciliation
const corrected = prediction.reconcile(
serverState,
serverSeq,
(state, input) => applyInput(state, input)
);
```
## Server Side
### GameServer
```typescript
import { GameServer } from '@esengine/network-server';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16, // Max players per room
tickRate: 20 // Sync rate (Hz)
}
});
// Start server
await server.start();
// Get room
const room = server.getOrCreateRoom('room-id');
// Stop server
await server.stop();
```
### Room
```typescript
class Room {
readonly id: string;
readonly playerCount: number;
readonly isFull: boolean;
// Add player
addPlayer(name: string, connection: Connection): IPlayer | null;
// Remove player
removePlayer(clientId: number): void;
// Get player
getPlayer(clientId: number): IPlayer | undefined;
// Handle input
handleInput(clientId: number, input: IPlayerInput): void;
// Destroy room
destroy(): void;
}
```
**Player interface:**
```typescript
interface IPlayer {
clientId: number; // Client ID
name: string; // Player name
connection: Connection; // Connection object
netId: number; // Network entity ID
}
```
## Protocol Types
### Message Types
```typescript
// State sync message
interface MsgSync {
time: number;
entities: IEntityState[];
}
// Entity state
interface IEntityState {
netId: number;
pos?: Vec2;
rot?: number;
}
// Spawn message
interface MsgSpawn {
netId: number;
ownerId: number;
prefab: string;
pos: Vec2;
rot: number;
}
// Despawn message
interface MsgDespawn {
netId: number;
}
// Input message
interface MsgInput {
input: IPlayerInput;
}
// Player input
interface IPlayerInput {
seq?: number;
moveDir?: Vec2;
actions?: string[];
}
```
### API Types
```typescript
// Join request
interface ReqJoin {
playerName: string;
roomId?: string;
}
// Join response
interface ResJoin {
clientId: number;
roomId: string;
playerCount: number;
}
```
## Blueprint Nodes
The network module provides blueprint nodes for visual scripting:
- `IsLocalPlayer` - Check if entity is local player
- `IsServer` - Check if running on server
- `HasAuthority` - Check if has authority over entity
- `GetNetworkId` - Get entity's network ID
- `GetLocalPlayerId` - Get local player ID
## Service Tokens
For dependency injection:
```typescript
import {
NetworkServiceToken,
NetworkSyncSystemToken,
NetworkSpawnSystemToken,
NetworkInputSystemToken
} from '@esengine/network';
// Get service
const networkService = services.get(NetworkServiceToken);
```
## Practical Example
### Complete Multiplayer Client
```typescript
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
NetworkTransform
} from '@esengine/network';
// Define game scene
class GameScene extends Scene {
initialize(): void {
this.name = 'MultiplayerGame';
// Network systems are automatically added by NetworkPlugin
// Add custom systems
this.addSystem(new LocalInputHandler());
}
}
// Initialize
async function initGame() {
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
// Install network plugin
const networkPlugin = new NetworkPlugin();
await Core.installPlugin(networkPlugin);
// Register player prefab
networkPlugin.registerPrefab('player', (scene, spawn) => {
const entity = scene.createEntity(`player_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
entity.addComponent(new NetworkTransform());
// If local player, add input marker
if (identity.isLocalPlayer) {
entity.addComponent(new LocalInputComponent());
}
return entity;
});
// Connect to server
const success = await networkPlugin.connect('ws://localhost:3000', 'Player1');
if (success) {
console.log('Connected!');
} else {
console.error('Connection failed');
}
return networkPlugin;
}
// Game loop
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
}
initGame();
```
### Handling Input
```typescript
class LocalInputHandler extends EntitySystem {
private _networkPlugin: NetworkPlugin | null = null;
constructor() {
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
}
protected onAddedToScene(): void {
// Get NetworkPlugin reference
this._networkPlugin = Core.getPlugin(NetworkPlugin);
}
protected processEntity(entity: Entity, dt: number): void {
if (!this._networkPlugin) return;
const identity = entity.getComponent(NetworkIdentity)!;
if (!identity.isLocalPlayer) return;
// Read keyboard input
let moveX = 0;
let moveY = 0;
if (keyboard.isPressed('A')) moveX -= 1;
if (keyboard.isPressed('D')) moveX += 1;
if (keyboard.isPressed('W')) moveY += 1;
if (keyboard.isPressed('S')) moveY -= 1;
if (moveX !== 0 || moveY !== 0) {
this._networkPlugin.sendMoveInput(moveX, moveY);
}
if (keyboard.isJustPressed('Space')) {
this._networkPlugin.sendActionInput('jump');
}
}
}
```
## Best Practices
1. **Set appropriate sync rate**: Choose `tickRate` based on game type, action games typically need 20-60 Hz
2. **Use interpolation delay**: Set appropriate `interpolationDelay` to balance latency and smoothness
3. **Client prediction**: Use client-side prediction for local players to reduce input lag
4. **Prefab management**: Register prefab factories for each networked entity type
5. **Authority checks**: Use `bHasAuthority` to check entity control permissions
6. **Connection state**: Monitor connection state changes, handle reconnection
```typescript
networkService.setCallbacks({
onConnected: () => console.log('Connected'),
onDisconnected: () => {
console.log('Disconnected');
// Handle reconnection logic
}
});
```

View File

@@ -0,0 +1,299 @@
# Pathfinding System
`@esengine/pathfinding` provides a complete 2D pathfinding solution including A* algorithm, grid maps, navigation meshes, and path smoothing.
## Installation
```bash
npm install @esengine/pathfinding
```
## Quick Start
### Grid Map Pathfinding
```typescript
import { createGridMap, createAStarPathfinder } from '@esengine/pathfinding';
// Create 20x20 grid
const grid = createGridMap(20, 20);
// Set obstacles
grid.setWalkable(5, 5, false);
grid.setWalkable(5, 6, false);
// Create pathfinder
const pathfinder = createAStarPathfinder(grid);
// Find path
const result = pathfinder.findPath(0, 0, 15, 15);
if (result.found) {
console.log('Path found!');
console.log('Path:', result.path);
console.log('Cost:', result.cost);
}
```
### NavMesh Pathfinding
```typescript
import { createNavMesh } from '@esengine/pathfinding';
const navmesh = createNavMesh();
// Add polygon areas
navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// Auto-build connections
navmesh.build();
// Find path
const result = navmesh.findPath(1, 1, 18, 8);
```
## Core Concepts
### IPathResult
```typescript
interface IPathResult {
readonly found: boolean; // Path found
readonly path: readonly IPoint[];// Path points
readonly cost: number; // Total cost
readonly nodesSearched: number; // Nodes searched
}
```
### IPathfindingOptions
```typescript
interface IPathfindingOptions {
maxNodes?: number; // Max search nodes (default 10000)
heuristicWeight?: number; // Heuristic weight (>1 faster but may be suboptimal)
allowDiagonal?: boolean; // Allow diagonal movement (default true)
avoidCorners?: boolean; // Avoid corner cutting (default true)
}
```
## Heuristic Functions
| Function | Use Case | Description |
|----------|----------|-------------|
| `manhattanDistance` | 4-directional | Manhattan distance |
| `euclideanDistance` | Any direction | Euclidean distance |
| `chebyshevDistance` | 8-directional | Diagonal cost = 1 |
| `octileDistance` | 8-directional | Diagonal cost = √2 (default) |
## Grid Map API
### createGridMap
```typescript
function createGridMap(
width: number,
height: number,
options?: IGridMapOptions
): GridMap
```
**Options:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `allowDiagonal` | `boolean` | `true` | Allow diagonal movement |
| `diagonalCost` | `number` | `√2` | Diagonal movement cost |
| `avoidCorners` | `boolean` | `true` | Avoid corner cutting |
| `heuristic` | `HeuristicFunction` | `octileDistance` | Heuristic function |
### Map Operations
```typescript
// Check/set walkability
grid.isWalkable(x, y);
grid.setWalkable(x, y, false);
// Set movement cost (e.g., swamp, sand)
grid.setCost(x, y, 2);
// Set rectangle region
grid.setRectWalkable(0, 0, 5, 5, false);
// Load from array (0=walkable, non-0=blocked)
grid.loadFromArray([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0]
]);
// Load from string (.=walkable, #=blocked)
grid.loadFromString(`
.....
.#.#.
`);
// Export and reset
console.log(grid.toString());
grid.reset();
```
## A* Pathfinder API
```typescript
const pathfinder = createAStarPathfinder(grid);
const result = pathfinder.findPath(
startX, startY,
endX, endY,
{ maxNodes: 5000, heuristicWeight: 1.5 }
);
// Pathfinder is reusable
pathfinder.findPath(0, 0, 10, 10);
pathfinder.findPath(5, 5, 15, 15);
```
## NavMesh API
```typescript
const navmesh = createNavMesh();
// Add convex polygons
const id1 = navmesh.addPolygon(vertices1);
const id2 = navmesh.addPolygon(vertices2);
// Auto-detect shared edges
navmesh.build();
// Or manually set connections
navmesh.setConnection(id1, id2, {
left: { x: 10, y: 0 },
right: { x: 10, y: 10 }
});
// Query and pathfind
const polygon = navmesh.findPolygonAt(5, 5);
navmesh.isWalkable(5, 5);
const result = navmesh.findPath(1, 1, 18, 8);
```
## Path Smoothing
### Line of Sight Smoothing
Remove unnecessary waypoints:
```typescript
import { createLineOfSightSmoother } from '@esengine/pathfinding';
const smoother = createLineOfSightSmoother();
const smoothedPath = smoother.smooth(result.path, grid);
```
### Curve Smoothing
Catmull-Rom spline:
```typescript
import { createCatmullRomSmoother } from '@esengine/pathfinding';
const smoother = createCatmullRomSmoother(5, 0.5);
const curvedPath = smoother.smooth(result.path, grid);
```
### Combined Smoothing
```typescript
import { createCombinedSmoother } from '@esengine/pathfinding';
const smoother = createCombinedSmoother(5, 0.5);
const finalPath = smoother.smooth(result.path, grid);
```
### Line of Sight Functions
```typescript
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
const hasLOS2 = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
```
## Practical Examples
### Dynamic Obstacles
```typescript
class DynamicPathfinding {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private dynamicObstacles: Set<string> = new Set();
addDynamicObstacle(x: number, y: number): void {
this.dynamicObstacles.add(`${x},${y}`);
this.grid.setWalkable(x, y, false);
}
removeDynamicObstacle(x: number, y: number): void {
this.dynamicObstacles.delete(`${x},${y}`);
this.grid.setWalkable(x, y, true);
}
}
```
### Terrain Costs
```typescript
const grid = createGridMap(50, 50);
// Normal ground - cost 1 (default)
// Sand - cost 2
for (let y = 10; y < 20; y++) {
for (let x = 0; x < 50; x++) {
grid.setCost(x, y, 2);
}
}
// Swamp - cost 4
for (let y = 30; y < 35; y++) {
for (let x = 20; x < 30; x++) {
grid.setCost(x, y, 4);
}
}
```
## Blueprint Nodes
- `FindPath` - Find path
- `FindPathSmooth` - Find and smooth path
- `IsWalkable` - Check walkability
- `GetPathLength` - Get path point count
- `GetPathDistance` - Get total path distance
- `GetPathPoint` - Get specific path point
- `MoveAlongPath` - Move along path
- `HasLineOfSight` - Check line of sight
## Performance Tips
1. **Limit search range**: `{ maxNodes: 1000 }`
2. **Use heuristic weight**: `{ heuristicWeight: 1.5 }` (faster but may not be optimal)
3. **Reuse pathfinder instances**
4. **Use NavMesh for complex terrain**
5. **Choose appropriate heuristic for movement type**
## Grid vs NavMesh
| Feature | GridMap | NavMesh |
|---------|---------|---------|
| Use Case | Regular tile maps | Complex polygon terrain |
| Memory | Higher (width × height) | Lower (polygon count) |
| Precision | Grid-aligned | Continuous coordinates |
| Dynamic Updates | Easy | Requires rebuild |
| Setup Complexity | Simple | More complex |

View File

@@ -0,0 +1,396 @@
# Procedural Generation (Procgen)
`@esengine/procgen` provides core tools for procedural content generation, including noise functions, seeded random numbers, and various random utilities.
## Installation
```bash
npm install @esengine/procgen
```
## Quick Start
### Noise Generation
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
// Create Perlin noise
const perlin = createPerlinNoise(12345); // seed
// Sample 2D noise
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]
// Use FBM for more natural results
const fbm = createFBM(perlin, {
octaves: 6,
persistence: 0.5
});
const height = fbm.noise2D(x * 0.01, y * 0.01);
```
### Seeded Random
```typescript
import { createSeededRandom } from '@esengine/procgen';
// Create deterministic random generator
const rng = createSeededRandom(42);
// Same seed always produces same sequence
console.log(rng.next()); // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3)); // 30% true
```
### Weighted Random
```typescript
import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
const loot = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
const drop = loot.pick(rng);
console.log(drop); // Likely 'common'
```
## Noise Functions
### Perlin Noise
Classic gradient noise, output range [-1, 1]:
```typescript
import { createPerlinNoise } from '@esengine/procgen';
const perlin = createPerlinNoise(seed);
const value2D = perlin.noise2D(x, y);
const value3D = perlin.noise3D(x, y, z);
```
### Simplex Noise
Faster than Perlin, less directional bias:
```typescript
import { createSimplexNoise } from '@esengine/procgen';
const simplex = createSimplexNoise(seed);
const value = simplex.noise2D(x, y);
```
### Worley Noise
Cell-based noise for stone, cell textures:
```typescript
import { createWorleyNoise } from '@esengine/procgen';
const worley = createWorleyNoise(seed);
const distance = worley.noise2D(x, y);
```
### FBM (Fractal Brownian Motion)
Layer multiple noise octaves for richer detail:
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
const baseNoise = createPerlinNoise(seed);
const fbm = createFBM(baseNoise, {
octaves: 6, // Layer count (more = richer detail)
lacunarity: 2.0, // Frequency multiplier
persistence: 0.5, // Amplitude decay
frequency: 1.0, // Initial frequency
amplitude: 1.0 // Initial amplitude
});
// Standard FBM
const value = fbm.noise2D(x, y);
// Ridged FBM (for mountains)
const ridged = fbm.ridged2D(x, y);
// Turbulence
const turb = fbm.turbulence2D(x, y);
// Billowed (for clouds)
const cloud = fbm.billowed2D(x, y);
```
## Seeded Random API
### SeededRandom
Deterministic PRNG based on xorshift128+:
```typescript
import { createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
```
### Basic Methods
```typescript
rng.next(); // [0, 1) float
rng.nextInt(1, 10); // [min, max] integer
rng.nextFloat(0, 100); // [min, max) float
rng.nextBool(); // 50%
rng.nextBool(0.3); // 30%
rng.reset(); // Reset to initial state
```
### Distribution Methods
```typescript
// Normal distribution (Gaussian)
rng.nextGaussian(); // mean 0, stdDev 1
rng.nextGaussian(100, 15); // mean 100, stdDev 15
// Exponential distribution
rng.nextExponential(); // λ = 1
rng.nextExponential(0.5); // λ = 0.5
```
### Geometry Methods
```typescript
// Uniform point in circle
const point = rng.nextPointInCircle(50); // { x, y }
// Point on circle edge
const edge = rng.nextPointOnCircle(50); // { x, y }
// Uniform point in sphere
const point3D = rng.nextPointInSphere(50); // { x, y, z }
// Random direction vector
const dir = rng.nextDirection2D(); // { x, y }, length 1
```
## Weighted Random API
### WeightedRandom
Precomputed cumulative weights for efficient selection:
```typescript
import { createWeightedRandom } from '@esengine/procgen';
const selector = createWeightedRandom([
{ value: 'apple', weight: 5 },
{ value: 'banana', weight: 3 },
{ value: 'cherry', weight: 2 }
]);
const result = selector.pick(rng);
const result2 = selector.pickRandom(); // Uses Math.random
console.log(selector.getProbability(0)); // 0.5 (5/10)
console.log(selector.size); // 3
console.log(selector.totalWeight); // 10
```
### Convenience Functions
```typescript
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
const item = weightedPick([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 2 }
], rng);
const item2 = weightedPickFromMap({
'common': 60,
'rare': 30,
'epic': 10
}, rng);
```
## Shuffle and Sampling
### shuffle / shuffleCopy
Fisher-Yates shuffle:
```typescript
import { shuffle, shuffleCopy } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5];
shuffle(arr, rng); // In-place
const shuffled = shuffleCopy(arr, rng); // Copy
```
### pickOne
```typescript
import { pickOne } from '@esengine/procgen';
const item = pickOne(['a', 'b', 'c', 'd'], rng);
```
### sample / sampleWithReplacement
```typescript
import { sample, sampleWithReplacement } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const unique = sample(arr, 3, rng); // 3 unique
const withRep = sampleWithReplacement(arr, 5, rng); // 5 with replacement
```
### randomIntegers
```typescript
import { randomIntegers } from '@esengine/procgen';
// 5 unique random integers from 1-100
const nums = randomIntegers(1, 100, 5, rng);
```
### weightedSample
```typescript
import { weightedSample } from '@esengine/procgen';
const items = ['A', 'B', 'C', 'D', 'E'];
const weights = [10, 8, 6, 4, 2];
const selected = weightedSample(items, weights, 3, rng);
```
## Practical Examples
### Procedural Terrain
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
class TerrainGenerator {
private fbm: FBM;
private moistureFbm: FBM;
constructor(seed: number) {
const heightNoise = createPerlinNoise(seed);
const moistureNoise = createPerlinNoise(seed + 1000);
this.fbm = createFBM(heightNoise, {
octaves: 8,
persistence: 0.5,
frequency: 0.01
});
this.moistureFbm = createFBM(moistureNoise, {
octaves: 4,
persistence: 0.6,
frequency: 0.02
});
}
getHeight(x: number, y: number): number {
let height = this.fbm.noise2D(x, y);
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
return (height + 1) * 0.5; // Normalize to [0, 1]
}
getBiome(x: number, y: number): string {
const height = this.getHeight(x, y);
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
if (height < 0.3) return 'water';
if (height < 0.4) return 'beach';
if (height > 0.8) return 'mountain';
if (moisture < 0.3) return 'desert';
if (moisture > 0.7) return 'forest';
return 'grassland';
}
}
```
### Loot System
```typescript
import { createSeededRandom, createWeightedRandom } from '@esengine/procgen';
class LootSystem {
private rng: SeededRandom;
private raritySelector: WeightedRandom<string>;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
this.raritySelector = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
}
generateLoot(count: number): LootItem[] {
const loot: LootItem[] = [];
for (let i = 0; i < count; i++) {
const rarity = this.raritySelector.pick(this.rng);
// Get item from rarity table...
loot.push(item);
}
return loot;
}
}
```
## Blueprint Nodes
### Noise Nodes
- `SampleNoise2D` - Sample 2D noise
- `SampleFBM` - Sample FBM noise
### Random Nodes
- `SeededRandom` - Generate random float
- `SeededRandomInt` - Generate random integer
- `WeightedPick` - Weighted random selection
- `ShuffleArray` - Shuffle array
- `PickRandom` - Pick random element
- `SampleArray` - Sample from array
- `RandomPointInCircle` - Random point in circle
## Best Practices
1. **Use seeds for reproducibility**
```typescript
const seed = Date.now();
const rng = createSeededRandom(seed);
saveSeed(seed);
```
2. **Precompute weighted selectors**
```typescript
// Good: Create once, use many times
const selector = createWeightedRandom(items);
for (let i = 0; i < 1000; i++) {
selector.pick(rng);
}
```
3. **Choose appropriate noise**
- Perlin: Smooth terrain, clouds
- Simplex: Performance-critical
- Worley: Cell textures, stone
- FBM: Natural multi-detail effects
4. **Tune FBM parameters**
- `octaves`: More = richer detail, higher cost
- `persistence`: 0.5 is common, higher = more high-frequency detail
- `lacunarity`: Usually 2, controls frequency growth

View File

@@ -0,0 +1,322 @@
# Spatial Index System
`@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management.
## Installation
```bash
npm install @esengine/spatial
```
## Quick Start
### Spatial Index
```typescript
import { createGridSpatialIndex } from '@esengine/spatial';
// Create spatial index (cell size 100)
const spatialIndex = createGridSpatialIndex<Entity>(100);
// Insert objects
spatialIndex.insert(player, { x: 100, y: 200 });
spatialIndex.insert(enemy1, { x: 150, y: 250 });
spatialIndex.insert(enemy2, { x: 500, y: 600 });
// Find objects within radius
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
console.log(nearby); // [player, enemy1]
// Find nearest object
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
console.log(nearest); // enemy1
// Update position
spatialIndex.update(player, { x: 120, y: 220 });
```
### AOI (Area of Interest)
```typescript
import { createGridAOI } from '@esengine/spatial';
// Create AOI manager
const aoi = createGridAOI<Entity>(100);
// Add observers
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
// Listen to enter/exit events
aoi.addListener((event) => {
if (event.type === 'enter') {
console.log(`${event.observer} saw ${event.target}`);
} else if (event.type === 'exit') {
console.log(`${event.target} left ${event.observer}'s view`);
}
});
// Update position (triggers enter/exit events)
aoi.updatePosition(player, { x: 200, y: 200 });
// Get visible entities
const visible = aoi.getEntitiesInView(player);
```
## Core Concepts
### Spatial Index vs AOI
| Feature | SpatialIndex | AOI |
|---------|--------------|-----|
| Purpose | General spatial queries | Entity visibility tracking |
| Events | No event notification | Enter/exit events |
| Direction | One-way query | Two-way tracking |
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
### IBounds
```typescript
interface IBounds {
readonly minX: number;
readonly minY: number;
readonly maxX: number;
readonly maxY: number;
}
```
### IRaycastHit
```typescript
interface IRaycastHit<T> {
readonly target: T; // Hit object
readonly point: IVector2; // Hit point
readonly normal: IVector2;// Hit normal
readonly distance: number;// Distance from origin
}
```
## Spatial Index API
### createGridSpatialIndex
```typescript
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
```
**Choosing cellSize:**
- Too small: High memory, reduced query efficiency
- Too large: Many objects per cell, slow iteration
- Recommended: 1-2x average object spacing
### Management Methods
```typescript
spatialIndex.insert(entity, position);
spatialIndex.remove(entity);
spatialIndex.update(entity, newPosition);
spatialIndex.clear();
```
### Query Methods
#### findInRadius
```typescript
const enemies = spatialIndex.findInRadius(
{ x: 100, y: 200 },
50,
(entity) => entity.type === 'enemy' // Optional filter
);
```
#### findInRect
```typescript
import { createBounds } from '@esengine/spatial';
const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);
```
#### findNearest
```typescript
const nearest = spatialIndex.findNearest(
playerPosition,
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### findKNearest
```typescript
const nearestEnemies = spatialIndex.findKNearest(
playerPosition,
5, // k
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### raycast / raycastFirst
```typescript
const hits = spatialIndex.raycast(origin, direction, maxDistance);
const firstHit = spatialIndex.raycastFirst(origin, direction, maxDistance);
```
## AOI API
### createGridAOI
```typescript
function createGridAOI<T>(cellSize?: number): GridAOI<T>
```
### Observer Management
```typescript
// Add observer
aoi.addObserver(player, position, {
viewRange: 200,
observable: true // Can be seen by others
});
// Remove observer
aoi.removeObserver(player);
// Update position
aoi.updatePosition(player, newPosition);
// Update view range
aoi.updateViewRange(player, 300);
```
### Query Methods
```typescript
// Get entities in observer's view
const visible = aoi.getEntitiesInView(player);
// Get observers who can see entity
const observers = aoi.getObserversOf(monster);
// Check visibility
if (aoi.canSee(player, enemy)) { ... }
```
### Event System
```typescript
// Global event listener
aoi.addListener((event) => {
switch (event.type) {
case 'enter': /* entered view */ break;
case 'exit': /* left view */ break;
}
});
// Entity-specific listener
aoi.addEntityListener(player, (event) => {
if (event.type === 'enter') {
sendToClient(player, 'entity_enter', event.target);
}
});
```
## Utility Functions
### Bounds Creation
```typescript
import {
createBounds,
createBoundsFromCenter,
createBoundsFromCircle
} from '@esengine/spatial';
const bounds1 = createBounds(0, 0, 100, 100);
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
```
### Geometry Checks
```typescript
import {
isPointInBounds,
boundsIntersect,
boundsIntersectsCircle,
distance,
distanceSquared
} from '@esengine/spatial';
if (isPointInBounds(point, bounds)) { ... }
if (boundsIntersect(boundsA, boundsB)) { ... }
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
const dist = distance(pointA, pointB);
const distSq = distanceSquared(pointA, pointB); // Faster
```
## Practical Examples
### Range Attack Detection
```typescript
class CombatSystem {
private spatialIndex: ISpatialIndex<Entity>;
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
const targets = this.spatialIndex.findInRadius(
center, radius,
(entity) => entity.hasComponent(HealthComponent)
);
for (const target of targets) {
target.getComponent(HealthComponent).takeDamage(damage);
}
}
}
```
### MMO Sync System
```typescript
class SyncSystem {
private aoi: IAOIManager<Player>;
constructor() {
this.aoi = createGridAOI<Player>(100);
this.aoi.addListener((event) => {
const packet = this.createSyncPacket(event);
this.sendToPlayer(event.observer, packet);
});
}
onPlayerMove(player: Player, newPosition: IVector2): void {
this.aoi.updatePosition(player, newPosition);
}
}
```
## Blueprint Nodes
### Spatial Query Nodes
- `FindInRadius`, `FindInRect`, `FindNearest`, `FindKNearest`
- `Raycast`, `RaycastFirst`
### AOI Nodes
- `GetEntitiesInView`, `GetObserversOf`, `CanSee`
- `OnEntityEnterView`, `OnEntityExitView`
## Service Tokens
```typescript
import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';
services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));
```

View File

@@ -0,0 +1,352 @@
# Timer System
`@esengine/timer` provides a flexible timer and cooldown system for delayed execution, repeating tasks, skill cooldowns, and more.
## Installation
```bash
npm install @esengine/timer
```
## Quick Start
```typescript
import { createTimerService } from '@esengine/timer';
// Create timer service
const timerService = createTimerService();
// One-time timer (executes after 1 second)
const handle = timerService.schedule('myTimer', 1000, () => {
console.log('Timer fired!');
});
// Repeating timer (every 100ms)
timerService.scheduleRepeating('heartbeat', 100, () => {
console.log('Tick');
});
// Cooldown system (5 second cooldown)
timerService.startCooldown('skill_fireball', 5000);
if (timerService.isCooldownReady('skill_fireball')) {
useFireball();
timerService.startCooldown('skill_fireball', 5000);
}
// Update in game loop
function gameLoop(deltaTime: number) {
timerService.update(deltaTime);
}
```
## Core Concepts
### Timer vs Cooldown
| Feature | Timer | Cooldown |
|---------|-------|----------|
| Purpose | Delayed code execution | Rate limiting |
| Callback | Has callback function | No callback |
| Repeat | Supports repeating | One-time |
| Query | Query remaining time | Query progress/ready status |
### TimerHandle
Handle object returned when scheduling a timer:
```typescript
interface TimerHandle {
readonly id: string; // Timer ID
readonly isValid: boolean; // Whether valid (not cancelled)
cancel(): void; // Cancel timer
}
```
### TimerInfo
Timer information object:
```typescript
interface TimerInfo {
readonly id: string; // Timer ID
readonly remaining: number; // Remaining time (ms)
readonly repeating: boolean; // Whether repeating
readonly interval?: number; // Interval (repeating only)
}
```
### CooldownInfo
Cooldown information object:
```typescript
interface CooldownInfo {
readonly id: string; // Cooldown ID
readonly duration: number; // Total duration (ms)
readonly remaining: number; // Remaining time (ms)
readonly progress: number; // Progress (0-1, 0=started, 1=finished)
readonly isReady: boolean; // Whether ready
}
```
## API Reference
### createTimerService
```typescript
function createTimerService(config?: TimerServiceConfig): ITimerService
```
**Configuration:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `maxTimers` | `number` | `0` | Maximum timer count (0 = unlimited) |
| `maxCooldowns` | `number` | `0` | Maximum cooldown count (0 = unlimited) |
### Timer API
#### schedule
Schedule a one-time timer:
```typescript
const handle = timerService.schedule('explosion', 2000, () => {
createExplosion();
});
// Cancel early
handle.cancel();
```
#### scheduleRepeating
Schedule a repeating timer:
```typescript
// Execute every second
timerService.scheduleRepeating('regen', 1000, () => {
player.hp += 5;
});
// Execute immediately once, then repeat every second
timerService.scheduleRepeating('tick', 1000, () => {
console.log('Tick');
}, true); // immediate = true
```
#### cancel / cancelById
Cancel timers:
```typescript
// Cancel by handle
handle.cancel();
// or
timerService.cancel(handle);
// Cancel by ID
timerService.cancelById('regen');
```
#### hasTimer
Check if timer exists:
```typescript
if (timerService.hasTimer('explosion')) {
console.log('Explosion is pending');
}
```
#### getTimerInfo
Get timer information:
```typescript
const info = timerService.getTimerInfo('explosion');
if (info) {
console.log(`Remaining: ${info.remaining}ms`);
console.log(`Repeating: ${info.repeating}`);
}
```
### Cooldown API
#### startCooldown
Start a cooldown:
```typescript
timerService.startCooldown('skill_fireball', 5000);
```
#### isCooldownReady / isOnCooldown
Check cooldown status:
```typescript
if (timerService.isCooldownReady('skill_fireball')) {
castFireball();
timerService.startCooldown('skill_fireball', 5000);
}
if (timerService.isOnCooldown('skill_fireball')) {
console.log('On cooldown...');
}
```
#### getCooldownProgress / getCooldownRemaining
Get cooldown progress:
```typescript
// Progress 0-1 (0=started, 1=complete)
const progress = timerService.getCooldownProgress('skill_fireball');
console.log(`Progress: ${(progress * 100).toFixed(0)}%`);
// Remaining time (ms)
const remaining = timerService.getCooldownRemaining('skill_fireball');
console.log(`Remaining: ${(remaining / 1000).toFixed(1)}s`);
```
#### getCooldownInfo
Get complete cooldown info:
```typescript
const info = timerService.getCooldownInfo('skill_fireball');
if (info) {
console.log(`Duration: ${info.duration}ms`);
console.log(`Remaining: ${info.remaining}ms`);
console.log(`Progress: ${info.progress}`);
console.log(`Ready: ${info.isReady}`);
}
```
#### resetCooldown / clearAllCooldowns
Reset cooldowns:
```typescript
// Reset single cooldown
timerService.resetCooldown('skill_fireball');
// Clear all cooldowns (e.g., on respawn)
timerService.clearAllCooldowns();
```
### Lifecycle
#### update
Update timer service (call every frame):
```typescript
function gameLoop(deltaTime: number) {
timerService.update(deltaTime); // deltaTime in ms
}
```
#### clear
Clear all timers and cooldowns:
```typescript
timerService.clear();
```
### Debug Properties
```typescript
console.log(timerService.activeTimerCount);
console.log(timerService.activeCooldownCount);
const timerIds = timerService.getActiveTimerIds();
const cooldownIds = timerService.getActiveCooldownIds();
```
## Practical Examples
### Skill Cooldown System
```typescript
import { createTimerService, type ITimerService } from '@esengine/timer';
class SkillSystem {
private timerService: ITimerService;
private skills: Map<string, SkillData> = new Map();
constructor() {
this.timerService = createTimerService();
}
useSkill(skillId: string): boolean {
const skill = this.skills.get(skillId);
if (!skill) return false;
if (!this.timerService.isCooldownReady(skillId)) {
const remaining = this.timerService.getCooldownRemaining(skillId);
console.log(`Skill ${skillId} on cooldown, ${remaining}ms remaining`);
return false;
}
this.executeSkill(skill);
this.timerService.startCooldown(skillId, skill.cooldown);
return true;
}
update(dt: number): void {
this.timerService.update(dt);
}
}
```
### DOT Effects
```typescript
class EffectSystem {
private timerService: ITimerService;
applyDOT(target: Entity, damage: number, duration: number): void {
const dotId = `dot_${target.id}_${Date.now()}`;
let elapsed = 0;
this.timerService.scheduleRepeating(dotId, 1000, () => {
elapsed += 1000;
target.takeDamage(damage);
if (elapsed >= duration) {
this.timerService.cancelById(dotId);
}
});
}
}
```
## Blueprint Nodes
### Cooldown Nodes
- `StartCooldown` - Start cooldown
- `IsCooldownReady` - Check if cooldown is ready
- `GetCooldownProgress` - Get cooldown progress
- `GetCooldownInfo` - Get cooldown info
- `ResetCooldown` - Reset cooldown
### Timer Nodes
- `HasTimer` - Check if timer exists
- `CancelTimer` - Cancel timer
- `GetTimerRemaining` - Get timer remaining time
## Service Token
For dependency injection:
```typescript
import { TimerServiceToken, createTimerService } from '@esengine/timer';
services.register(TimerServiceToken, createTimerService());
const timerService = services.get(TimerServiceToken);
```

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
@@ -371,6 +430,137 @@ class GameManager {
}
```
## 编译查询 (CompiledQuery)
> **v2.4.0+**
CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。
### 基本用法
```typescript
// 创建编译查询
const query = scene.querySystem.compile(Position, Velocity);
// 类型安全的遍历 - 组件参数自动推断类型
query.forEach((entity, pos, vel) => {
pos.x += vel.vx * deltaTime;
pos.y += vel.vy * deltaTime;
});
// 获取实体数量
console.log(`匹配实体数: ${query.count}`);
// 获取第一个匹配的实体
const first = query.first();
if (first) {
const [entity, pos, vel] = first;
console.log(`第一个实体: ${entity.name}`);
}
```
### 变更检测
CompiledQuery 支持基于 epoch 的变更检测:
```typescript
class RenderSystem extends EntitySystem {
private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
private _lastEpoch = 0;
protected onInitialize(): void {
this._query = this.scene!.querySystem.compile(Transform, Sprite);
}
protected process(entities: readonly Entity[]): void {
// 只处理 Transform 或 Sprite 发生变化的实体
this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
this.updateRenderData(entity, transform, sprite);
});
// 保存当前 epoch 作为下次检查的起点
this._lastEpoch = this.scene!.epochManager.current;
}
private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
// 更新渲染数据
}
}
```
### 函数式 API
CompiledQuery 提供了丰富的函数式 API
```typescript
const query = scene.querySystem.compile(Position, Health);
// map - 转换实体数据
const positions = query.map((entity, pos, health) => ({
x: pos.x,
y: pos.y,
healthPercent: health.current / health.max
}));
// filter - 过滤实体
const lowHealthEntities = query.filter((entity, pos, health) => {
return health.current < health.max * 0.2;
});
// find - 查找第一个匹配的实体
const target = query.find((entity, pos, health) => {
return health.current > 0 && pos.x > 100;
});
// toArray - 转换为数组
const allData = query.toArray();
for (const [entity, pos, health] of allData) {
console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
}
// any/empty - 检查是否有匹配
if (query.any()) {
console.log('有匹配的实体');
}
if (query.empty()) {
console.log('没有匹配的实体');
}
```
### CompiledQuery vs EntitySystem
| 特性 | CompiledQuery | EntitySystem |
|------|---------------|--------------|
| **用途** | 轻量级查询工具 | 完整的系统逻辑 |
| **生命周期** | 无 | 完整 (onInitialize, onDestroy 等) |
| **调度集成** | 无 | 支持 @Stage, @Before, @After |
| **变更检测** | forEachChanged | forEachChanged |
| **事件监听** | 无 | addEventListener |
| **命令缓冲** | 无 | this.commands |
| **类型安全组件** | forEach 参数自动推断 | 需要手动 getComponent |
| **适用场景** | 临时查询、工具、原型 | 核心游戏逻辑 |
**选择建议**
- 使用 **EntitySystem** 处理核心游戏逻辑移动、战斗、AI 等)
- 使用 **CompiledQuery** 进行一次性查询、工具开发或简单迭代
### CompiledQuery API 参考
| 方法 | 说明 |
|------|------|
| `forEach(callback)` | 遍历所有匹配实体,类型安全的组件参数 |
| `forEachChanged(sinceEpoch, callback)` | 只遍历变更的实体 |
| `first()` | 获取第一个匹配的实体和组件 |
| `toArray()` | 转换为 [entity, ...components] 数组 |
| `map(callback)` | 映射转换 |
| `filter(predicate)` | 过滤实体 |
| `find(predicate)` | 查找第一个满足条件的实体 |
| `any()` | 是否有任何匹配 |
| `empty()` | 是否没有匹配 |
| `count` | 匹配的实体数量 |
| `entities` | 匹配的实体列表(只读) |
## 最佳实践
### 1. 优先使用 EntitySystem
@@ -493,6 +683,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,156 @@ entity.components.forEach(component => {
});
```
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。
## 实体句柄 (EntityHandle)
实体句柄是一种安全的实体引用方式,用于解决"引用已销毁实体"的问题。
### 问题场景
假设你的 AI 系统需要追踪一个目标敌人:
```typescript
// 错误做法:直接存储实体引用
class AISystem extends EntitySystem {
private targetEnemy: Entity | null = null;
setTarget(enemy: Entity) {
this.targetEnemy = enemy;
}
process() {
if (this.targetEnemy) {
// 危险!敌人可能已被销毁,但引用还在
// 更糟糕:这个内存位置可能被新实体复用了
const health = this.targetEnemy.getComponent(Health);
// 可能操作了错误的实体!
}
}
}
```
### 使用句柄的正确做法
每个实体创建时会自动分配一个句柄,通过 `entity.handle` 获取:
```typescript
import { EntityHandle, NULL_HANDLE, isValidHandle } from '@esengine/ecs-framework';
class AISystem extends EntitySystem {
// 存储句柄而非实体引用
private targetHandle: EntityHandle = NULL_HANDLE;
setTarget(enemy: Entity) {
// 保存敌人的句柄
this.targetHandle = enemy.handle;
}
process() {
if (!isValidHandle(this.targetHandle)) {
return; // 没有目标
}
// 通过句柄获取实体(自动检测是否有效)
const enemy = this.scene.findEntityByHandle(this.targetHandle);
if (!enemy) {
// 敌人已被销毁,清空引用
this.targetHandle = NULL_HANDLE;
return;
}
// 安全操作
const health = enemy.getComponent(Health);
}
}
```
### 完整示例:技能目标锁定
```typescript
import {
EntitySystem, Entity, EntityHandle, NULL_HANDLE, isValidHandle
} from '@esengine/ecs-framework';
@ECSSystem('SkillTargeting')
class SkillTargetingSystem extends EntitySystem {
// 存储多个目标的句柄
private lockedTargets: Map<Entity, EntityHandle> = new Map();
// 锁定目标
lockTarget(caster: Entity, target: Entity) {
this.lockedTargets.set(caster, target.handle);
}
// 获取锁定的目标
getLockedTarget(caster: Entity): Entity | null {
const handle = this.lockedTargets.get(caster);
if (!handle || !isValidHandle(handle)) {
return null;
}
// findEntityByHandle 会检查句柄是否有效
const target = this.scene.findEntityByHandle(handle);
if (!target) {
// 目标已死亡,清除锁定
this.lockedTargets.delete(caster);
}
return target;
}
// 释放技能
castSkill(caster: Entity) {
const target = this.getLockedTarget(caster);
if (!target) {
console.log('目标丢失,技能取消');
return;
}
// 安全地对目标造成伤害
const health = target.getComponent(Health);
if (health) {
health.current -= 10;
}
}
}
```
### 句柄 vs 实体引用
| 场景 | 推荐方式 |
|-----|---------|
| 同一帧内临时使用 | 直接用 `Entity` 引用 |
| 跨帧存储(如 AI 目标、技能目标) | 使用 `EntityHandle` |
| 需要序列化保存 | 使用 `EntityHandle`(数字类型) |
| 网络同步 | 使用 `EntityHandle`(可直接传输) |
### API 速查
```typescript
// 获取实体的句柄
const handle = entity.handle;
// 检查句柄是否非空
if (isValidHandle(handle)) { ... }
// 通过句柄获取实体(自动检测有效性)
const entity = scene.findEntityByHandle(handle);
// 检查句柄对应的实体是否存活
const alive = scene.handleManager.isAlive(handle);
// 空句柄常量
const emptyHandle = NULL_HANDLE;
```
## 下一步
- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系
- 了解 [组件系统](./component.md) 为实体添加功能
- 了解 [场景管理](./scene.md) 组织和管理实体

View File

@@ -4,7 +4,24 @@
## 安装
### NPM 安装
### 使用 CLI推荐
在现有项目中添加 ECS 的最简单方式:
```bash
# 在项目目录中运行
npx @esengine/cli init
```
CLI 会自动检测项目类型Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js并生成相应的集成代码包括
- `ECSManager` 组件/脚本 - 负责 ECS 生命周期管理
- 示例组件和系统 - 帮助快速上手
- 自动安装依赖
### NPM 手动安装
如果你更喜欢手动配置,可以直接安装:
```bash
# 使用 npm
@@ -23,7 +40,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 +55,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 +70,6 @@ interface ICoreConfig {
/** 是否启用调试模式 - 影响日志级别和性能监控 */
debug?: boolean;
/** 是否启用实体系统 - 核心ECS功能开关 */
enableEntitySystems?: boolean;
/** 高级调试配置 - 用于开发工具集成 */
debugConfig?: {
enabled: boolean; // 是否启用调试服务器
@@ -338,27 +350,39 @@ function gameLoop(deltaTime: number) {
## 与游戏引擎集成
### Laya 引擎集成
### Laya 3.x 引擎集成
推荐使用 `Laya.Script` 组件来管理 ECS 生命周期:
```typescript
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
import { Core, Scene } from '@esengine/ecs-framework';
// 初始化 Laya
Laya.init(800, 600).then(() => {
// 初始化 ECS
Core.create(true);
Core.setScene(new GameScene());
const { regClass } = Laya;
// 启动游戏循环
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // 自动更新全局服务和场景
});
});
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new GameScene();
onAwake(): void {
// 初始化 ECS
Core.create({ debug: true });
Core.setScene(this.ecsScene);
}
onUpdate(): void {
// 自动更新全局服务和场景
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
// 清理资源
Core.destroy();
}
}
```
在 Laya IDE 中,将 `ECSManager` 脚本挂载到场景中的节点上即可。
### Cocos Creator 集成
```typescript

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

@@ -0,0 +1,437 @@
# 层级系统
在游戏开发中实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent``HierarchySystem` 实现,完全遵循 ECS 组合原则。
## 设计理念
### 为什么不在 Entity 中内置层级?
传统的游戏对象模型将层级关系内置于实体中。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

@@ -0,0 +1,360 @@
# 持久化实体
> **版本**: v2.3.0+
持久化实体Persistent Entity是一种可以在场景切换时自动迁移到新场景的特殊实体。适用于需要跨场景保持状态的游戏对象如玩家、游戏管理器、音频管理器等。
## 基本概念
在 ECS 框架中,实体有两种生命周期策略:
| 策略 | 说明 | 默认 |
|-----|------|------|
| `SceneLocal` | 场景本地实体,场景切换时销毁 | ✓ |
| `Persistent` | 持久化实体,场景切换时自动迁移 | |
## 快速开始
### 创建持久化实体
```typescript
import { Scene } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 创建持久化玩家实体
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(100, 200));
player.addComponent(new PlayerData('Hero', 500));
// 创建普通敌人实体(场景切换时销毁)
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(300, 200));
enemy.addComponent(new EnemyAI());
}
}
```
### 场景切换时的行为
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 初始场景
class Level1Scene extends Scene {
protected initialize(): void {
// 玩家 - 持久化,会迁移到下一个场景
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(0, 0));
player.addComponent(new Health(100));
// 敌人 - 场景本地,切换时销毁
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(100, 100));
}
}
// 目标场景
class Level2Scene extends Scene {
protected initialize(): void {
// 新的敌人
const enemy = this.createEntity('Boss');
enemy.addComponent(new Position(200, 200));
}
public onStart(): void {
// 玩家已自动迁移到此场景
const player = this.findEntity('Player');
console.log(player !== null); // true
// 位置和血量数据完整保留
const position = player?.getComponent(Position);
const health = player?.getComponent(Health);
console.log(position?.x, position?.y); // 0, 0
console.log(health?.value); // 100
}
}
// 切换场景
Core.create({ debug: true });
Core.setScene(new Level1Scene());
// 稍后切换到 Level2
Core.loadScene(new Level2Scene());
// Player 实体自动迁移Enemy 实体被销毁
```
## API 参考
### Entity 方法
#### setPersistent()
将实体标记为持久化,场景切换时不会被销毁。
```typescript
public setPersistent(): this
```
**返回**: 返回实体本身,支持链式调用
**示例**:
```typescript
const player = scene.createEntity('Player')
.setPersistent();
player.addComponent(new Position(100, 200));
```
#### setSceneLocal()
将实体恢复为场景本地策略(默认)。
```typescript
public setSceneLocal(): this
```
**返回**: 返回实体本身,支持链式调用
**示例**:
```typescript
// 动态取消持久化
player.setSceneLocal();
```
#### isPersistent
检查实体是否为持久化实体。
```typescript
public get isPersistent(): boolean
```
**示例**:
```typescript
if (entity.isPersistent) {
console.log('这是持久化实体');
}
```
#### lifecyclePolicy
获取实体的生命周期策略。
```typescript
public get lifecyclePolicy(): EEntityLifecyclePolicy
```
**示例**:
```typescript
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
console.log('持久化实体');
}
```
### Scene 方法
#### findPersistentEntities()
查找场景中所有持久化实体。
```typescript
public findPersistentEntities(): Entity[]
```
**返回**: 持久化实体数组
**示例**:
```typescript
const persistentEntities = scene.findPersistentEntities();
console.log(`场景中有 ${persistentEntities.length} 个持久化实体`);
```
#### extractPersistentEntities()
提取并从场景中移除所有持久化实体(通常由框架内部调用)。
```typescript
public extractPersistentEntities(): Entity[]
```
**返回**: 被提取的持久化实体数组
#### receiveMigratedEntities()
接收迁移过来的实体(通常由框架内部调用)。
```typescript
public receiveMigratedEntities(entities: Entity[]): void
```
**参数**:
- `entities` - 要接收的实体数组
## 使用场景
### 1. 玩家实体跨关卡
```typescript
class PlayerSetupScene extends Scene {
protected initialize(): void {
// 玩家在所有关卡中保持状态
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Transform(0, 0));
player.addComponent(new Health(100));
player.addComponent(new Inventory());
player.addComponent(new PlayerStats());
}
}
class Level1 extends Scene { /* ... */ }
class Level2 extends Scene { /* ... */ }
class Level3 extends Scene { /* ... */ }
// 玩家实体会自动在所有关卡间迁移
Core.setScene(new PlayerSetupScene());
// ... 游戏进行
Core.loadScene(new Level1());
// ... 关卡完成
Core.loadScene(new Level2());
// 玩家数据(血量、物品栏、属性)完整保留
```
### 2. 全局管理器
```typescript
class BootstrapScene extends Scene {
protected initialize(): void {
// 音频管理器 - 跨场景保持
const audioManager = this.createEntity('AudioManager').setPersistent();
audioManager.addComponent(new AudioController());
// 成就管理器 - 跨场景保持
const achievementManager = this.createEntity('AchievementManager').setPersistent();
achievementManager.addComponent(new AchievementTracker());
// 游戏设置 - 跨场景保持
const settings = this.createEntity('GameSettings').setPersistent();
settings.addComponent(new SettingsData());
}
}
```
### 3. 动态切换持久化状态
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 初始创建为普通实体
const companion = this.createEntity('Companion');
companion.addComponent(new Transform(0, 0));
companion.addComponent(new CompanionAI());
// 监听招募事件
this.eventSystem.on('companion:recruited', () => {
// 招募后变为持久化实体
companion.setPersistent();
console.log('同伴已加入队伍,将跟随玩家跨场景');
});
// 监听解散事件
this.eventSystem.on('companion:dismissed', () => {
// 解散后恢复为场景本地实体
companion.setSceneLocal();
console.log('同伴已离队,不再跨场景');
});
}
}
```
## 最佳实践
### 1. 明确标识持久化实体
```typescript
// 推荐:在创建时立即标记
const player = this.createEntity('Player').setPersistent();
// 不推荐:创建后再标记(容易遗漏)
const player = this.createEntity('Player');
// ... 很多代码 ...
player.setPersistent(); // 容易忘记
```
### 2. 合理使用持久化
```typescript
// ✓ 适合持久化的实体
const player = this.createEntity('Player').setPersistent(); // 玩家
const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器
const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统
// ✗ 不应持久化的实体
const bullet = this.createEntity('Bullet'); // 临时对象
const enemy = this.createEntity('Enemy'); // 关卡特定敌人
const particle = this.createEntity('Particle'); // 特效粒子
```
### 3. 检查迁移后的实体
```typescript
class NewScene extends Scene {
public onStart(): void {
// 检查预期的持久化实体是否存在
const player = this.findEntity('Player');
if (!player) {
console.error('玩家实体未正确迁移!');
// 处理错误情况
}
}
}
```
### 4. 避免循环引用
```typescript
// ✗ 避免:持久化实体引用场景本地实体
class BadScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// 危险player 持久化但 enemy 不是
// 场景切换后 enemy 被销毁,引用失效
player.addComponent(new TargetComponent(enemy));
}
}
// ✓ 推荐:使用 ID 引用或事件系统
class GoodScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// 存储 ID 而非直接引用
player.addComponent(new TargetComponent(enemy.id));
// 或使用事件系统通信
}
}
```
## 注意事项
1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久化也不会迁移。
2. **组件数据完整保留**:迁移时所有组件及其状态都会保留。
3. **场景引用会更新**:迁移后实体的 `scene` 属性会指向新场景。
4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。
5. **延迟切换同样生效**:使用 `Core.loadScene()` 延迟切换时,持久化实体同样会迁移。
## 相关文档
- [场景管理](./scene.md) - 了解场景的基本使用
- [SceneManager](./scene-manager.md) - 了解场景切换
- [WorldManager](./world-manager.md) - 了解多世界管理

View File

@@ -6,409 +6,198 @@
## 特性支持
-**Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json
-**SharedArrayBuffer**: 不支持
-**Transferable Objects**: 不支持(只支持可序列化对象)
-**高精度时间**: 使用 `Date.now()``wx.getPerformance()`
-**设备信息**: 完整的微信小游戏设备信息
| 特性 | 支持情况 | 说明 |
|------|----------|------|
| **Worker** | ✅ 支持 | 需要使用预编译文件,配置 `workerScriptPath` |
| **SharedArrayBuffer** | ❌ 不支持 | 微信小游戏环境不支持 |
| **Transferable Objects** | ❌ 不支持 | 只支持可序列化对象 |
| **高精度时间** | ✅ 支持 | 使用 `wx.getPerformance()` |
| **设备信息** | ✅ 支持 | 完整的微信小游戏设备信息 |
## 完整实现
## WorkerEntitySystem 使用方式
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig,
WeChatDeviceInfo
} from '@esengine/ecs-framework';
### 重要:微信小游戏 Worker 限制
/**
* 微信小游戏平台适配器
* 支持微信小游戏环境
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
微信小游戏的 Worker 有以下限制:
- **Worker 脚本必须在代码包内**,不能动态生成
- **必须在 `game.json` 中配置** `workers` 目录
- **最多只能创建 1 个 Worker**
constructor() {
// 获取微信小游戏版本信息
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.version || 'unknown';
}
因此,使用 `WorkerEntitySystem` 时有两种方式:
1. **推荐:使用 CLI 工具自动生成** Worker 文件
2. 手动创建 Worker 文件
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
// 微信小游戏支持Worker通过wx.createWorker创建
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
### 方式一:使用 CLI 工具自动生成(推荐)
/**
* 检查是否支持SharedArrayBuffer不支持
*/
public isSharedArrayBufferSupported(): boolean {
return false; // 微信小游戏不支持SharedArrayBuffer
}
我们提供了 `@esengine/worker-generator` 工具,可以自动从你的 TypeScript 代码中提取 `workerProcess` 函数并生成微信小游戏兼容的 Worker 文件。
/**
* 获取硬件并发数
*/
public getHardwareConcurrency(): number {
// 微信小游戏官方限制:最多只能创建 1 个 Worker
return 1;
}
#### 安装
/**
* 创建Worker
* @param script 脚本内容或文件路径
* @param options Worker创建选项
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏不支持Worker');
}
```bash
pnpm add -D @esengine/worker-generator
# 或
npm install --save-dev @esengine/worker-generator
```
try {
return new WeChatWorker(script, options);
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
#### 使用
/**
* 创建SharedArrayBuffer不支持
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null; // 微信小游戏不支持SharedArrayBuffer
}
```bash
# 扫描 src 目录,生成 Worker 文件到 workers 目录
npx esengine-worker-gen --src ./src --out ./workers --wechat
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
// 尝试使用微信的性能API否则使用Date.now()
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
return performance.now();
}
return Date.now();
}
# 查看帮助
npx esengine-worker-gen --help
```
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker
supportsModuleWorker: false, // 不支持模块Worker
supportsTransferableObjects: this.checkTransferableObjectsSupport(),
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 微信小游戏限制eval使用
requiresWorkerInit: false,
memoryLimit: this.getMemoryLimit(),
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'创建新Worker前必须先调用 Worker.terminate()',
'Worker脚本必须为项目内相对路径',
'需要在 game.json 中配置 workers 路径',
'使用 worker.onMessage() 而不是 self.onmessage',
'需要基础库 1.9.90 及以上版本'
]
},
extensions: {
platform: 'wechat-minigame',
systemInfo: this.systemInfo,
appId: this.systemInfo.host?.appId || 'unknown'
}
};
}
#### 参数说明
/**
* 获取微信小游戏设备信息
*/
public getDeviceInfo(): WeChatDeviceInfo {
return {
// 设备基础信息
brand: this.systemInfo.brand,
model: this.systemInfo.model,
platform: this.systemInfo.platform,
system: this.systemInfo.system,
benchmarkLevel: this.systemInfo.benchmarkLevel,
cpuType: this.systemInfo.cpuType,
memorySize: this.systemInfo.memorySize,
deviceAbi: this.systemInfo.deviceAbi,
abi: this.systemInfo.abi,
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-s, --src <dir>` | 源代码目录 | `./src` |
| `-o, --out <dir>` | 输出目录 | `./workers` |
| `-w, --wechat` | 生成微信小游戏兼容代码 | `false` |
| `-m, --mapping` | 生成 worker-mapping.json | `true` |
| `-t, --tsconfig <path>` | TypeScript 配置文件路径 | 自动查找 |
| `-v, --verbose` | 详细输出 | `false` |
// 窗口信息
screenWidth: this.systemInfo.screenWidth,
screenHeight: this.systemInfo.screenHeight,
screenTop: this.systemInfo.screenTop,
windowWidth: this.systemInfo.windowWidth,
windowHeight: this.systemInfo.windowHeight,
pixelRatio: this.systemInfo.pixelRatio,
statusBarHeight: this.systemInfo.statusBarHeight,
safeArea: this.systemInfo.safeArea,
#### 示例输出
// 应用信息
version: this.systemInfo.version,
language: this.systemInfo.language,
theme: this.systemInfo.theme,
SDKVersion: this.systemInfo.SDKVersion,
enableDebug: this.systemInfo.enableDebug,
fontSizeSetting: this.systemInfo.fontSizeSetting,
host: this.systemInfo.host
};
}
```
🔧 ESEngine Worker Generator
/**
* 异步获取完整的平台配置
*/
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
// 可以在这里添加异步获取设备性能信息的逻辑
const baseConfig = this.getPlatformConfig();
Source directory: /project/src
Output directory: /project/workers
WeChat mode: Yes
// 尝试获取设备性能信息
try {
const benchmarkLevel = await this.getBenchmarkLevel();
baseConfig.extensions = {
...baseConfig.extensions,
benchmarkLevel
};
} catch (error) {
console.warn('获取性能基准失败:', error);
}
Scanning for WorkerEntitySystem classes...
return baseConfig;
}
✓ Found 1 WorkerEntitySystem class(es):
- PhysicsSystem (src/systems/PhysicsSystem.ts)
/**
* 检查是否支持Transferable Objects
*/
private checkTransferableObjectsSupport(): boolean {
// 微信小游戏不支持 Transferable Objects
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
return false;
}
Generating Worker files...
/**
* 获取系统信息
*/
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
return {};
}
}
return {};
}
✓ Successfully generated 1 Worker file(s):
- PhysicsSystem -> workers/physics-system-worker.js
/**
* 获取内存限制
*/
private getMemoryLimit(): number {
// 微信小游戏通常有内存限制
const memorySize = this.systemInfo.memorySize;
if (memorySize) {
// 解析内存大小字符串(如 "4GB"
const match = memorySize.match(/(\d+)([GM]B)?/i);
if (match) {
const value = parseInt(match[1], 10);
const unit = match[2]?.toUpperCase();
📝 Usage:
1. Copy the generated files to your project's workers/ directory
2. Configure game.json (WeChat): { "workers": "workers" }
3. In your System constructor, add:
workerScriptPath: 'workers/physics-system-worker.js'
```
if (unit === 'GB') {
return value * 1024 * 1024 * 1024;
} else if (unit === 'MB') {
return value * 1024 * 1024;
}
}
}
#### 在构建流程中集成
// 默认限制为512MB
return 512 * 1024 * 1024;
}
/**
* 异步获取设备性能基准
*/
private async getBenchmarkLevel(): Promise<number> {
return new Promise((resolve) => {
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res: any) => {
resolve(res.benchmarkLevel || 0);
},
fail: () => {
resolve(0);
}
});
} else {
resolve(this.systemInfo.benchmarkLevel || 0);
}
});
}
}
/**
* 微信Worker封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
private scriptPath: string;
private isTemporaryFile: boolean = false;
constructor(script: string, options: WorkerCreationOptions = {}) {
if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') {
throw new Error('微信小游戏不支持Worker');
}
try {
// 判断 script 是文件路径还是脚本内容
if (this.isFilePath(script)) {
// 直接使用文件路径
this.scriptPath = script;
this.isTemporaryFile = false;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true // 启用实验性Worker获得更好性能
});
} else {
// 微信小游戏不支持动态脚本内容,只能使用文件路径
// 将脚本内容写入文件系统
this.scriptPath = this.writeScriptToFile(script, options.name);
this.isTemporaryFile = true;
this.worker = wx.createWorker(this.scriptPath, {
useExperimentalWorker: true
});
}
} catch (error) {
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
}
}
/**
* 判断是否为文件路径
*/
private isFilePath(script: string): boolean {
// 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径
return script.endsWith('.js') &&
!script.includes('\n') &&
!script.includes(';') &&
script.length < 200; // 文件路径通常不会太长
}
/**
* 将脚本内容写入文件系统
* 注意微信小游戏不支持blob URL只能使用文件系统
*/
private writeScriptToFile(script: string, name?: string): string {
const fs = wx.getFileSystemManager();
const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`;
const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`;
try {
fs.writeFileSync(filePath, script, 'utf8');
return filePath;
} catch (error) {
throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`);
}
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
// 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数
this.worker.postMessage(message);
} catch (error) {
throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
// 微信小游戏使用 onMessage 方法,不是 onmessage 属性
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
// 注意:微信小游戏 Worker 的错误处理可能与标准不同
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
// 清理临时脚本文件
this.cleanupScriptFile();
} catch (error) {
console.error('终止微信Worker失败:', error);
}
}
}
/**
* 清理临时脚本文件
*/
private cleanupScriptFile(): void {
// 只清理临时创建的文件,不清理用户提供的文件路径
if (this.scriptPath && this.isTemporaryFile) {
try {
const fs = wx.getFileSystemManager();
fs.unlinkSync(this.scriptPath);
} catch (error) {
console.warn('清理Worker脚本文件失败:', error);
}
}
}
```json
// package.json
{
"scripts": {
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
"build": "pnpm build:workers && your-build-command"
}
}
```
## 使用方法
### 方式二:手动创建 Worker 文件
### 1. 复制代码
如果你不想使用 CLI 工具,也可以手动创建 Worker 文件。
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`
在项目中创建 `workers/entity-worker.js`
### 2. 注册适配器
```javascript
// workers/entity-worker.js
// 微信小游戏 WorkerEntitySystem 通用 Worker 模板
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
let sharedFloatArray = null;
// 检查是否在微信小游戏环境
if (typeof wx !== 'undefined') {
const wechatAdapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(wechatAdapter);
worker.onMessage(function(e) {
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
try {
// 处理 SharedArrayBuffer 初始化
if (type === 'init' && sharedBuffer) {
sharedFloatArray = new Float32Array(sharedBuffer);
worker.postMessage({ type: 'init', success: true });
return;
}
// 处理 SharedArrayBuffer 数据
if (type === 'shared' && sharedFloatArray) {
processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig);
worker.postMessage({ id, result: null });
return;
}
// 传统处理方式
if (entities) {
const result = workerProcess(entities, deltaTime, systemConfig);
// 处理 Promise 返回值
if (result && typeof result.then === 'function') {
result.then(function(finalResult) {
worker.postMessage({ id, result: finalResult });
}).catch(function(error) {
worker.postMessage({ id, error: error.message });
});
} else {
worker.postMessage({ id, result: result });
}
}
} catch (error) {
worker.postMessage({ id, error: error.message });
}
});
/**
* 实体处理函数 - 根据你的业务逻辑修改此函数
* @param {Array} entities - 实体数据数组
* @param {number} deltaTime - 帧间隔时间
* @param {Object} systemConfig - 系统配置
* @returns {Array} 处理后的实体数据
*/
function workerProcess(entities, deltaTime, systemConfig) {
// ====== 在这里编写你的处理逻辑 ======
// 示例:物理计算
return entities.map(function(entity) {
// 应用重力
entity.vy += (systemConfig.gravity || 100) * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= (systemConfig.friction || 0.95);
entity.vy *= (systemConfig.friction || 0.95);
return entity;
});
}
/**
* SharedArrayBuffer 处理函数(可选)
*/
function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) {
if (!sharedFloatArray) return;
// ====== 根据需要实现 SharedArrayBuffer 处理逻辑 ======
// 注意:微信小游戏不支持 SharedArrayBuffer此函数通常不会被调用
}
```
### 3. WorkerEntitySystem 使用方式
### 步骤 2配置 game.json
微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建
`game.json` 中添加 workers 配置
#### 基本使用方式(推荐)
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers"
}
```
### 步骤 3使用 WorkerEntitySystem
```typescript
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
@@ -426,13 +215,17 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: true,
workerCount: 1, // 微信小游戏限制只能创建1个Worker
systemConfig: { gravity: 100, friction: 0.95 }
workerCount: 1, // 微信小游戏限制只能创建 1 个 Worker
workerScriptPath: 'workers/entity-worker.js', // 指定预编译的 Worker 文件
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected getDefaultEntityDataSize(): number {
return 6; // id, x, y, vx, vy, mass
return 6;
}
protected extractEntityData(entity: Entity): PhysicsData {
@@ -450,20 +243,15 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
};
}
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
// 注意:在微信小游戏中,此方法不会被使用
// Worker 的处理逻辑在 workers/entity-worker.js 中的 workerProcess 函数里
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
return entities.map(entity => {
// 应用重力
entity.vy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// 应用摩擦力
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
@@ -477,201 +265,219 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer 相关方法(微信小游戏不支持,可省略)
protected writeEntityToBuffer(data: PhysicsData, offset: number): void {}
protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; }
}
```
#### 使用预先创建的 Worker 文件(可选
### 临时禁用 Worker降级到同步模式
如果你希望使用预先创建的 Worker 文件
如果遇到问题,可以临时禁用 Worker
```typescript
// 1. 在 game.json 中配置 Worker 路径
/*
{
"workers": "workers"
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Transform, Velocity), {
enableWorker: false, // 禁用 Worker使用主线程同步处理
// ... 其他配置
});
}
}
*/
```
// 2. 创建 workers/physics.js 文件
// workers/physics.js 内容:
/*
// 微信小游戏 Worker 使用标准的 self.onmessage
self.onmessage = function(e) {
const { type, id, entities, deltaTime, systemConfig } = e.data;
## 完整适配器实现
if (entities) {
// 处理物理计算
const results = entities.map(entity => {
entity.vy += systemConfig.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
```typescript
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 微信小游戏平台适配器
*/
export class WeChatMiniGameAdapter implements IPlatformAdapter {
public readonly name = 'wechat-minigame';
public readonly version: string;
private systemInfo: any;
constructor() {
this.systemInfo = this.getSystemInfo();
this.version = this.systemInfo.SDKVersion || 'unknown';
}
public isWorkerSupported(): boolean {
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
}
public isSharedArrayBufferSupported(): boolean {
return false;
}
public getHardwareConcurrency(): number {
return 1; // 微信小游戏最多 1 个 Worker
}
public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('微信小游戏环境不支持 Worker');
}
// scriptPath 必须是代码包内的文件路径
const worker = wx.createWorker(scriptPath, {
useExperimentalWorker: true
});
self.postMessage({ id, result: results });
return new WeChatWorker(worker);
}
};
*/
// 3. 通过平台适配器直接创建不推荐WorkerEntitySystem会自动处理
const adapter = PlatformManager.getInstance().getAdapter();
const worker = adapter.createWorker('workers/physics.js');
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
return null;
}
public getHighResTimestamp(): number {
if (typeof wx !== 'undefined' && wx.getPerformance) {
return wx.getPerformance().now();
}
return Date.now();
}
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1,
supportsModuleWorker: false,
supportsTransferableObjects: false,
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true, // 重要:标记不支持动态脚本
requiresWorkerInit: false,
memoryLimit: 512 * 1024 * 1024,
workerNotSupported: false,
workerLimitations: [
'最多只能创建 1 个 Worker',
'Worker 脚本必须在代码包内',
'需要在 game.json 中配置 workers 路径',
'需要使用 workerScriptPath 配置'
]
},
extensions: {
platform: 'wechat-minigame',
sdkVersion: this.systemInfo.SDKVersion
}
};
}
private getSystemInfo(): any {
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
try {
return wx.getSystemInfoSync();
} catch (error) {
console.warn('获取微信系统信息失败:', error);
}
}
return {};
}
}
/**
* 微信 Worker 封装
*/
class WeChatWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: any;
constructor(worker: any) {
this.worker = worker;
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker 已被终止');
}
this.worker.postMessage(message);
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onMessage((res: any) => {
handler({ data: res });
});
}
public onError(handler: (error: ErrorEvent) => void): void {
if (this.worker.onError) {
this.worker.onError(handler);
}
}
public terminate(): void {
if (this._state === 'running') {
this.worker.terminate();
this._state = 'terminated';
}
}
}
```
### 4. 获取设备信息
## 注册适配器
```typescript
const manager = PlatformManager.getInstance();
if (manager.hasAdapter()) {
const adapter = manager.getAdapter();
console.log('微信设备信息:', adapter.getDeviceInfo());
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
// 在游戏启动时注册适配器
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
PlatformManager.getInstance().registerAdapter(adapter);
}
```
## 官方文档参考
在使用微信小游戏 Worker 之前,建议先阅读官方文档:
- [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html)
- [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html)
- [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html)
- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html)
## 重要注意事项
### Worker 限制和配置
### Worker 限制
微信小游戏的 Worker 有以下限制:
- **数量限制**: 最多只能创建 1 个 Worker
- **版本要求**: 需要基础库 1.9.90 及以上版本
- **脚本支持**: 不支持 blob URL只能使用文件路径或写入文件系统
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker
- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()`
- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能
#### Worker 配置(可选)
如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置:
```json
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"workers": "workers",
"subpackages": []
}
```
**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。
| 限制项 | 说明 |
|--------|------|
| 数量限制 | 最多只能创建 1 个 Worker |
| 版本要求 | 需要基础库 1.9.90 及以上 |
| 脚本位置 | 必须在代码包内,不支持动态生成 |
| 生命周期 | 创建新 Worker 前必须先 terminate() |
### 内存限制
微信小游戏有严格的内存限制:
- 通常限制在 256MB - 512MB
- 需要及时释放不用的资源
- 避免内存泄漏
### API 限制
- 不支持 `eval()` 函数
- 不支持 `Function` 构造器
- DOM API 受限
- 文件系统 API 受限
## 性能优化建议
### 1. 分帧处理
- 建议监听内存警告:
```typescript
class FramedProcessor {
private tasks: (() => void)[] = [];
private isProcessing = false;
public addTask(task: () => void): void {
this.tasks.push(task);
if (!this.isProcessing) {
this.processNextFrame();
}
}
private processNextFrame(): void {
this.isProcessing = true;
const startTime = Date.now();
const frameTime = 16; // 16ms per frame
while (this.tasks.length > 0 && Date.now() - startTime < frameTime) {
const task = this.tasks.shift();
if (task) task();
}
if (this.tasks.length > 0) {
setTimeout(() => this.processNextFrame(), 0);
} else {
this.isProcessing = false;
}
}
}
```
### 2. 内存管理
```typescript
class MemoryManager {
private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB
public static checkMemoryUsage(): void {
if (typeof wx !== 'undefined' && wx.getPerformance) {
const performance = wx.getPerformance();
const memoryInfo = performance.getEntries().find(
(entry: any) => entry.entryType === 'memory'
);
if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) {
console.warn('内存使用率过高,建议清理资源');
// 触发垃圾回收或资源清理
}
}
}
}
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
```
## 调试技巧
```typescript
// 检查微信小游戏环境
if (typeof wx !== 'undefined') {
const adapter = new WeChatMiniGameAdapter();
// 检查 Worker 配置
const adapter = PlatformManager.getInstance().getAdapter();
const config = adapter.getPlatformConfig();
console.log('微信版本:', adapter.version);
console.log('设备信息:', adapter.getDeviceInfo());
console.log('平台配置:', adapter.getPlatformConfig());
// 检查功能支持
console.log('Worker支持:', adapter.isWorkerSupported());
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
}
console.log('Worker 支持:', adapter.isWorkerSupported());
console.log('最大 Worker 数:', config.maxWorkerCount);
console.log('平台限制:', config.limitations);
```
## 微信小游戏特殊API
```typescript
// 获取设备性能等级
if (typeof wx !== 'undefined' && wx.getDeviceInfo) {
wx.getDeviceInfo({
success: (res) => {
console.log('设备性能等级:', res.benchmarkLevel);
}
});
}
// 监听内存警告
if (typeof wx !== 'undefined' && wx.onMemoryWarning) {
wx.onMemoryWarning(() => {
console.warn('收到内存警告,开始清理资源');
// 清理不必要的资源
});
}
```

View File

@@ -1,4 +1,4 @@
# SceneManager
# SceneManager
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API支持场景切换和延迟加载。
@@ -19,6 +19,7 @@ SceneManager 适合以下场景:
- 自动管理 ECS 流式 API
- 自动处理场景生命周期
- 集成在 Core 中,自动更新
- 支持[持久化实体](./persistent-entity.md)跨场景迁移v2.3.0+
## 基本使用
@@ -672,4 +673,9 @@ setTimeout(() => {
}, 3000);
```
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。如果你需要更高级的多世界隔离功能,请参考 [WorldManager](./world-manager.md) 文档。
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松地管理场景切换。
## 相关文档
- [持久化实体](./persistent-entity.md) - 了解如何让实体跨场景保持状态
- [WorldManager](./world-manager.md) - 了解更高级的多世界隔离功能

View File

@@ -1,4 +1,4 @@
# 场景管理
# 场景管理
在 ECS 架构中场景Scene是游戏世界的容器负责管理实体、系统和组件的生命周期。场景提供了完整的 ECS 运行环境。
@@ -657,5 +657,6 @@ world.setSceneActive('main', true);
- 了解 [SceneManager](./scene-manager.md) - 适用于大多数游戏的简单场景管理
- 了解 [WorldManager](./world-manager.md) - 适用于需要多世界隔离的高级场景
- 了解 [持久化实体](./persistent-entity.md) - 让实体跨场景保持状态v2.3.0+
场景是 ECS 框架的核心容器,正确使用场景管理能让你的游戏架构更加清晰、模块化和易于维护。

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

@@ -1,4 +1,4 @@
# 系统架构
# 系统架构
在 ECS 架构中系统System是处理业务逻辑的地方。系统负责对拥有特定组件组合的实体执行操作是 ECS 架构的逻辑处理单元。
@@ -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) 文档。
## 系统生命周期
系统提供了完整的生命周期回调:
@@ -179,11 +216,13 @@ class ExampleSystem extends EntitySystem {
// 主要的处理逻辑
for (const entity of entities) {
// 处理每个实体
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
}
}
protected lateProcess(entities: readonly Entity[]): void {
// 主处理之后的后期处理
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
}
protected onEnd(): void {
@@ -233,6 +272,172 @@ class EnemyManagerSystem extends EntitySystem {
}
```
### 重要onAdded/onRemoved 的调用时机
> ⚠️ **注意**`onAdded` 和 `onRemoved` 回调是**同步调用**的,会在 `addComponent`/`removeComponent` 返回**之前**立即执行。
这意味着:
```typescript
// ❌ 错误的用法:链式赋值在 onAdded 之后才执行
const comp = entity.addComponent(new ClickComponent());
comp.element = this._element; // 此时 onAdded 已经执行完了!
// ✅ 正确的用法:通过构造函数传入初始值
const comp = entity.addComponent(new ClickComponent(this._element));
// ✅ 或者使用 createComponent 方法
const comp = entity.createComponent(ClickComponent, this._element);
```
**为什么这样设计?**
事件驱动设计确保 `onAdded`/`onRemoved` 回调不受系统注册顺序的影响。当组件被添加时,所有监听该组件的系统都会立即收到通知,而不是等到下一帧。
**最佳实践:**
1. 组件的初始值应该通过**构造函数**传入
2. 不要依赖 `addComponent` 返回后再设置属性
3. 如果需要在 `onAdded` 中访问组件属性,确保这些属性在构造时已经设置
### 在 process/lateProcess 中安全地修改组件
`process``lateProcess` 中迭代实体时,可以安全地添加或移除组件,不会影响当前的迭代过程:
```typescript
@ECSSystem('Damage')
class DamageSystem extends EntitySystem {
constructor() {
super(Matcher.all(Health, DamageReceiver));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
const damage = entity.getComponent(DamageReceiver);
if (health && damage) {
health.current -= damage.amount;
// ✅ 安全:移除组件不会影响当前迭代
entity.removeComponent(damage);
if (health.current <= 0) {
// ✅ 安全:添加组件也不会影响当前迭代
entity.addComponent(new Dead());
}
}
}
}
}
```
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。
## 命令缓冲区 (CommandBuffer)
> **v2.3.0+**
CommandBuffer 提供了一种延迟执行实体操作的机制。当你需要在迭代过程中销毁实体或进行其他可能影响迭代的操作时,使用 CommandBuffer 可以将这些操作推迟到帧末统一执行。
### 基本用法
每个 EntitySystem 都内置了 `commands` 属性:
```typescript
@ECSSystem('Damage')
class DamageSystem extends EntitySystem {
constructor() {
super(Matcher.all(Health, DamageReceiver));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
const damage = entity.getComponent(DamageReceiver);
if (health && damage) {
health.current -= damage.amount;
// 使用命令缓冲区延迟移除组件
this.commands.removeComponent(entity, DamageReceiver);
if (health.current <= 0) {
// 延迟添加死亡标记
this.commands.addComponent(entity, new Dead());
// 延迟销毁实体
this.commands.destroyEntity(entity);
}
}
}
}
}
```
### 支持的命令
| 方法 | 说明 |
|------|------|
| `addComponent(entity, component)` | 延迟添加组件 |
| `removeComponent(entity, ComponentType)` | 延迟移除组件 |
| `destroyEntity(entity)` | 延迟销毁实体 |
| `setEntityActive(entity, active)` | 延迟设置实体激活状态 |
### 执行时机
命令缓冲区中的命令会在每帧的 `lateUpdate` 阶段之后自动执行。执行顺序与命令入队顺序一致。
```
场景更新流程:
1. onBegin()
2. process()
3. lateProcess()
4. onEnd()
5. flushCommandBuffers() <-- 命令在这里执行
```
### 使用场景
CommandBuffer 适用于以下场景:
1. **在迭代中销毁实体**:避免修改正在遍历的集合
2. **批量延迟操作**:将多个操作合并到帧末执行
3. **跨系统协调**:一个系统标记,另一个系统响应
```typescript
// 示例:敌人死亡系统
@ECSSystem('EnemyDeath')
class EnemyDeathSystem extends EntitySystem {
constructor() {
super(Matcher.all(Enemy, Health));
}
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health && health.current <= 0) {
// 播放死亡动画、掉落物品等
this.spawnLoot(entity);
// 延迟销毁,不影响当前迭代
this.commands.destroyEntity(entity);
}
}
}
private spawnLoot(entity: Entity): void {
// 掉落物品逻辑
}
}
```
### 注意事项
- 命令会跳过已销毁的实体(安全检查)
- 单个命令执行失败不会影响其他命令
- 命令按入队顺序执行
- 每次 `flush()` 后命令队列会清空
## 系统属性和方法
### 重要属性
@@ -420,6 +625,8 @@ class GameScene extends Scene {
### 系统更新顺序
系统的执行顺序由 `updateOrder` 属性决定,数值越小越先执行:
```typescript
@ECSSystem('Input')
class InputSystem extends EntitySystem {
@@ -446,6 +653,262 @@ class RenderSystem extends EntitySystem {
}
```
#### 稳定排序addOrder
当多个系统的 `updateOrder` 相同时,框架使用 `addOrder`(添加顺序)作为第二排序条件,确保排序结果稳定可预测:
```typescript
// 这两个系统 updateOrder 都是默认值 0
@ECSSystem('SystemA')
class SystemA extends EntitySystem { /* ... */ }
@ECSSystem('SystemB')
class SystemB extends EntitySystem { /* ... */ }
// 添加顺序决定了执行顺序
scene.addSystem(new SystemA()); // addOrder = 0先执行
scene.addSystem(new SystemB()); // addOrder = 1后执行
```
> **注意**`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
## 声明式系统调度
> **v2.4.0+**
除了使用 `updateOrder` 手动控制执行顺序外,框架还提供了声明式的系统调度机制,让你可以通过依赖关系来定义系统的执行顺序。
### 调度装饰器
```typescript
import { EntitySystem, ECSSystem, Stage, Before, After, InSet } from '@esengine/ecs-framework';
// 使用装饰器声明系统调度
@ECSSystem('Movement')
@Stage('update') // 在 update 阶段执行
@After('InputSystem') // 在 InputSystem 之后执行
@Before('RenderSystem') // 在 RenderSystem 之前执行
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
// 移动逻辑
}
}
// 使用系统集合进行分组
@ECSSystem('Physics')
@Stage('update')
@InSet('CoreSystems') // 属于 CoreSystems 集合
class PhysicsSystem extends EntitySystem {
// ...
}
@ECSSystem('Collision')
@Stage('update')
@After('set:CoreSystems') // 在 CoreSystems 集合的所有系统之后执行
class CollisionSystem extends EntitySystem {
// ...
}
```
### 系统执行阶段
框架定义了以下系统执行阶段,按顺序执行:
| 阶段 | 说明 | 典型用途 |
|------|------|----------|
| `startup` | 启动阶段 | 一次性初始化 |
| `preUpdate` | 更新前阶段 | 输入处理、状态准备 |
| `update` | 主更新阶段(默认) | 核心游戏逻辑 |
| `postUpdate` | 更新后阶段 | 物理、碰撞检测 |
| `cleanup` | 清理阶段 | 资源清理、状态重置 |
### Fluent API 配置
如果不想使用装饰器,也可以使用 Fluent API 在运行时配置调度:
```typescript
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
// 使用 Fluent API 配置调度
this.stage('update')
.after('InputSystem')
.before('RenderSystem')
.inSet('CoreSystems');
}
}
```
### 循环依赖检测
框架会自动检测循环依赖并抛出明确的错误:
```typescript
// 这会导致循环依赖错误
@ECSSystem('SystemA')
@Before('SystemB')
class SystemA extends EntitySystem { }
@ECSSystem('SystemB')
@Before('SystemA') // 错误A -> B -> A 形成循环
class SystemB extends EntitySystem { }
// 错误信息Cyclic dependency detected: SystemA -> SystemB -> SystemA
```
## 帧级变更检测
> **v2.4.0+**
框架提供了基于 epoch 的帧级变更检测机制,让系统可以只处理发生变化的实体,大幅提升性能。
### 核心概念
- **Epoch**:全局帧计数器,每帧递增
- **lastWriteEpoch**:组件最后被修改时的 epoch
- **变更检测**:通过比较 epoch 判断组件是否在指定时间点后发生变化
### 标记组件为已修改
修改组件数据后,需要标记组件为已变更。有两种方式:
**方式 1通过 Entity 辅助方法(推荐)**
```typescript
// 修改组件后通过 entity.markDirty() 标记
const pos = entity.getComponent(Position)!;
pos.x = 100;
pos.y = 200;
entity.markDirty(pos);
// 可以同时标记多个组件
const vel = entity.getComponent(Velocity)!;
vel.vx = 10;
entity.markDirty(pos, vel);
```
**方式 2在组件内部封装**
```typescript
class VelocityComponent extends Component {
private _vx: number = 0;
private _vy: number = 0;
// 提供修改方法,接收 epoch 参数
public setVelocity(vx: number, vy: number, epoch: number): void {
this._vx = vx;
this._vy = vy;
this.markDirty(epoch);
}
public get vx(): number { return this._vx; }
public get vy(): number { return this._vy; }
}
// 在系统中使用
const vel = entity.getComponent(VelocityComponent)!;
vel.setVelocity(10, 20, this.currentEpoch);
```
### 在系统中使用变更检测
EntitySystem 提供了多个变更检测辅助方法:
```typescript
@ECSSystem('Physics')
class PhysicsSystem extends EntitySystem {
constructor() {
super(Matcher.all(Position, Velocity));
}
protected process(entities: readonly Entity[]): void {
// 方式1使用 forEachChanged 只处理变更的实体
// 自动保存 epoch 检查点
this.forEachChanged(entities, [Velocity], (entity) => {
const pos = this.requireComponent(entity, Position);
const vel = this.requireComponent(entity, Velocity);
// 只有 Velocity 变化时才更新位置
pos.x += vel.vx * Time.deltaTime;
pos.y += vel.vy * Time.deltaTime;
});
}
}
@ECSSystem('Transform')
class TransformSystem extends EntitySystem {
constructor() {
super(Matcher.all(Transform, RigidBody));
}
protected process(entities: readonly Entity[]): void {
// 方式2使用 filterChanged 获取变更的实体列表
const changedEntities = this.filterChanged(entities, [RigidBody]);
for (const entity of changedEntities) {
// 处理物理状态变化的实体
this.updatePhysics(entity);
}
// 手动保存 epoch 检查点
this.saveEpoch();
}
protected updatePhysics(entity: Entity): void {
// 物理更新逻辑
}
}
```
### 变更检测 API 参考
| 方法 | 说明 |
|------|------|
| `forEachChanged(entities, [Types], callback)` | 遍历指定组件发生变更的实体,自动保存检查点 |
| `filterChanged(entities, [Types])` | 返回指定组件发生变更的实体数组 |
| `hasChanged(entity, [Types])` | 检查单个实体的指定组件是否发生变更 |
| `saveEpoch()` | 手动保存当前 epoch 作为检查点 |
| `lastProcessEpoch` | 获取上次保存的 epoch 检查点 |
| `currentEpoch` | 获取当前场景的 epoch |
### 使用场景
变更检测特别适合以下场景:
1. **脏标记优化**:只在数据变化时更新渲染
2. **物理同步**:只同步位置/速度发生变化的实体
3. **网络同步**:只发送变化的组件数据
4. **缓存失效**:只在依赖数据变化时重新计算
```typescript
@ECSSystem('NetworkSync')
class NetworkSyncSystem extends EntitySystem {
constructor() {
super(Matcher.all(NetworkComponent, Transform));
}
protected process(entities: readonly Entity[]): void {
// 只同步变化的实体,大幅减少网络流量
this.forEachChanged(entities, [Transform], (entity) => {
const transform = this.requireComponent(entity, Transform);
const network = this.requireComponent(entity, NetworkComponent);
this.sendTransformUpdate(network.id, transform);
});
}
private sendTransformUpdate(id: string, transform: Transform): void {
// 发送网络更新
}
}
```
## 复杂系统示例
### 碰撞检测系统
@@ -563,9 +1026,28 @@ class GameSystem extends EntitySystem {
}
```
### 2. 使用装饰器
### 2. 使用 @ECSSystem 装饰器
**必须使用 `@ECSSystem` 装饰器**
`@ECSSystem` 是系统类必须使用的装饰器,它为系统提供类型标识和元数据管理。
#### 为什么必须使用
| 功能 | 说明 |
|------|------|
| **类型识别** | 提供稳定的系统名称,代码混淆后仍能正确识别 |
| **调试支持** | 在性能监控、日志和调试工具中显示可读的系统名称 |
| **系统管理** | 通过名称查找和管理系统 |
| **序列化支持** | 场景序列化时可以记录系统配置 |
#### 基本语法
```typescript
@ECSSystem(systemName: string)
```
- `systemName`: 系统的名称,建议使用描述性的名称
#### 使用示例
```typescript
// ✅ 正确的用法
@@ -574,12 +1056,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
@@ -647,4 +1158,4 @@ class ResourceSystem extends EntitySystem {
}
```
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。
系统是 ECS 架构的逻辑处理核心,正确设计和使用系统能让你的游戏代码更加模块化、高效和易于维护。

View File

@@ -30,6 +30,64 @@ class GameSystem extends EntitySystem {
}
```
### 游戏暂停
框架提供两种暂停方式,适用于不同场景:
#### Core.paused推荐
`Core.paused` 是**真正的暂停**,设置后整个游戏循环停止:
```typescript
import { Core } from '@esengine/ecs-framework';
class PauseMenuSystem extends EntitySystem {
public pauseGame(): void {
// 真正暂停 - 所有系统停止执行
Core.paused = true;
console.log('游戏已暂停');
}
public resumeGame(): void {
// 恢复游戏
Core.paused = false;
console.log('游戏已恢复');
}
public togglePause(): void {
Core.paused = !Core.paused;
console.log(Core.paused ? '游戏已暂停' : '游戏已恢复');
}
}
```
#### Time.timeScale = 0
`Time.timeScale = 0` 只是让 `deltaTime` 变为 0**系统仍然在执行**
```typescript
class SlowMotionSystem extends EntitySystem {
public freezeTime(): void {
// 时间冻结 - 系统仍在执行,只是 deltaTime = 0
Time.timeScale = 0;
}
}
```
#### 两种方式对比
| 特性 | `Core.paused = true` | `Time.timeScale = 0` |
|------|---------------------|---------------------|
| 系统执行 | ❌ 完全停止 | ✅ 仍在执行 |
| CPU 开销 | 零 | 正常开销 |
| Time 更新 | ❌ 停止 | ✅ 继续deltaTime=0 |
| 定时器 | ❌ 停止 | ✅ 继续(但时间不走) |
| 适用场景 | 暂停菜单、游戏暂停 | 慢动作、时间冻结特效 |
**推荐**
- 暂停菜单、真正的游戏暂停 → 使用 `Core.paused = true`
- 慢动作、子弹时间等特效 → 使用 `Time.timeScale`
### 时间缩放
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
@@ -48,10 +106,10 @@ class TimeControlSystem extends EntitySystem {
console.log('快进模式启用');
}
public pauseGame(): void {
// 暂停游戏(时间静止
Time.timeScale = 0;
console.log('游戏暂停');
public enableBulletTime(): void {
// 子弹时间效果10%速度
Time.timeScale = 0.1;
console.log('子弹时间启用');
}
public resumeNormalSpeed(): void {

View File

@@ -145,6 +145,8 @@ interface WorkerSystemConfig {
entityDataSize?: number;
/** 最大实体数量用于预分配SharedArrayBuffer */
maxEntities?: number;
/** 预编译的Worker脚本路径用于微信小游戏等不支持动态脚本的平台 */
workerScriptPath?: string;
}
```
@@ -605,4 +607,166 @@ public getPerformanceMetrics(): WorkerPerformanceMetrics {
- SharedArrayBuffer优化
- 大量实体的并行处理
## 微信小游戏支持
微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。
### 微信小游戏 Worker 限制
| 特性 | 浏览器 | 微信小游戏 |
|------|--------|-----------|
| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 |
| Worker 数量 | 多个 | 最多 1 个 |
| 脚本来源 | 任意 | 必须是代码包内文件 |
| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 |
### 使用 Worker Generator CLI
#### 1. 安装工具
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. 配置 workerScriptPath
在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// 物理计算逻辑
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... 其他方法
}
```
#### 3. 生成 Worker 文件
运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件:
```bash
# 基本用法
npx esengine-worker-gen --src ./src --wechat
# 完整选项
npx esengine-worker-gen \
--src ./src \ # 源码目录
--wechat \ # 生成微信小游戏兼容代码
--mapping \ # 生成 worker-mapping.json
--verbose # 详细输出
```
CLI 工具会:
1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类
2. 读取每个类的 `workerScriptPath` 配置
3. 提取 `workerProcess` 方法体
4. 转换为 ES5 语法(微信小游戏兼容)
5. 生成到配置的路径
#### 4. 配置 game.json
在微信小游戏的 `game.json` 中配置 workers 目录:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. 项目结构
```
your-game/
├── game.js
├── game.json # 配置 "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # 自动生成
└── worker-mapping.json # 自动生成
```
### 临时禁用 Worker
如果需要临时禁用 Worker例如调试时有两种方式
#### 方式 1配置禁用
```typescript
constructor() {
super(matcher, {
enableWorker: false, // 禁用 Worker使用主线程处理
// ...
});
}
```
#### 方式 2平台适配器禁用
在自定义平台适配器中返回不支持 Worker
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // 返回 false 禁用 Worker
}
// ...
}
```
### 注意事项
1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件
2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量:
```typescript
// ✅ 正确:只使用参数
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// ❌ 错误:使用 this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Worker 中无法访问 this
return e;
});
}
```
3. **配置数据通过 `systemConfig` 传递**,而不是类属性
4. **开发者工具中的警告可以忽略**
- `getNetworkType:fail not support` - 微信开发者工具内部行为
- `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现
Worker系统为ECS框架提供了强大的并行计算能力让你能够充分利用现代多核处理器的性能为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。

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>

View File

@@ -192,6 +192,6 @@ export class AttackAction implements INodeExecutor {
## 获取帮助
- 提交 [Issue](https://github.com/esengine/ecs-framework/issues)
- 提交 [Issue](https://github.com/esengine/esengine/issues)
- 加入社区讨论
- 参考文档中的完整代码示例

View File

@@ -0,0 +1,507 @@
# 蓝图可视化脚本 (Blueprint)
`@esengine/blueprint` 提供了一个功能完整的可视化脚本系统,支持节点式编程、事件驱动和蓝图组合。
## 安装
```bash
npm install @esengine/blueprint
```
## 快速开始
```typescript
import {
createBlueprintSystem,
createBlueprintComponentData,
NodeRegistry,
RegisterNode
} from '@esengine/blueprint';
// 创建蓝图系统
const blueprintSystem = createBlueprintSystem(scene);
// 加载蓝图资产
const blueprint = await loadBlueprintAsset('player.bp');
// 创建蓝图组件数据
const componentData = createBlueprintComponentData();
componentData.blueprintAsset = blueprint;
// 在游戏循环中更新
function gameLoop(dt: number) {
blueprintSystem.process(entities, dt);
}
```
## 核心概念
### 蓝图资产结构
蓝图保存为 `.bp` 文件,包含以下结构:
```typescript
interface BlueprintAsset {
version: number; // 格式版本
type: 'blueprint'; // 资产类型
metadata: BlueprintMetadata; // 元数据
variables: BlueprintVariable[]; // 变量定义
nodes: BlueprintNode[]; // 节点实例
connections: BlueprintConnection[]; // 连接
}
```
### 节点类型
节点按功能分为以下类别:
| 类别 | 说明 | 颜色 |
|------|------|------|
| `event` | 事件节点(入口点) | 红色 |
| `flow` | 流程控制 | 灰色 |
| `entity` | 实体操作 | 蓝色 |
| `component` | 组件访问 | 青色 |
| `math` | 数学运算 | 绿色 |
| `logic` | 逻辑运算 | 红色 |
| `variable` | 变量访问 | 紫色 |
| `time` | 时间工具 | 青色 |
| `debug` | 调试工具 | 灰色 |
### 引脚类型
节点通过引脚连接:
```typescript
interface BlueprintPinDefinition {
name: string; // 引脚名称
type: PinDataType; // 数据类型
direction: 'input' | 'output';
isExec?: boolean; // 是否是执行引脚
defaultValue?: unknown;
}
// 支持的数据类型
type PinDataType =
| 'exec' // 执行流
| 'boolean' // 布尔值
| 'number' // 数字
| 'string' // 字符串
| 'vector2' // 2D 向量
| 'vector3' // 3D 向量
| 'entity' // 实体引用
| 'component' // 组件引用
| 'any'; // 任意类型
```
### 变量作用域
```typescript
type VariableScope =
| 'local' // 每次执行独立
| 'instance' // 每个实体独立
| 'global'; // 全局共享
```
## 虚拟机 API
### BlueprintVM
蓝图虚拟机负责执行蓝图图:
```typescript
import { BlueprintVM } from '@esengine/blueprint';
// 创建 VM
const vm = new BlueprintVM(blueprintAsset, entity, scene);
// 启动(触发 BeginPlay
vm.start();
// 每帧更新(触发 Tick
vm.tick(deltaTime);
// 停止(触发 EndPlay
vm.stop();
// 暂停/恢复
vm.pause();
vm.resume();
// 触发事件
vm.triggerEvent('EventCollision', { other: otherEntity });
vm.triggerCustomEvent('OnDamage', { amount: 50 });
// 调试模式
vm.debug = true;
```
### 执行上下文
```typescript
interface ExecutionContext {
blueprint: BlueprintAsset; // 蓝图资产
entity: Entity; // 当前实体
scene: IScene; // 当前场景
deltaTime: number; // 帧间隔时间
time: number; // 总运行时间
// 获取输入值
getInput<T>(nodeId: string, pinName: string): T;
// 设置输出值
setOutput(nodeId: string, pinName: string, value: unknown): void;
// 变量访问
getVariable<T>(name: string): T;
setVariable(name: string, value: unknown): void;
}
```
### 执行结果
```typescript
interface ExecutionResult {
outputs?: Record<string, unknown>; // 输出值
nextExec?: string | null; // 下一个执行引脚
delay?: number; // 延迟执行(毫秒)
yield?: boolean; // 暂停到下一帧
error?: string; // 错误信息
}
```
## 自定义节点
### 定义节点模板
```typescript
import { BlueprintNodeTemplate } from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'A custom node example',
keywords: ['custom', 'example'],
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
```
### 实现节点执行器
```typescript
import { INodeExecutor, RegisterNode } from '@esengine/blueprint';
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
// 获取输入
const value = context.getInput<number>(node.id, 'value');
// 执行逻辑
const result = value * 2;
// 返回结果
return {
outputs: { result },
nextExec: 'exec' // 继续执行
};
}
}
```
### 使用装饰器注册
```typescript
// 方式 1: 使用装饰器
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor { ... }
// 方式 2: 手动注册
NodeRegistry.instance.register(MyNodeTemplate, new MyNodeExecutor());
```
## 节点注册表
```typescript
import { NodeRegistry } from '@esengine/blueprint';
// 获取单例
const registry = NodeRegistry.instance;
// 获取所有模板
const allTemplates = registry.getAllTemplates();
// 按类别获取
const mathNodes = registry.getTemplatesByCategory('math');
// 搜索节点
const results = registry.searchTemplates('add');
// 检查是否存在
if (registry.has('MyCustomNode')) { ... }
```
## 内置节点
### 事件节点
| 节点 | 说明 |
|------|------|
| `EventBeginPlay` | 蓝图启动时触发 |
| `EventTick` | 每帧触发 |
| `EventEndPlay` | 蓝图停止时触发 |
| `EventCollision` | 碰撞时触发 |
| `EventInput` | 输入事件触发 |
| `EventTimer` | 定时器触发 |
| `EventMessage` | 自定义消息触发 |
### 时间节点
| 节点 | 说明 |
|------|------|
| `Delay` | 延迟执行 |
| `GetDeltaTime` | 获取帧间隔 |
| `GetTime` | 获取运行时间 |
### 数学节点
| 节点 | 说明 |
|------|------|
| `Add` | 加法 |
| `Subtract` | 减法 |
| `Multiply` | 乘法 |
| `Divide` | 除法 |
| `Abs` | 绝对值 |
| `Clamp` | 限制范围 |
| `Lerp` | 线性插值 |
| `Min` / `Max` | 最小/最大值 |
### 调试节点
| 节点 | 说明 |
|------|------|
| `Print` | 打印到控制台 |
## 蓝图组合
### 蓝图片段
将可复用的逻辑封装为片段:
```typescript
import { createFragment } from '@esengine/blueprint';
const healthFragment = createFragment('HealthSystem', {
inputs: [
{ name: 'damage', type: 'number', internalNodeId: 'input1', internalPinName: 'value' }
],
outputs: [
{ name: 'isDead', type: 'boolean', internalNodeId: 'output1', internalPinName: 'value' }
],
graph: {
nodes: [...],
connections: [...],
variables: [...]
}
});
```
### 组合蓝图
```typescript
import { createComposer, FragmentRegistry } from '@esengine/blueprint';
// 注册片段
FragmentRegistry.instance.register('health', healthFragment);
FragmentRegistry.instance.register('movement', movementFragment);
// 创建组合器
const composer = createComposer('PlayerBlueprint');
// 添加片段到槽位
composer.addFragment(healthFragment, 'slot1', { position: { x: 0, y: 0 } });
composer.addFragment(movementFragment, 'slot2', { position: { x: 400, y: 0 } });
// 连接槽位
composer.connect('slot1', 'onDeath', 'slot2', 'disable');
// 验证
const validation = composer.validate();
if (!validation.isValid) {
console.error(validation.errors);
}
// 编译成蓝图
const blueprint = composer.compile();
```
## 触发器系统
### 定义触发条件
```typescript
import { TriggerCondition, TriggerDispatcher } from '@esengine/blueprint';
const lowHealthCondition: TriggerCondition = {
type: 'comparison',
left: { type: 'variable', name: 'health' },
operator: '<',
right: { type: 'constant', value: 20 }
};
```
### 使用触发器分发器
```typescript
const dispatcher = new TriggerDispatcher();
// 注册触发器
dispatcher.register('lowHealth', lowHealthCondition, (context) => {
context.triggerEvent('OnLowHealth');
});
// 每帧评估
dispatcher.evaluate(context);
```
## 与 ECS 集成
### 使用蓝图系统
```typescript
import { createBlueprintSystem } from '@esengine/blueprint';
class GameScene {
private blueprintSystem: BlueprintSystem;
initialize() {
this.blueprintSystem = createBlueprintSystem(this.scene);
}
update(dt: number) {
// 处理所有带蓝图组件的实体
this.blueprintSystem.process(this.entities, dt);
}
}
```
### 触发蓝图事件
```typescript
import { triggerBlueprintEvent, triggerCustomBlueprintEvent } from '@esengine/blueprint';
// 触发内置事件
triggerBlueprintEvent(entity, 'Collision', { other: otherEntity });
// 触发自定义事件
triggerCustomBlueprintEvent(entity, 'OnPickup', { item: itemEntity });
```
## 实际示例
### 玩家控制蓝图
```typescript
// 定义输入处理节点
const InputMoveTemplate: BlueprintNodeTemplate = {
type: 'InputMove',
title: 'Get Movement Input',
category: 'input',
inputs: [],
outputs: [
{ name: 'direction', type: 'vector2', direction: 'output' }
],
isPure: true
};
@RegisterNode(InputMoveTemplate)
class InputMoveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const input = context.scene.services.get(InputServiceToken);
const direction = {
x: input.getAxis('horizontal'),
y: input.getAxis('vertical')
};
return { outputs: { direction } };
}
}
```
### 状态切换逻辑
```typescript
// 在蓝图中实现状态机逻辑
const stateBlueprint = createEmptyBlueprint('PlayerState');
// 添加状态变量
stateBlueprint.variables.push({
name: 'currentState',
type: 'string',
defaultValue: 'idle',
scope: 'instance'
});
// 在 Tick 事件中检查状态转换
// ... 通过节点连接实现
```
## 序列化
### 保存蓝图
```typescript
import { validateBlueprintAsset } from '@esengine/blueprint';
function saveBlueprint(blueprint: BlueprintAsset, path: string): void {
if (!validateBlueprintAsset(blueprint)) {
throw new Error('Invalid blueprint structure');
}
const json = JSON.stringify(blueprint, null, 2);
fs.writeFileSync(path, json);
}
```
### 加载蓝图
```typescript
async function loadBlueprint(path: string): Promise<BlueprintAsset> {
const json = await fs.readFile(path, 'utf-8');
const asset = JSON.parse(json);
if (!validateBlueprintAsset(asset)) {
throw new Error('Invalid blueprint file');
}
return asset;
}
```
## 最佳实践
1. **使用片段复用逻辑**
- 将通用逻辑封装为片段
- 通过组合器构建复杂蓝图
2. **合理使用变量作用域**
- `local`: 临时计算结果
- `instance`: 实体状态(如生命值)
- `global`: 游戏全局状态
3. **避免无限循环**
- VM 有每帧最大执行步数限制(默认 1000
- 使用 Delay 节点打断长执行链
4. **调试技巧**
- 启用 `vm.debug = true` 查看执行日志
- 使用 Print 节点输出中间值
5. **性能优化**
- 纯节点(`isPure: true`)的输出会被缓存
- 避免在 Tick 中执行重计算

337
docs/modules/fsm/index.md Normal file
View File

@@ -0,0 +1,337 @@
# 状态机 (FSM)
`@esengine/fsm` 提供了一个类型安全的有限状态机实现用于角色、AI 或任何需要状态管理的场景。
## 安装
```bash
npm install @esengine/fsm
```
## 快速开始
```typescript
import { createStateMachine } from '@esengine/fsm';
// 定义状态类型
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
// 创建状态机
const fsm = createStateMachine<PlayerState>('idle');
// 定义状态和回调
fsm.defineState('idle', {
onEnter: (ctx, from) => console.log(`${from} 进入 idle`),
onExit: (ctx, to) => console.log(`从 idle 退出到 ${to}`),
onUpdate: (ctx, dt) => { /* 每帧更新 */ }
});
fsm.defineState('walk', {
onEnter: () => console.log('开始行走')
});
// 手动切换状态
fsm.transition('walk');
console.log(fsm.current); // 'walk'
```
## 核心概念
### 状态配置
每个状态可以配置以下回调:
```typescript
interface StateConfig<TState, TContext> {
name: TState; // 状态名称
onEnter?: (context: TContext, from: TState | null) => void; // 进入回调
onExit?: (context: TContext, to: TState) => void; // 退出回调
onUpdate?: (context: TContext, deltaTime: number) => void; // 更新回调
tags?: string[]; // 状态标签
metadata?: Record<string, unknown>; // 元数据
}
```
### 转换条件
可以定义带条件的状态转换:
```typescript
interface Context {
isMoving: boolean;
isRunning: boolean;
isGrounded: boolean;
}
const fsm = createStateMachine<PlayerState, Context>('idle', {
context: { isMoving: false, isRunning: false, isGrounded: true }
});
// 定义转换条件
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving);
fsm.defineTransition('walk', 'run', (ctx) => ctx.isRunning);
fsm.defineTransition('walk', 'idle', (ctx) => !ctx.isMoving);
// 自动评估并执行满足条件的转换
fsm.evaluateTransitions();
```
### 转换优先级
当多个转换条件同时满足时,优先级高的先执行:
```typescript
// 优先级数字越大越优先
fsm.defineTransition('idle', 'attack', (ctx) => ctx.isAttacking, 10);
fsm.defineTransition('idle', 'walk', (ctx) => ctx.isMoving, 1);
// 如果同时满足,会先尝试 attack优先级 10
```
## API 参考
### createStateMachine
```typescript
function createStateMachine<TState extends string, TContext = unknown>(
initialState: TState,
options?: StateMachineOptions<TContext>
): IStateMachine<TState, TContext>
```
**参数:**
- `initialState` - 初始状态
- `options.context` - 上下文对象,在回调中可访问
- `options.maxHistorySize` - 最大历史记录数(默认 100
- `options.enableHistory` - 是否启用历史记录(默认 true
### 状态机属性
| 属性 | 类型 | 描述 |
|------|------|------|
| `current` | `TState` | 当前状态 |
| `previous` | `TState \| null` | 上一个状态 |
| `context` | `TContext` | 上下文对象 |
| `isTransitioning` | `boolean` | 是否正在转换中 |
| `currentStateDuration` | `number` | 当前状态持续时间(毫秒) |
### 状态机方法
#### 状态定义
```typescript
// 定义状态
fsm.defineState('idle', {
onEnter: (ctx, from) => {},
onExit: (ctx, to) => {},
onUpdate: (ctx, dt) => {}
});
// 检查状态是否存在
fsm.hasState('idle'); // true
// 获取状态配置
fsm.getStateConfig('idle');
// 获取所有状态
fsm.getStates(); // ['idle', 'walk', ...]
```
#### 转换操作
```typescript
// 定义转换
fsm.defineTransition('idle', 'walk', condition, priority);
// 移除转换
fsm.removeTransition('idle', 'walk');
// 获取从某状态出发的所有转换
fsm.getTransitionsFrom('idle');
// 检查是否可以转换
fsm.canTransition('walk'); // true/false
// 手动转换
fsm.transition('walk');
// 强制转换(忽略条件)
fsm.transition('walk', true);
// 自动评估转换条件
fsm.evaluateTransitions();
```
#### 生命周期
```typescript
// 更新状态机(调用当前状态的 onUpdate
fsm.update(deltaTime);
// 重置状态机
fsm.reset(); // 重置到当前状态
fsm.reset('idle'); // 重置到指定状态
```
#### 事件监听
```typescript
// 监听进入特定状态
const unsubscribe = fsm.onEnter('walk', (from) => {
console.log(`${from} 进入 walk`);
});
// 监听退出特定状态
fsm.onExit('walk', (to) => {
console.log(`从 walk 退出到 ${to}`);
});
// 监听任意状态变化
fsm.onChange((event) => {
console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);
});
// 取消订阅
unsubscribe();
```
#### 调试
```typescript
// 获取状态历史
const history = fsm.getHistory();
// [{ from: 'idle', to: 'walk', timestamp: 1234567890 }, ...]
// 清除历史
fsm.clearHistory();
// 获取调试信息
const info = fsm.getDebugInfo();
// { current, previous, duration, stateCount, transitionCount, historySize }
```
## 实际示例
### 角色状态机
```typescript
import { createStateMachine } from '@esengine/fsm';
type CharacterState = 'idle' | 'walk' | 'run' | 'jump' | 'fall' | 'attack';
interface CharacterContext {
velocity: { x: number; y: number };
isGrounded: boolean;
isAttacking: boolean;
speed: number;
}
const characterFSM = createStateMachine<CharacterState, CharacterContext>('idle', {
context: {
velocity: { x: 0, y: 0 },
isGrounded: true,
isAttacking: false,
speed: 0
}
});
// 定义状态
characterFSM.defineState('idle', {
onEnter: (ctx) => {
ctx.speed = 0;
},
onUpdate: (ctx, dt) => {
// 播放待机动画
}
});
characterFSM.defineState('walk', {
onEnter: (ctx) => {
ctx.speed = 100;
}
});
characterFSM.defineState('run', {
onEnter: (ctx) => {
ctx.speed = 200;
}
});
characterFSM.defineState('jump', {
onEnter: (ctx) => {
ctx.velocity.y = -300;
ctx.isGrounded = false;
}
});
// 定义转换
characterFSM.defineTransition('idle', 'walk', (ctx) => Math.abs(ctx.velocity.x) > 0);
characterFSM.defineTransition('walk', 'idle', (ctx) => ctx.velocity.x === 0);
characterFSM.defineTransition('walk', 'run', (ctx) => Math.abs(ctx.velocity.x) > 150);
characterFSM.defineTransition('run', 'walk', (ctx) => Math.abs(ctx.velocity.x) <= 150);
// 跳跃有最高优先级
characterFSM.defineTransition('idle', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('walk', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('run', 'jump', (ctx) => !ctx.isGrounded, 10);
characterFSM.defineTransition('jump', 'fall', (ctx) => ctx.velocity.y > 0);
characterFSM.defineTransition('fall', 'idle', (ctx) => ctx.isGrounded);
// 游戏循环中使用
function gameUpdate(dt: number) {
// 更新上下文
characterFSM.context.velocity.x = getInputVelocity();
characterFSM.context.isGrounded = checkGrounded();
// 评估状态转换
characterFSM.evaluateTransitions();
// 更新当前状态
characterFSM.update(dt);
}
```
### 与 ECS 集成
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createStateMachine, type IStateMachine } from '@esengine/fsm';
// 状态机组件
class FSMComponent extends Component {
fsm: IStateMachine<string>;
constructor(initialState: string) {
super();
this.fsm = createStateMachine(initialState);
}
}
// 状态机系统
class FSMSystem extends EntitySystem {
constructor() {
super(Matcher.all(FSMComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const fsmComp = entity.getComponent(FSMComponent);
fsmComp.fsm.evaluateTransitions();
fsmComp.fsm.update(dt);
}
}
```
## 蓝图节点
FSM 模块提供了可视化脚本支持的蓝图节点:
- `GetCurrentState` - 获取当前状态
- `TransitionTo` - 转换到指定状态
- `CanTransition` - 检查是否可以转换
- `IsInState` - 检查是否在指定状态
- `WasInState` - 检查是否曾在指定状态
- `GetStateDuration` - 获取状态持续时间
- `EvaluateTransitions` - 评估转换条件
- `ResetStateMachine` - 重置状态机

54
docs/modules/index.md Normal file
View File

@@ -0,0 +1,54 @@
# 功能模块
ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中。
## 模块列表
### AI 模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [行为树](/modules/behavior-tree/) | `@esengine/behavior-tree` | AI 行为树系统,支持可视化编辑 |
| [状态机](/modules/fsm/) | `@esengine/fsm` | 有限状态机,用于角色/AI 状态管理 |
### 游戏逻辑
| 模块 | 包名 | 描述 |
|------|------|------|
| [定时器](/modules/timer/) | `@esengine/timer` | 定时器和冷却系统 |
| [空间索引](/modules/spatial/) | `@esengine/spatial` | 空间查询、AOI 兴趣区域管理 |
| [寻路系统](/modules/pathfinding/) | `@esengine/pathfinding` | A* 寻路、NavMesh 导航网格 |
### 工具模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [可视化脚本](/modules/blueprint/) | `@esengine/blueprint` | 蓝图可视化脚本系统 |
| [程序化生成](/modules/procgen/) | `@esengine/procgen` | 噪声函数、随机工具 |
### 网络模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
## 安装
所有模块都可以独立安装:
```bash
# 安装单个模块
npm install @esengine/behavior-tree
# 或使用 CLI 添加到现有项目
npx @esengine/cli add behavior-tree
```
## 平台兼容性
所有功能模块都是纯 TypeScript 实现,兼容:
- Cocos Creator 3.x
- Laya 3.x
- Node.js
- 浏览器

View File

@@ -0,0 +1,727 @@
# 网络同步系统 (Network)
`@esengine/network` 提供基于 TSRPC 的客户端-服务器网络同步解决方案,用于多人游戏的实体同步、输入处理和状态插值。
## 概述
网络模块由三个包组成:
| 包名 | 描述 |
|------|------|
| `@esengine/network` | 客户端 ECS 插件 |
| `@esengine/network-protocols` | 共享协议定义 |
| `@esengine/network-server` | 服务器端实现 |
## 安装
```bash
# 客户端
npm install @esengine/network
# 服务器端
npm install @esengine/network-server
```
## 使用 CLI 快速创建服务端
推荐使用 ESEngine CLI 快速创建完整的游戏服务端项目:
```bash
# 创建项目目录
mkdir my-game-server && cd my-game-server
npm init -y
# 使用 CLI 初始化 Node.js 服务端
npx @esengine/cli init -p nodejs
```
CLI 会自动生成以下项目结构:
```
my-game-server/
├── src/
│ ├── index.ts # 入口文件
│ ├── server/
│ │ └── GameServer.ts # 网络服务器配置
│ └── game/
│ ├── Game.ts # ECS 游戏主类
│ ├── scenes/
│ │ └── MainScene.ts # 主场景
│ ├── components/ # ECS 组件
│ │ ├── PositionComponent.ts
│ │ └── VelocityComponent.ts
│ └── systems/ # ECS 系统
│ └── MovementSystem.ts
├── tsconfig.json
├── package.json
└── README.md
```
启动服务端:
```bash
# 开发模式(热重载)
npm run dev
# 生产模式
npm run start
```
## 快速开始
### 客户端
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
NetworkTransform
} from '@esengine/network';
// 定义游戏场景
class GameScene extends Scene {
initialize(): void {
this.name = 'Game';
// 网络系统由 NetworkPlugin 自动添加
}
}
// 初始化 Core
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
// 安装网络插件
const networkPlugin = new NetworkPlugin();
await Core.installPlugin(networkPlugin);
// 注册预制体工厂
networkPlugin.registerPrefab('player', (scene, spawn) => {
const entity = scene.createEntity(`player_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
entity.addComponent(new NetworkTransform());
return entity;
});
// 连接服务器
const success = await networkPlugin.connect('ws://localhost:3000', 'PlayerName');
if (success) {
console.log('Connected!');
}
// 游戏循环
function gameLoop(dt: number) {
Core.update(dt);
}
// 断开连接
await networkPlugin.disconnect();
```
### 服务器端
使用 CLI 创建服务端项目后,默认生成的代码已经配置好了 GameServer
```typescript
import { GameServer } from '@esengine/network-server';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16,
tickRate: 20
}
});
await server.start();
console.log('Server started on ws://localhost:3000');
```
## 核心概念
### 架构
```
客户端 服务器
┌────────────────┐ ┌────────────────┐
│ NetworkPlugin │◄──── WS ────► │ GameServer │
│ ├─ Service │ │ ├─ Room │
│ ├─ SyncSystem │ │ └─ Players │
│ ├─ SpawnSystem │ └────────────────┘
│ └─ InputSystem │
└────────────────┘
```
### 组件
#### NetworkIdentity
网络标识组件,每个网络同步的实体必须拥有:
```typescript
class NetworkIdentity extends Component {
netId: number; // 网络唯一 ID
ownerId: number; // 所有者客户端 ID
bIsLocalPlayer: boolean; // 是否为本地玩家
bHasAuthority: boolean; // 是否有权限控制
}
```
#### NetworkTransform
网络变换组件,用于位置和旋转同步:
```typescript
class NetworkTransform extends Component {
position: { x: number; y: number };
rotation: number;
velocity: { x: number; y: number };
}
```
### 系统
#### NetworkSyncSystem
处理服务器状态同步和插值:
- 接收服务器状态快照
- 将状态存入快照缓冲区
- 对远程实体进行插值平滑
#### NetworkSpawnSystem
处理实体的网络生成和销毁:
- 监听 Spawn/Despawn 消息
- 使用注册的预制体工厂创建实体
- 管理网络实体的生命周期
#### NetworkInputSystem
处理本地玩家输入的网络发送:
- 收集本地玩家输入
- 发送输入到服务器
- 支持移动和动作输入
## API 参考
### NetworkPlugin
```typescript
class NetworkPlugin {
constructor(config: INetworkPluginConfig);
// 安装插件
install(services: ServiceContainer): void;
// 连接服务器
connect(playerName: string, roomId?: string): Promise<void>;
// 断开连接
disconnect(): void;
// 注册预制体工厂
registerPrefab(prefab: string, factory: PrefabFactory): void;
// 属性
readonly localPlayerId: number | null;
readonly isConnected: boolean;
}
```
**配置选项:**
| 属性 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `serverUrl` | `string` | 是 | WebSocket 服务器地址 |
### NetworkService
网络服务,管理 WebSocket 连接:
```typescript
class NetworkService {
// 连接状态
readonly state: ENetworkState;
readonly isConnected: boolean;
readonly clientId: number | null;
readonly roomId: string | null;
// 连接控制
connect(serverUrl: string): Promise<void>;
disconnect(): void;
// 加入房间
join(playerName: string, roomId?: string): Promise<ResJoin>;
// 发送输入
sendInput(input: IPlayerInput): void;
// 事件回调
setCallbacks(callbacks: Partial<INetworkCallbacks>): void;
}
```
**网络状态枚举:**
```typescript
enum ENetworkState {
Disconnected = 'disconnected',
Connecting = 'connecting',
Connected = 'connected',
Joining = 'joining',
Joined = 'joined'
}
```
**回调接口:**
```typescript
interface INetworkCallbacks {
onConnected?: () => void;
onDisconnected?: () => void;
onJoined?: (clientId: number, roomId: string) => void;
onSync?: (msg: MsgSync) => void;
onSpawn?: (msg: MsgSpawn) => void;
onDespawn?: (msg: MsgDespawn) => void;
}
```
### 预制体工厂
```typescript
type PrefabFactory = (scene: Scene, spawn: MsgSpawn) => Entity;
```
注册预制体工厂用于网络实体的创建:
```typescript
networkPlugin.registerPrefab('enemy', (scene, spawn) => {
const entity = scene.createEntity(`enemy_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
entity.addComponent(new NetworkTransform());
entity.addComponent(new EnemyComponent());
return entity;
});
```
### 输入系统
#### NetworkInputSystem
```typescript
class NetworkInputSystem extends EntitySystem {
// 添加移动输入
addMoveInput(x: number, y: number): void;
// 添加动作输入
addActionInput(action: string): void;
// 清除输入
clearInput(): void;
}
```
使用示例:
```typescript
// 通过 NetworkPlugin 发送输入(推荐)
networkPlugin.sendMoveInput(0, 1); // 移动
networkPlugin.sendActionInput('jump'); // 动作
// 或直接使用 inputSystem
const inputSystem = networkPlugin.inputSystem;
if (keyboard.isPressed('W')) {
inputSystem.addMoveInput(0, 1);
}
if (keyboard.isPressed('Space')) {
inputSystem.addActionInput('jump');
}
```
## 状态同步
### 快照缓冲区
用于存储服务器状态快照并进行插值:
```typescript
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
const buffer = createSnapshotBuffer<IStateSnapshot>({
maxSnapshots: 30, // 最大快照数
interpolationDelay: 100 // 插值延迟 (ms)
});
// 添加快照
buffer.addSnapshot({
time: serverTime,
entities: states
});
// 获取插值状态
const interpolated = buffer.getInterpolatedState(clientTime);
```
### 变换插值器
#### 线性插值器
```typescript
import { createTransformInterpolator } from '@esengine/network';
const interpolator = createTransformInterpolator();
// 添加状态
interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
// 获取插值结果
const state = interpolator.getInterpolatedState(currentTime);
```
#### Hermite 插值器
使用 Hermite 样条实现更平滑的插值:
```typescript
import { createHermiteTransformInterpolator } from '@esengine/network';
const interpolator = createHermiteTransformInterpolator({
bufferSize: 10
});
// 添加带速度的状态
interpolator.addState(time, {
x: 100,
y: 200,
rotation: 0,
vx: 5,
vy: 0
});
// 获取平滑的插值结果
const state = interpolator.getInterpolatedState(currentTime);
```
### 客户端预测
实现客户端预测和服务器校正:
```typescript
import { createClientPrediction } from '@esengine/network';
const prediction = createClientPrediction({
maxPredictedInputs: 60,
reconciliationThreshold: 0.1
});
// 预测输入
const seq = prediction.predict(inputState, currentState, (state, input) => {
// 应用输入到状态
return applyInput(state, input);
});
// 服务器校正
const corrected = prediction.reconcile(
serverState,
serverSeq,
(state, input) => applyInput(state, input)
);
```
## 服务器端
### GameServer
```typescript
import { GameServer } from '@esengine/network-server';
const server = new GameServer({
port: 3000,
roomConfig: {
maxPlayers: 16, // 房间最大玩家数
tickRate: 20 // 同步频率 (Hz)
}
});
// 启动服务器
await server.start();
// 获取房间
const room = server.getOrCreateRoom('room-id');
// 停止服务器
await server.stop();
```
### Room
```typescript
class Room {
readonly id: string;
readonly playerCount: number;
readonly isFull: boolean;
// 添加玩家
addPlayer(name: string, connection: Connection): IPlayer | null;
// 移除玩家
removePlayer(clientId: number): void;
// 获取玩家
getPlayer(clientId: number): IPlayer | undefined;
// 处理输入
handleInput(clientId: number, input: IPlayerInput): void;
// 销毁房间
destroy(): void;
}
```
**玩家接口:**
```typescript
interface IPlayer {
clientId: number; // 客户端 ID
name: string; // 玩家名称
connection: Connection; // 连接对象
netId: number; // 网络实体 ID
}
```
## 协议类型
### 消息类型
```typescript
// 状态同步消息
interface MsgSync {
time: number;
entities: IEntityState[];
}
// 实体状态
interface IEntityState {
netId: number;
pos?: Vec2;
rot?: number;
}
// 生成消息
interface MsgSpawn {
netId: number;
ownerId: number;
prefab: string;
pos: Vec2;
rot: number;
}
// 销毁消息
interface MsgDespawn {
netId: number;
}
// 输入消息
interface MsgInput {
input: IPlayerInput;
}
// 玩家输入
interface IPlayerInput {
seq?: number;
moveDir?: Vec2;
actions?: string[];
}
```
### API 类型
```typescript
// 加入请求
interface ReqJoin {
playerName: string;
roomId?: string;
}
// 加入响应
interface ResJoin {
clientId: number;
roomId: string;
playerCount: number;
}
```
## 蓝图节点
网络模块提供了可视化脚本支持的蓝图节点:
- `IsLocalPlayer` - 检查实体是否为本地玩家
- `IsServer` - 检查是否运行在服务器端
- `HasAuthority` - 检查是否有权限控制实体
- `GetNetworkId` - 获取实体的网络 ID
- `GetLocalPlayerId` - 获取本地玩家 ID
## 服务令牌
用于依赖注入:
```typescript
import {
NetworkServiceToken,
NetworkSyncSystemToken,
NetworkSpawnSystemToken,
NetworkInputSystemToken
} from '@esengine/network';
// 获取服务
const networkService = services.get(NetworkServiceToken);
```
## 实际示例
### 完整的多人游戏客户端
```typescript
import { Core, Scene, EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import {
NetworkPlugin,
NetworkIdentity,
NetworkTransform
} from '@esengine/network';
// 定义游戏场景
class GameScene extends Scene {
initialize(): void {
this.name = 'MultiplayerGame';
// 网络系统由 NetworkPlugin 自动添加
// 添加自定义系统
this.addSystem(new LocalInputHandler());
}
}
// 初始化
async function initGame() {
Core.create({ debug: false });
const scene = new GameScene();
Core.setScene(scene);
// 安装网络插件
const networkPlugin = new NetworkPlugin();
await Core.installPlugin(networkPlugin);
// 注册玩家预制体
networkPlugin.registerPrefab('player', (scene, spawn) => {
const entity = scene.createEntity(`player_${spawn.netId}`);
const identity = entity.addComponent(new NetworkIdentity());
identity.netId = spawn.netId;
identity.ownerId = spawn.ownerId;
identity.isLocalPlayer = spawn.ownerId === networkPlugin.networkService.localClientId;
entity.addComponent(new NetworkTransform());
// 如果是本地玩家,添加输入标记
if (identity.isLocalPlayer) {
entity.addComponent(new LocalInputComponent());
}
return entity;
});
// 连接服务器
const success = await networkPlugin.connect('ws://localhost:3000', 'Player1');
if (success) {
console.log('已连接!');
} else {
console.error('连接失败');
}
return networkPlugin;
}
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
}
initGame();
```
### 处理输入
```typescript
class LocalInputHandler extends EntitySystem {
private _networkPlugin: NetworkPlugin | null = null;
constructor() {
super(Matcher.empty().all(NetworkIdentity, LocalInputComponent));
}
protected onAddedToScene(): void {
// 获取 NetworkPlugin 引用
this._networkPlugin = Core.getPlugin(NetworkPlugin);
}
protected processEntity(entity: Entity, dt: number): void {
if (!this._networkPlugin) return;
const identity = entity.getComponent(NetworkIdentity)!;
if (!identity.isLocalPlayer) return;
// 读取键盘输入
let moveX = 0;
let moveY = 0;
if (keyboard.isPressed('A')) moveX -= 1;
if (keyboard.isPressed('D')) moveX += 1;
if (keyboard.isPressed('W')) moveY += 1;
if (keyboard.isPressed('S')) moveY -= 1;
if (moveX !== 0 || moveY !== 0) {
this._networkPlugin.sendMoveInput(moveX, moveY);
}
if (keyboard.isJustPressed('Space')) {
this._networkPlugin.sendActionInput('jump');
}
}
}
```
## 最佳实践
1. **合理设置同步频率**:根据游戏类型选择合适的 `tickRate`,动作游戏通常需要 20-60 Hz
2. **使用插值延迟**:设置适当的 `interpolationDelay` 来平衡延迟和平滑度
3. **客户端预测**:对于本地玩家使用客户端预测减少输入延迟
4. **预制体管理**:为每种网络实体类型注册对应的预制体工厂
5. **权限检查**:使用 `bHasAuthority` 检查是否有权限修改实体
6. **连接状态**:监听连接状态变化,处理断线重连
```typescript
networkService.setCallbacks({
onConnected: () => console.log('已连接'),
onDisconnected: () => {
console.log('已断开');
// 处理重连逻辑
}
});
```

View File

@@ -0,0 +1,502 @@
# 寻路系统 (Pathfinding)
`@esengine/pathfinding` 提供了完整的 2D 寻路解决方案,包括 A* 算法、网格地图、导航网格和路径平滑。
## 安装
```bash
npm install @esengine/pathfinding
```
## 快速开始
### 网格地图寻路
```typescript
import { createGridMap, createAStarPathfinder } from '@esengine/pathfinding';
// 创建 20x20 的网格地图
const grid = createGridMap(20, 20);
// 设置障碍物
grid.setWalkable(5, 5, false);
grid.setWalkable(5, 6, false);
grid.setWalkable(5, 7, false);
// 创建寻路器
const pathfinder = createAStarPathfinder(grid);
// 查找路径
const result = pathfinder.findPath(0, 0, 15, 15);
if (result.found) {
console.log('找到路径!');
console.log('路径点:', result.path);
console.log('总代价:', result.cost);
console.log('搜索节点数:', result.nodesSearched);
}
```
### 导航网格寻路
```typescript
import { createNavMesh } from '@esengine/pathfinding';
// 创建导航网格
const navmesh = createNavMesh();
// 添加多边形区域
navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// 自动建立连接
navmesh.build();
// 寻路
const result = navmesh.findPath(1, 1, 18, 8);
```
## 核心概念
### IPoint - 坐标点
```typescript
interface IPoint {
readonly x: number;
readonly y: number;
}
```
### IPathResult - 寻路结果
```typescript
interface IPathResult {
readonly found: boolean; // 是否找到路径
readonly path: readonly IPoint[]; // 路径点列表
readonly cost: number; // 路径总代价
readonly nodesSearched: number; // 搜索的节点数
}
```
### IPathfindingOptions - 寻路配置
```typescript
interface IPathfindingOptions {
maxNodes?: number; // 最大搜索节点数(默认 10000
heuristicWeight?: number; // 启发式权重(>1 更快但可能非最优)
allowDiagonal?: boolean; // 是否允许对角移动(默认 true
avoidCorners?: boolean; // 是否避免穿角(默认 true
}
```
## 启发式函数
模块提供了四种启发式函数:
| 函数 | 适用场景 | 说明 |
|------|----------|------|
| `manhattanDistance` | 4方向移动 | 曼哈顿距离,只考虑水平/垂直 |
| `euclideanDistance` | 任意方向 | 欧几里得距离,直线距离 |
| `chebyshevDistance` | 8方向移动 | 切比雪夫距离,对角线代价为 1 |
| `octileDistance` | 8方向移动 | 八角距离,对角线代价为 √2默认 |
```typescript
import { manhattanDistance, octileDistance } from '@esengine/pathfinding';
// 自定义启发式
const grid = createGridMap(20, 20, {
heuristic: manhattanDistance // 使用曼哈顿距离
});
```
## 网格地图 API
### createGridMap
```typescript
function createGridMap(
width: number,
height: number,
options?: IGridMapOptions
): GridMap
```
**配置选项:**
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `allowDiagonal` | `boolean` | `true` | 允许对角移动 |
| `diagonalCost` | `number` | `√2` | 对角移动代价 |
| `avoidCorners` | `boolean` | `true` | 避免穿角 |
| `heuristic` | `HeuristicFunction` | `octileDistance` | 启发式函数 |
### 地图操作
```typescript
// 检查/设置可通行性
grid.isWalkable(x, y);
grid.setWalkable(x, y, false);
// 设置移动代价(如沼泽、沙地)
grid.setCost(x, y, 2); // 代价为 2默认 1
// 设置矩形区域
grid.setRectWalkable(0, 0, 5, 5, false);
// 从数组加载0=可通行非0=障碍)
grid.loadFromArray([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 0, 0, 0]
]);
// 从字符串加载(.=可通行,#=障碍)
grid.loadFromString(`
.....
.#.#.
.#...
`);
// 导出为字符串
console.log(grid.toString());
// 重置所有节点为可通行
grid.reset();
```
### 方向常量
```typescript
import { DIRECTIONS_4, DIRECTIONS_8 } from '@esengine/pathfinding';
// 4方向上下左右
DIRECTIONS_4 // [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, ...]
// 8方向含对角线
DIRECTIONS_8 // [{ dx: 0, dy: -1 }, { dx: 1, dy: -1 }, ...]
```
## A* 寻路器 API
### createAStarPathfinder
```typescript
function createAStarPathfinder(map: IPathfindingMap): AStarPathfinder
```
### findPath
```typescript
const result = pathfinder.findPath(
startX, startY,
endX, endY,
{
maxNodes: 5000, // 限制搜索节点数
heuristicWeight: 1.5 // 加速但可能非最优
}
);
```
### 重用寻路器
```typescript
// 寻路器可重用,内部会自动清理状态
pathfinder.findPath(0, 0, 10, 10);
pathfinder.findPath(5, 5, 15, 15);
// 手动清理(可选)
pathfinder.clear();
```
## 导航网格 API
### createNavMesh
```typescript
function createNavMesh(): NavMesh
```
### 构建导航网格
```typescript
const navmesh = createNavMesh();
// 添加凸多边形
const id1 = navmesh.addPolygon([
{ x: 0, y: 0 }, { x: 10, y: 0 },
{ x: 10, y: 10 }, { x: 0, y: 10 }
]);
const id2 = navmesh.addPolygon([
{ x: 10, y: 0 }, { x: 20, y: 0 },
{ x: 20, y: 10 }, { x: 10, y: 10 }
]);
// 方式1自动检测共享边并建立连接
navmesh.build();
// 方式2手动设置连接
navmesh.setConnection(id1, id2, {
left: { x: 10, y: 0 },
right: { x: 10, y: 10 }
});
```
### 查询和寻路
```typescript
// 查找包含点的多边形
const polygon = navmesh.findPolygonAt(5, 5);
// 检查位置是否可通行
navmesh.isWalkable(5, 5);
// 寻路(内部使用漏斗算法优化路径)
const result = navmesh.findPath(1, 1, 18, 8);
```
## 路径平滑 API
### 视线简化
移除不必要的中间点:
```typescript
import { createLineOfSightSmoother } from '@esengine/pathfinding';
const smoother = createLineOfSightSmoother();
const smoothedPath = smoother.smooth(result.path, grid);
// 原路径: [(0,0), (1,1), (2,2), (3,3), (4,4)]
// 简化后: [(0,0), (4,4)]
```
### 曲线平滑
使用 Catmull-Rom 样条曲线:
```typescript
import { createCatmullRomSmoother } from '@esengine/pathfinding';
const smoother = createCatmullRomSmoother(
5, // segments - 每段插值点数
0.5 // tension - 张力 (0-1)
);
const curvedPath = smoother.smooth(result.path, grid);
```
### 组合平滑
先简化再曲线平滑:
```typescript
import { createCombinedSmoother } from '@esengine/pathfinding';
const smoother = createCombinedSmoother(5, 0.5);
const finalPath = smoother.smooth(result.path, grid);
```
### 视线检测函数
```typescript
import { bresenhamLineOfSight, raycastLineOfSight } from '@esengine/pathfinding';
// Bresenham 算法(快速,网格对齐)
const hasLOS = bresenhamLineOfSight(x1, y1, x2, y2, grid);
// 射线投射(精确,支持浮点坐标)
const hasLOS = raycastLineOfSight(x1, y1, x2, y2, grid, 0.5);
```
## 实际示例
### 游戏角色移动
```typescript
class MovementSystem {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private smoother: CombinedSmoother;
constructor(width: number, height: number) {
this.grid = createGridMap(width, height);
this.pathfinder = createAStarPathfinder(this.grid);
this.smoother = createCombinedSmoother();
}
findPath(from: IPoint, to: IPoint): IPoint[] | null {
const result = this.pathfinder.findPath(
from.x, from.y,
to.x, to.y
);
if (!result.found) {
return null;
}
// 平滑路径
return this.smoother.smooth(result.path, this.grid);
}
setObstacle(x: number, y: number): void {
this.grid.setWalkable(x, y, false);
}
setTerrain(x: number, y: number, cost: number): void {
this.grid.setCost(x, y, cost);
}
}
```
### 动态障碍物
```typescript
class DynamicPathfinding {
private grid: GridMap;
private pathfinder: AStarPathfinder;
private dynamicObstacles: Set<string> = new Set();
addDynamicObstacle(x: number, y: number): void {
const key = `${x},${y}`;
if (!this.dynamicObstacles.has(key)) {
this.dynamicObstacles.add(key);
this.grid.setWalkable(x, y, false);
}
}
removeDynamicObstacle(x: number, y: number): void {
const key = `${x},${y}`;
if (this.dynamicObstacles.has(key)) {
this.dynamicObstacles.delete(key);
this.grid.setWalkable(x, y, true);
}
}
findPath(from: IPoint, to: IPoint): IPathResult {
return this.pathfinder.findPath(from.x, from.y, to.x, to.y);
}
}
```
### 不同地形代价
```typescript
// 设置不同地形的移动代价
const grid = createGridMap(50, 50);
// 普通地面 - 代价 1默认
// 沙地 - 代价 2
for (let y = 10; y < 20; y++) {
for (let x = 0; x < 50; x++) {
grid.setCost(x, y, 2);
}
}
// 沼泽 - 代价 4
for (let y = 30; y < 35; y++) {
for (let x = 20; x < 30; x++) {
grid.setCost(x, y, 4);
}
}
// 寻路时会自动考虑地形代价
const result = pathfinder.findPath(0, 0, 49, 49);
```
### 分层寻路
对于大型地图,使用层级化寻路:
```typescript
class HierarchicalPathfinding {
private coarseGrid: GridMap; // 粗粒度网格
private fineGrid: GridMap; // 细粒度网格
private coarsePathfinder: AStarPathfinder;
private finePathfinder: AStarPathfinder;
private cellSize = 10;
findPath(from: IPoint, to: IPoint): IPoint[] {
// 1. 在粗粒度网格上寻路
const coarseFrom = this.toCoarse(from);
const coarseTo = this.toCoarse(to);
const coarseResult = this.coarsePathfinder.findPath(
coarseFrom.x, coarseFrom.y,
coarseTo.x, coarseTo.y
);
if (!coarseResult.found) {
return [];
}
// 2. 在每个粗粒度单元内进行细粒度寻路
const finePath: IPoint[] = [];
// ... 详细实现略
return finePath;
}
private toCoarse(p: IPoint): IPoint {
return {
x: Math.floor(p.x / this.cellSize),
y: Math.floor(p.y / this.cellSize)
};
}
}
```
## 蓝图节点
Pathfinding 模块提供了可视化脚本支持的蓝图节点:
- `FindPath` - 查找路径
- `FindPathSmooth` - 查找并平滑路径
- `IsWalkable` - 检查位置是否可通行
- `GetPathLength` - 获取路径点数
- `GetPathDistance` - 获取路径总距离
- `GetPathPoint` - 获取路径上的指定点
- `MoveAlongPath` - 沿路径移动
- `HasLineOfSight` - 检查视线
## 性能优化
1. **限制搜索范围**
```typescript
pathfinder.findPath(x1, y1, x2, y2, { maxNodes: 1000 });
```
2. **使用启发式权重**
```typescript
// 权重 > 1 会更快但可能不是最优路径
pathfinder.findPath(x1, y1, x2, y2, { heuristicWeight: 1.5 });
```
3. **复用寻路器实例**
```typescript
// 创建一次,多次使用
const pathfinder = createAStarPathfinder(grid);
```
4. **使用导航网格**
- 对于复杂地形NavMesh 比网格寻路更高效
- 多边形数量远少于网格单元格数量
5. **选择合适的启发式**
- 4方向移动用 `manhattanDistance`
- 8方向移动用 `octileDistance`(默认)
## 网格 vs 导航网格
| 特性 | GridMap | NavMesh |
|------|---------|---------|
| 适用场景 | 规则瓦片地图 | 复杂多边形地形 |
| 内存占用 | 较高 (width × height) | 较低 (多边形数) |
| 精度 | 网格对齐 | 连续坐标 |
| 动态修改 | 容易 | 需要重建 |
| 设置复杂度 | 简单 | 较复杂 |

View File

@@ -0,0 +1,557 @@
# 程序化生成 (Procgen)
`@esengine/procgen` 提供了程序化内容生成的核心工具,包括噪声函数、种子随机数和各种随机工具。
## 安装
```bash
npm install @esengine/procgen
```
## 快速开始
### 噪声生成
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
// 创建 Perlin 噪声
const perlin = createPerlinNoise(12345); // 种子
// 采样 2D 噪声
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]
// 使用 FBM 获得更自然的效果
const fbm = createFBM(perlin, {
octaves: 6,
persistence: 0.5
});
const height = fbm.noise2D(x * 0.01, y * 0.01);
```
### 种子随机数
```typescript
import { createSeededRandom } from '@esengine/procgen';
// 创建确定性随机数生成器
const rng = createSeededRandom(42);
// 相同种子总是产生相同序列
console.log(rng.next()); // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3)); // 30% true
```
### 加权随机
```typescript
import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
// 创建加权选择器
const loot = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
// 随机选择
const drop = loot.pick(rng);
console.log(drop); // 大概率是 'common'
```
## 噪声函数
### Perlin 噪声
经典的梯度噪声,输出范围 [-1, 1]
```typescript
import { createPerlinNoise } from '@esengine/procgen';
const perlin = createPerlinNoise(seed);
// 2D 噪声
const value2D = perlin.noise2D(x, y);
// 3D 噪声
const value3D = perlin.noise3D(x, y, z);
```
### Simplex 噪声
比 Perlin 更快、更少方向性偏差:
```typescript
import { createSimplexNoise } from '@esengine/procgen';
const simplex = createSimplexNoise(seed);
const value = simplex.noise2D(x, y);
```
### Worley 噪声
基于细胞的噪声,适合生成石头、细胞等纹理:
```typescript
import { createWorleyNoise } from '@esengine/procgen';
const worley = createWorleyNoise(seed);
// 返回到最近点的距离
const distance = worley.noise2D(x, y);
```
### FBM (分形布朗运动)
叠加多层噪声创建更丰富的细节:
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
const baseNoise = createPerlinNoise(seed);
const fbm = createFBM(baseNoise, {
octaves: 6, // 层数(越多细节越丰富)
lacunarity: 2.0, // 频率倍增因子
persistence: 0.5, // 振幅衰减因子
frequency: 1.0, // 初始频率
amplitude: 1.0 // 初始振幅
});
// 标准 FBM
const value = fbm.noise2D(x, y);
// Ridged FBM脊状适合山脉
const ridged = fbm.ridged2D(x, y);
// Turbulence湍流
const turb = fbm.turbulence2D(x, y);
// Billowed膨胀适合云朵
const cloud = fbm.billowed2D(x, y);
```
## 种子随机数 API
### SeededRandom
基于 xorshift128+ 算法的确定性伪随机数生成器:
```typescript
import { createSeededRandom } from '@esengine/procgen';
const rng = createSeededRandom(42);
```
### 基础方法
```typescript
// [0, 1) 浮点数
rng.next();
// [min, max] 整数
rng.nextInt(1, 10);
// [min, max) 浮点数
rng.nextFloat(0, 100);
// 布尔值(可指定概率)
rng.nextBool(); // 50%
rng.nextBool(0.3); // 30%
// 重置到初始状态
rng.reset();
```
### 分布方法
```typescript
// 正态分布(高斯分布)
rng.nextGaussian(); // 均值 0, 标准差 1
rng.nextGaussian(100, 15); // 均值 100, 标准差 15
// 指数分布
rng.nextExponential(); // λ = 1
rng.nextExponential(0.5); // λ = 0.5
```
### 几何方法
```typescript
// 圆内均匀分布的点
const point = rng.nextPointInCircle(50); // { x, y }
// 圆周上的点
const edge = rng.nextPointOnCircle(50); // { x, y }
// 球内均匀分布的点
const point3D = rng.nextPointInSphere(50); // { x, y, z }
// 随机方向向量
const dir = rng.nextDirection2D(); // { x, y },长度为 1
```
## 加权随机 API
### WeightedRandom
预计算累积权重,高效随机选择:
```typescript
import { createWeightedRandom } from '@esengine/procgen';
const selector = createWeightedRandom([
{ value: 'apple', weight: 5 },
{ value: 'banana', weight: 3 },
{ value: 'cherry', weight: 2 }
]);
// 使用种子随机数
const result = selector.pick(rng);
// 使用 Math.random
const result2 = selector.pickRandom();
// 获取概率
console.log(selector.getProbability(0)); // 0.5 (5/10)
console.log(selector.size); // 3
console.log(selector.totalWeight); // 10
```
### 便捷函数
```typescript
import { weightedPick, weightedPickFromMap } from '@esengine/procgen';
// 从数组选择
const item = weightedPick([
{ value: 'a', weight: 1 },
{ value: 'b', weight: 2 }
], rng);
// 从对象选择
const item2 = weightedPickFromMap({
'common': 60,
'rare': 30,
'epic': 10
}, rng);
```
## 洗牌和采样 API
### shuffle / shuffleCopy
Fisher-Yates 洗牌算法:
```typescript
import { shuffle, shuffleCopy } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5];
// 原地洗牌
shuffle(arr, rng);
// 创建洗牌副本(不修改原数组)
const shuffled = shuffleCopy(arr, rng);
```
### pickOne
随机选择一个元素:
```typescript
import { pickOne } from '@esengine/procgen';
const items = ['a', 'b', 'c', 'd'];
const item = pickOne(items, rng);
```
### sample / sampleWithReplacement
采样:
```typescript
import { sample, sampleWithReplacement } from '@esengine/procgen';
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 采样 3 个不重复元素
const unique = sample(arr, 3, rng);
// 采样 5 个(可重复)
const withRep = sampleWithReplacement(arr, 5, rng);
```
### randomIntegers
生成范围内的随机整数数组:
```typescript
import { randomIntegers } from '@esengine/procgen';
// 从 1-100 中随机选 5 个不重复的数
const nums = randomIntegers(1, 100, 5, rng);
```
### weightedSample
按权重采样(不重复):
```typescript
import { weightedSample } from '@esengine/procgen';
const items = ['A', 'B', 'C', 'D', 'E'];
const weights = [10, 8, 6, 4, 2];
// 按权重选 3 个
const selected = weightedSample(items, weights, 3, rng);
```
## 实际示例
### 程序化地形生成
```typescript
import { createPerlinNoise, createFBM } from '@esengine/procgen';
class TerrainGenerator {
private fbm: FBM;
private moistureFbm: FBM;
constructor(seed: number) {
const heightNoise = createPerlinNoise(seed);
const moistureNoise = createPerlinNoise(seed + 1000);
this.fbm = createFBM(heightNoise, {
octaves: 8,
persistence: 0.5,
frequency: 0.01
});
this.moistureFbm = createFBM(moistureNoise, {
octaves: 4,
persistence: 0.6,
frequency: 0.02
});
}
getHeight(x: number, y: number): number {
// 基础高度
let height = this.fbm.noise2D(x, y);
// 添加山脉
height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
return (height + 1) * 0.5; // 归一化到 [0, 1]
}
getBiome(x: number, y: number): string {
const height = this.getHeight(x, y);
const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;
if (height < 0.3) return 'water';
if (height < 0.4) return 'beach';
if (height > 0.8) return 'mountain';
if (moisture < 0.3) return 'desert';
if (moisture > 0.7) return 'forest';
return 'grassland';
}
}
```
### 战利品系统
```typescript
import { createSeededRandom, createWeightedRandom, sample } from '@esengine/procgen';
interface LootItem {
id: string;
rarity: string;
}
class LootSystem {
private rng: SeededRandom;
private raritySelector: WeightedRandom<string>;
private lootTables: Map<string, LootItem[]> = new Map();
constructor(seed: number) {
this.rng = createSeededRandom(seed);
this.raritySelector = createWeightedRandom([
{ value: 'common', weight: 60 },
{ value: 'uncommon', weight: 25 },
{ value: 'rare', weight: 10 },
{ value: 'legendary', weight: 5 }
]);
// 初始化战利品表
this.lootTables.set('common', [/* ... */]);
this.lootTables.set('rare', [/* ... */]);
// ...
}
generateLoot(count: number): LootItem[] {
const loot: LootItem[] = [];
for (let i = 0; i < count; i++) {
const rarity = this.raritySelector.pick(this.rng);
const table = this.lootTables.get(rarity)!;
const item = pickOne(table, this.rng);
loot.push(item);
}
return loot;
}
// 保证可重现
setSeed(seed: number): void {
this.rng = createSeededRandom(seed);
}
}
```
### 程序化敌人放置
```typescript
import { createSeededRandom } from '@esengine/procgen';
class EnemySpawner {
private rng: SeededRandom;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
}
spawnEnemiesInArea(
centerX: number,
centerY: number,
radius: number,
count: number
): Array<{ x: number; y: number; type: string }> {
const enemies: Array<{ x: number; y: number; type: string }> = [];
for (let i = 0; i < count; i++) {
// 在圆内生成位置
const pos = this.rng.nextPointInCircle(radius);
// 随机选择敌人类型
const type = this.rng.nextBool(0.2) ? 'elite' : 'normal';
enemies.push({
x: centerX + pos.x,
y: centerY + pos.y,
type
});
}
return enemies;
}
}
```
### 程序化关卡布局
```typescript
import { createSeededRandom, shuffle } from '@esengine/procgen';
interface Room {
x: number;
y: number;
width: number;
height: number;
type: 'start' | 'combat' | 'treasure' | 'boss';
}
class DungeonGenerator {
private rng: SeededRandom;
constructor(seed: number) {
this.rng = createSeededRandom(seed);
}
generate(roomCount: number): Room[] {
const rooms: Room[] = [];
// 生成房间
for (let i = 0; i < roomCount; i++) {
rooms.push({
x: this.rng.nextInt(0, 100),
y: this.rng.nextInt(0, 100),
width: this.rng.nextInt(5, 15),
height: this.rng.nextInt(5, 15),
type: 'combat'
});
}
// 随机分配特殊房间
shuffle(rooms, this.rng);
rooms[0].type = 'start';
rooms[1].type = 'treasure';
rooms[rooms.length - 1].type = 'boss';
return rooms;
}
}
```
## 蓝图节点
Procgen 模块提供了可视化脚本支持的蓝图节点:
### 噪声节点
- `SampleNoise2D` - 采样 2D 噪声
- `SampleFBM` - 采样 FBM 噪声
### 随机节点
- `SeededRandom` - 生成随机浮点数
- `SeededRandomInt` - 生成随机整数
- `WeightedPick` - 加权随机选择
- `ShuffleArray` - 洗牌数组
- `PickRandom` - 随机选择元素
- `SampleArray` - 采样数组
- `RandomPointInCircle` - 圆内随机点
## 最佳实践
1. **使用种子保证可重现性**
```typescript
// 保存种子以便重现相同结果
const seed = Date.now();
const rng = createSeededRandom(seed);
saveSeed(seed);
```
2. **预计算加权选择器**
```typescript
// 好:创建一次,多次使用
const selector = createWeightedRandom(items);
for (let i = 0; i < 1000; i++) {
selector.pick(rng);
}
// 不好:每次都创建
for (let i = 0; i < 1000; i++) {
weightedPick(items, rng);
}
```
3. **选择合适的噪声函数**
- Perlin平滑过渡的地形、云彩
- Simplex性能要求高的场景
- Worley细胞、石头纹理
- FBM需要多层细节的自然效果
4. **调整 FBM 参数**
- `octaves`:越多细节越丰富,但性能开销越大
- `persistence`0.5 是常用值,越大高频细节越明显
- `lacunarity`:通常为 2控制频率增长速度

View File

@@ -0,0 +1,600 @@
# 空间索引系统 (Spatial)
`@esengine/spatial` 提供了高效的空间查询和索引功能,包括范围查询、最近邻查询、射线检测和 AOI兴趣区域管理。
## 安装
```bash
npm install @esengine/spatial
```
## 快速开始
### 空间索引
```typescript
import { createGridSpatialIndex } from '@esengine/spatial';
// 创建空间索引(网格单元格大小为 100
const spatialIndex = createGridSpatialIndex<Entity>(100);
// 插入对象
spatialIndex.insert(player, { x: 100, y: 200 });
spatialIndex.insert(enemy1, { x: 150, y: 250 });
spatialIndex.insert(enemy2, { x: 500, y: 600 });
// 查找半径内的对象
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
console.log(nearby); // [player, enemy1]
// 查找最近的对象
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
console.log(nearest); // enemy1
// 更新位置
spatialIndex.update(player, { x: 120, y: 220 });
```
### AOI 兴趣区域
```typescript
import { createGridAOI } from '@esengine/spatial';
// 创建 AOI 管理器
const aoi = createGridAOI<Entity>(100);
// 添加观察者(玩家)
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
// 监听进入/离开事件
aoi.addListener((event) => {
if (event.type === 'enter') {
console.log(`${event.observer} 看到了 ${event.target}`);
} else if (event.type === 'exit') {
console.log(`${event.target} 离开了 ${event.observer} 的视野`);
}
});
// 更新位置(会自动触发进入/离开事件)
aoi.updatePosition(player, { x: 200, y: 200 });
// 获取视野内的实体
const visible = aoi.getEntitiesInView(player);
```
## 核心概念
### 空间索引 vs AOI
| 特性 | 空间索引 (SpatialIndex) | AOI (Area of Interest) |
|------|------------------------|------------------------|
| 用途 | 通用空间查询 | 实体可见性追踪 |
| 事件 | 无事件通知 | 进入/离开事件 |
| 方向 | 单向查询 | 双向追踪(谁看到谁) |
| 场景 | 碰撞检测、范围攻击 | MMO 同步、NPC AI 感知 |
### IBounds 边界框
```typescript
interface IBounds {
readonly minX: number;
readonly minY: number;
readonly maxX: number;
readonly maxY: number;
}
```
### IRaycastHit 射线检测结果
```typescript
interface IRaycastHit<T> {
readonly target: T; // 命中的对象
readonly point: IVector2; // 命中点坐标
readonly normal: IVector2; // 命中点法线
readonly distance: number; // 距离射线起点的距离
}
```
## 空间索引 API
### createGridSpatialIndex
```typescript
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
```
创建基于均匀网格的空间索引。
**参数:**
- `cellSize` - 网格单元格大小(默认 100
**选择合适的 cellSize**
- 太小:内存占用高,查询效率降低
- 太大:单元格内对象过多,遍历耗时
- 建议:设置为对象平均分布间距的 1-2 倍
### 管理方法
#### insert
插入对象到索引:
```typescript
spatialIndex.insert(enemy, { x: 100, y: 200 });
```
#### remove
移除对象:
```typescript
spatialIndex.remove(enemy);
```
#### update
更新对象位置:
```typescript
spatialIndex.update(enemy, { x: 150, y: 250 });
```
#### clear
清空索引:
```typescript
spatialIndex.clear();
```
### 查询方法
#### findInRadius
查找圆形范围内的所有对象:
```typescript
// 查找中心点 (100, 200) 半径 50 内的所有敌人
const enemies = spatialIndex.findInRadius(
{ x: 100, y: 200 },
50,
(entity) => entity.type === 'enemy' // 可选过滤器
);
```
#### findInRect
查找矩形区域内的所有对象:
```typescript
import { createBounds } from '@esengine/spatial';
const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);
```
#### findNearest
查找最近的对象:
```typescript
// 查找最近的敌人(最大搜索距离 500
const nearest = spatialIndex.findNearest(
playerPosition,
500, // maxDistance
(entity) => entity.type === 'enemy'
);
if (nearest) {
attackTarget(nearest);
}
```
#### findKNearest
查找最近的 K 个对象:
```typescript
// 查找最近的 5 个敌人
const nearestEnemies = spatialIndex.findKNearest(
playerPosition,
5, // k
500, // maxDistance
(entity) => entity.type === 'enemy'
);
```
#### raycast
射线检测(返回所有命中):
```typescript
const hits = spatialIndex.raycast(
origin, // 射线起点
direction, // 射线方向(应归一化)
maxDistance, // 最大检测距离
filter // 可选过滤器
);
// hits 按距离排序
for (const hit of hits) {
console.log(`命中 ${hit.target} at ${hit.point}, 距离 ${hit.distance}`);
}
```
#### raycastFirst
射线检测(仅返回第一个命中):
```typescript
const hit = spatialIndex.raycastFirst(origin, direction, 1000);
if (hit) {
dealDamage(hit.target, calculateDamage(hit.distance));
}
```
### 属性
```typescript
// 获取索引中的对象数量
console.log(spatialIndex.count);
// 获取所有对象
const all = spatialIndex.getAll();
```
## AOI 兴趣区域 API
### createGridAOI
```typescript
function createGridAOI<T>(cellSize?: number): GridAOI<T>
```
创建基于网格的 AOI 管理器。
**参数:**
- `cellSize` - 网格单元格大小(建议为平均视野范围的 1-2 倍)
### 观察者管理
#### addObserver
添加观察者:
```typescript
aoi.addObserver(player, position, {
viewRange: 200, // 视野范围
observable: true // 是否可被其他观察者看到(默认 true
});
// NPC 只观察不被观察
aoi.addObserver(camera, position, {
viewRange: 500,
observable: false
});
```
#### removeObserver
移除观察者:
```typescript
aoi.removeObserver(player);
```
#### updatePosition
更新位置(自动触发进入/离开事件):
```typescript
aoi.updatePosition(player, newPosition);
```
#### updateViewRange
更新视野范围:
```typescript
// 获得增益后视野扩大
aoi.updateViewRange(player, 300);
```
### 查询方法
#### getEntitiesInView
获取观察者视野内的所有实体:
```typescript
const visible = aoi.getEntitiesInView(player);
for (const entity of visible) {
updateEntityForPlayer(player, entity);
}
```
#### getObserversOf
获取能看到指定实体的所有观察者:
```typescript
const observers = aoi.getObserversOf(monster);
for (const observer of observers) {
notifyMonsterMoved(observer, monster);
}
```
#### canSee
检查是否可见:
```typescript
if (aoi.canSee(player, enemy)) {
enemy.showHealthBar();
}
```
### 事件系统
#### 全局事件监听
```typescript
aoi.addListener((event) => {
switch (event.type) {
case 'enter':
console.log(`${event.observer} 看到了 ${event.target}`);
break;
case 'exit':
console.log(`${event.target} 离开了 ${event.observer} 的视野`);
break;
}
});
```
#### 实体特定事件监听
```typescript
// 只监听特定玩家的视野事件
aoi.addEntityListener(player, (event) => {
if (event.type === 'enter') {
sendToClient(player, 'entity_enter', event.target);
} else if (event.type === 'exit') {
sendToClient(player, 'entity_exit', event.target);
}
});
```
#### 事件类型
```typescript
interface IAOIEvent<T> {
type: 'enter' | 'exit' | 'update';
observer: T; // 观察者(谁看到了变化)
target: T; // 目标(发生变化的对象)
position: IVector2; // 目标位置
}
```
## 工具函数
### 边界框创建
```typescript
import {
createBounds,
createBoundsFromCenter,
createBoundsFromCircle
} from '@esengine/spatial';
// 从角点创建
const bounds1 = createBounds(0, 0, 100, 100);
// 从中心点和尺寸创建
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
// 从圆形创建(包围盒)
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
```
### 几何检测
```typescript
import {
isPointInBounds,
boundsIntersect,
boundsIntersectsCircle,
distance,
distanceSquared
} from '@esengine/spatial';
// 点在边界内?
if (isPointInBounds(point, bounds)) { ... }
// 两个边界框相交?
if (boundsIntersect(boundsA, boundsB)) { ... }
// 边界框与圆形相交?
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
// 距离计算
const dist = distance(pointA, pointB);
const distSq = distanceSquared(pointA, pointB); // 更快,避免 sqrt
```
## 实际示例
### 范围攻击检测
```typescript
class CombatSystem {
private spatialIndex: ISpatialIndex<Entity>;
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
const targets = this.spatialIndex.findInRadius(
center,
radius,
(entity) => entity.hasComponent(HealthComponent)
);
for (const target of targets) {
const health = target.getComponent(HealthComponent);
health.takeDamage(damage);
}
}
findNearestEnemy(position: IVector2, team: string): Entity | null {
return this.spatialIndex.findNearest(
position,
undefined, // 无距离限制
(entity) => {
const teamComp = entity.getComponent(TeamComponent);
return teamComp && teamComp.team !== team;
}
);
}
}
```
### MMO 同步系统
```typescript
class SyncSystem {
private aoi: IAOIManager<Player>;
constructor() {
this.aoi = createGridAOI<Player>(100);
// 监听进入/离开事件
this.aoi.addListener((event) => {
const packet = this.createSyncPacket(event);
this.sendToPlayer(event.observer, packet);
});
}
onPlayerJoin(player: Player): void {
this.aoi.addObserver(player, player.position, {
viewRange: player.viewRange
});
}
onPlayerMove(player: Player, newPosition: IVector2): void {
this.aoi.updatePosition(player, newPosition);
}
onPlayerLeave(player: Player): void {
this.aoi.removeObserver(player);
}
// 广播给所有能看到某玩家的其他玩家
broadcastToObservers(player: Player, packet: Packet): void {
const observers = this.aoi.getObserversOf(player);
for (const observer of observers) {
this.sendToPlayer(observer, packet);
}
}
}
```
### NPC AI 感知
```typescript
class AIPerceptionSystem {
private aoi: IAOIManager<Entity>;
constructor() {
this.aoi = createGridAOI<Entity>(50);
}
setupNPC(npc: Entity): void {
const perception = npc.getComponent(PerceptionComponent);
this.aoi.addObserver(npc, npc.position, {
viewRange: perception.range
});
// 监听该 NPC 的感知事件
this.aoi.addEntityListener(npc, (event) => {
const ai = npc.getComponent(AIComponent);
if (event.type === 'enter') {
ai.onTargetDetected(event.target);
} else if (event.type === 'exit') {
ai.onTargetLost(event.target);
}
});
}
update(): void {
// 更新所有 NPC 位置
for (const npc of this.npcs) {
this.aoi.updatePosition(npc, npc.position);
}
}
}
```
## 蓝图节点
### 空间查询节点
- `FindInRadius` - 查找半径内的对象
- `FindInRect` - 查找矩形内的对象
- `FindNearest` - 查找最近的对象
- `FindKNearest` - 查找最近的 K 个对象
- `Raycast` - 射线检测
- `RaycastFirst` - 射线检测(仅第一个)
### AOI 节点
- `GetEntitiesInView` - 获取视野内实体
- `GetObserversOf` - 获取观察者
- `CanSee` - 检查可见性
- `OnEntityEnterView` - 进入视野事件
- `OnEntityExitView` - 离开视野事件
## 服务令牌
在依赖注入场景中使用:
```typescript
import {
SpatialIndexToken,
SpatialQueryToken,
AOIManagerToken,
createGridSpatialIndex,
createGridAOI
} from '@esengine/spatial';
// 注册服务
services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));
// 获取服务
const spatialIndex = services.get(SpatialIndexToken);
const aoiManager = services.get(AOIManagerToken);
```
## 性能优化
1. **选择合适的 cellSize**
- 太小:内存占用高,单元格数量多
- 太大:单元格内对象多,遍历慢
- 经验法则:对象平均间距的 1-2 倍
2. **使用过滤器减少结果**
```typescript
// 在空间查询阶段就过滤,而不是事后过滤
spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy');
```
3. **使用 distanceSquared 代替 distance**
```typescript
// 避免 sqrt 计算
if (distanceSquared(a, b) < threshold * threshold) { ... }
```
4. **批量更新优化**
```typescript
// 如果有大量对象同时移动,考虑禁用事件后批量更新
```

479
docs/modules/timer/index.md Normal file
View File

@@ -0,0 +1,479 @@
# 定时器系统 (Timer)
`@esengine/timer` 提供了一个灵活的定时器和冷却系统,用于游戏中的延迟执行、重复任务、技能冷却等场景。
## 安装
```bash
npm install @esengine/timer
```
## 快速开始
```typescript
import { createTimerService } from '@esengine/timer';
// 创建定时器服务
const timerService = createTimerService();
// 一次性定时器1秒后执行
const handle = timerService.schedule('myTimer', 1000, () => {
console.log('Timer fired!');
});
// 重复定时器每100毫秒执行
timerService.scheduleRepeating('heartbeat', 100, () => {
console.log('Tick');
});
// 冷却系统5秒冷却
timerService.startCooldown('skill_fireball', 5000);
if (timerService.isCooldownReady('skill_fireball')) {
// 可以使用技能
useFireball();
timerService.startCooldown('skill_fireball', 5000);
}
// 游戏循环中更新
function gameLoop(deltaTime: number) {
timerService.update(deltaTime);
}
```
## 核心概念
### 定时器 vs 冷却
| 特性 | 定时器 (Timer) | 冷却 (Cooldown) |
|------|---------------|-----------------|
| 用途 | 延迟执行代码 | 限制操作频率 |
| 回调 | 有回调函数 | 无回调函数 |
| 重复 | 支持重复执行 | 一次性 |
| 查询 | 查询剩余时间 | 查询进度/是否就绪 |
### TimerHandle
调度定时器后返回的句柄对象,用于控制定时器:
```typescript
interface TimerHandle {
readonly id: string; // 定时器 ID
readonly isValid: boolean; // 是否有效(未被取消)
cancel(): void; // 取消定时器
}
```
### TimerInfo
定时器信息对象:
```typescript
interface TimerInfo {
readonly id: string; // 定时器 ID
readonly remaining: number; // 剩余时间(毫秒)
readonly repeating: boolean; // 是否重复执行
readonly interval?: number; // 间隔时间(仅重复定时器)
}
```
### CooldownInfo
冷却信息对象:
```typescript
interface CooldownInfo {
readonly id: string; // 冷却 ID
readonly duration: number; // 总持续时间(毫秒)
readonly remaining: number; // 剩余时间(毫秒)
readonly progress: number; // 进度0-10=刚开始1=结束)
readonly isReady: boolean; // 是否已就绪
}
```
## API 参考
### createTimerService
```typescript
function createTimerService(config?: TimerServiceConfig): ITimerService
```
**配置选项:**
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `maxTimers` | `number` | `0` | 最大定时器数量0 表示无限制) |
| `maxCooldowns` | `number` | `0` | 最大冷却数量0 表示无限制) |
### 定时器 API
#### schedule
调度一次性定时器:
```typescript
const handle = timerService.schedule('explosion', 2000, () => {
createExplosion();
});
// 提前取消
handle.cancel();
```
#### scheduleRepeating
调度重复定时器:
```typescript
// 每秒执行
timerService.scheduleRepeating('regen', 1000, () => {
player.hp += 5;
});
// 立即执行一次,然后每秒重复
timerService.scheduleRepeating('tick', 1000, () => {
console.log('Tick');
}, true); // immediate = true
```
#### cancel / cancelById
取消定时器:
```typescript
// 通过句柄取消
handle.cancel();
// 或
timerService.cancel(handle);
// 通过 ID 取消
timerService.cancelById('regen');
```
#### hasTimer
检查定时器是否存在:
```typescript
if (timerService.hasTimer('explosion')) {
console.log('Explosion is pending');
}
```
#### getTimerInfo
获取定时器信息:
```typescript
const info = timerService.getTimerInfo('explosion');
if (info) {
console.log(`剩余时间: ${info.remaining}ms`);
console.log(`是否重复: ${info.repeating}`);
}
```
### 冷却 API
#### startCooldown
开始冷却:
```typescript
// 5秒冷却
timerService.startCooldown('skill_fireball', 5000);
```
#### isCooldownReady / isOnCooldown
检查冷却状态:
```typescript
if (timerService.isCooldownReady('skill_fireball')) {
// 可以使用技能
castFireball();
timerService.startCooldown('skill_fireball', 5000);
} else {
console.log('技能还在冷却中');
}
// 或使用 isOnCooldown
if (timerService.isOnCooldown('skill_fireball')) {
console.log('冷却中...');
}
```
#### getCooldownProgress / getCooldownRemaining
获取冷却进度:
```typescript
// 进度 0-10=刚开始1=完成)
const progress = timerService.getCooldownProgress('skill_fireball');
console.log(`冷却进度: ${(progress * 100).toFixed(0)}%`);
// 剩余时间(毫秒)
const remaining = timerService.getCooldownRemaining('skill_fireball');
console.log(`剩余时间: ${(remaining / 1000).toFixed(1)}s`);
```
#### getCooldownInfo
获取完整冷却信息:
```typescript
const info = timerService.getCooldownInfo('skill_fireball');
if (info) {
console.log(`总时长: ${info.duration}ms`);
console.log(`剩余: ${info.remaining}ms`);
console.log(`进度: ${info.progress}`);
console.log(`就绪: ${info.isReady}`);
}
```
#### resetCooldown / clearAllCooldowns
重置冷却:
```typescript
// 重置单个冷却
timerService.resetCooldown('skill_fireball');
// 清除所有冷却(例如角色复活时)
timerService.clearAllCooldowns();
```
### 生命周期
#### update
更新定时器服务(需要每帧调用):
```typescript
function gameLoop(deltaTime: number) {
// deltaTime 单位是毫秒
timerService.update(deltaTime);
}
```
#### clear
清除所有定时器和冷却:
```typescript
timerService.clear();
```
### 调试属性
```typescript
// 获取活跃定时器数量
console.log(timerService.activeTimerCount);
// 获取活跃冷却数量
console.log(timerService.activeCooldownCount);
// 获取所有活跃定时器 ID
const timerIds = timerService.getActiveTimerIds();
// 获取所有活跃冷却 ID
const cooldownIds = timerService.getActiveCooldownIds();
```
## 实际示例
### 技能冷却系统
```typescript
import { createTimerService, type ITimerService } from '@esengine/timer';
class SkillSystem {
private timerService: ITimerService;
private skills: Map<string, SkillData> = new Map();
constructor() {
this.timerService = createTimerService();
}
registerSkill(id: string, data: SkillData): void {
this.skills.set(id, data);
}
useSkill(skillId: string): boolean {
const skill = this.skills.get(skillId);
if (!skill) return false;
// 检查冷却
if (!this.timerService.isCooldownReady(skillId)) {
const remaining = this.timerService.getCooldownRemaining(skillId);
console.log(`技能 ${skillId} 冷却中,剩余 ${remaining}ms`);
return false;
}
// 使用技能
this.executeSkill(skill);
// 开始冷却
this.timerService.startCooldown(skillId, skill.cooldown);
return true;
}
getSkillCooldownProgress(skillId: string): number {
return this.timerService.getCooldownProgress(skillId);
}
update(dt: number): void {
this.timerService.update(dt);
}
}
interface SkillData {
cooldown: number;
// ... other properties
}
```
### 延迟和定时效果
```typescript
class EffectSystem {
private timerService: ITimerService;
constructor(timerService: ITimerService) {
this.timerService = timerService;
}
// 延迟爆炸
scheduleExplosion(position: { x: number; y: number }, delay: number): void {
this.timerService.schedule(`explosion_${Date.now()}`, delay, () => {
this.createExplosion(position);
});
}
// DOT 伤害(每秒造成伤害)
applyDOT(target: Entity, damage: number, duration: number): void {
const dotId = `dot_${target.id}_${Date.now()}`;
let elapsed = 0;
this.timerService.scheduleRepeating(dotId, 1000, () => {
elapsed += 1000;
target.takeDamage(damage);
if (elapsed >= duration) {
this.timerService.cancelById(dotId);
}
});
}
// BUFF 效果(持续一段时间)
applyBuff(target: Entity, buffId: string, duration: number): void {
target.addBuff(buffId);
this.timerService.schedule(`buff_expire_${buffId}`, duration, () => {
target.removeBuff(buffId);
});
}
}
```
### 与 ECS 集成
```typescript
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
import { createTimerService, type ITimerService } from '@esengine/timer';
// 定时器组件
class TimerComponent extends Component {
timerService: ITimerService;
constructor() {
super();
this.timerService = createTimerService();
}
}
// 定时器系统
class TimerSystem extends EntitySystem {
constructor() {
super(Matcher.all(TimerComponent));
}
protected processEntity(entity: Entity, dt: number): void {
const timer = entity.getComponent(TimerComponent);
timer.timerService.update(dt);
}
}
// 冷却组件(用于共享冷却)
class CooldownComponent extends Component {
constructor(public timerService: ITimerService) {
super();
}
}
```
## 蓝图节点
Timer 模块提供了可视化脚本支持的蓝图节点:
### 冷却节点
- `StartCooldown` - 开始冷却
- `IsCooldownReady` - 检查冷却是否就绪
- `GetCooldownProgress` - 获取冷却进度
- `GetCooldownInfo` - 获取详细冷却信息
- `ResetCooldown` - 重置冷却
### 定时器节点
- `HasTimer` - 检查定时器是否存在
- `CancelTimer` - 取消定时器
- `GetTimerRemaining` - 获取定时器剩余时间
## 服务令牌
在依赖注入场景中使用:
```typescript
import { TimerServiceToken, createTimerService } from '@esengine/timer';
// 注册服务
services.register(TimerServiceToken, createTimerService());
// 获取服务
const timerService = services.get(TimerServiceToken);
```
## 最佳实践
1. **使用有意义的 ID**:使用描述性的 ID 便于调试和管理
```typescript
// 好
timerService.startCooldown('skill_fireball', 5000);
// 不好
timerService.startCooldown('cd1', 5000);
```
2. **避免重复 ID**:相同 ID 的定时器会覆盖之前的
```typescript
// 使用唯一 ID
const uniqueId = `explosion_${entity.id}_${Date.now()}`;
timerService.schedule(uniqueId, 1000, callback);
```
3. **及时清理**:在适当时机清理不需要的定时器和冷却
```typescript
// 实体销毁时
onDestroy() {
this.timerService.cancelById(this.timerId);
}
```
4. **配置限制**:在生产环境考虑设置最大数量限制
```typescript
const timerService = createTimerService({
maxTimers: 1000,
maxCooldowns: 500
});
```

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