Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee7cf4278 | ||
|
|
b9db6f0b40 | ||
|
|
8967cba3c7 | ||
|
|
d04ad2eea9 | ||
|
|
f2d3880a06 | ||
|
|
ec5f70ecfc | ||
|
|
40b3fe7165 | ||
|
|
4095f1e946 | ||
|
|
e219fc47ba | ||
|
|
6e2e7a4af5 | ||
|
|
2e7f764d6c | ||
|
|
ce64de5b3d | ||
|
|
35ca1dd7ea | ||
|
|
8d0ad6b871 | ||
|
|
0aa4791cf7 | ||
|
|
082c2b46d0 | ||
|
|
50420f9052 | ||
|
|
499cbf8a60 | ||
|
|
2e38284d6e | ||
|
|
00cc3a11c6 | ||
|
|
9909a7f7b0 | ||
|
|
3363fca160 | ||
|
|
78e0b09c7a | ||
|
|
caa3ffc8f5 | ||
|
|
323fb6a5fe | ||
|
|
1adc5f1729 | ||
|
|
78079252c9 | ||
|
|
72fdabd099 | ||
|
|
64bd6aa055 | ||
|
|
6329200b84 | ||
|
|
3f3fd16110 | ||
|
|
34f0c4ac2d | ||
|
|
dde04d514e | ||
|
|
ffddadd798 | ||
|
|
5dca337b92 | ||
|
|
131df181e6 | ||
|
|
e207952786 | ||
|
|
3f7ef284fc | ||
|
|
1a41533d15 | ||
|
|
85f7bbbf1a | ||
|
|
ccc603b59f | ||
|
|
8135f99616 | ||
|
|
713f4ae18b | ||
|
|
f3f5d0bbd1 | ||
|
|
4a9e11c480 | ||
|
|
cd94326aad | ||
|
|
a4b971bba0 | ||
|
|
5d1609111c | ||
|
|
d54ccaf629 | ||
|
|
6adea240e2 | ||
|
|
3486d403d2 | ||
|
|
f533186c8d | ||
|
|
0beadf8e5a | ||
|
|
8bc06f0476 | ||
|
|
8fd8f74b27 | ||
|
|
9fc9b60de5 | ||
|
|
2a026726db | ||
|
|
fd82486bbc | ||
|
|
1e5ddadd00 | ||
|
|
7bab76d765 |
4
.fleet/settings.json
Normal file
4
.fleet/settings.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"toolchains": [],
|
||||
"backend.maxHeapSizeMb": 896
|
||||
}
|
||||
34
.github/workflows/node.js.yml
vendored
34
.github/workflows/node.js.yml
vendored
@@ -1,34 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [8.x]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: cmd
|
||||
working-directory: ./source
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- run: npm run build --if-present
|
||||
- run: gulp build
|
||||
85
.gitignore
vendored
85
.gitignore
vendored
@@ -1,10 +1,75 @@
|
||||
/source/node_modules
|
||||
/demo/bin-debug
|
||||
/demo/bin-release
|
||||
/.idea
|
||||
/.vscode
|
||||
/demo_wxgame
|
||||
/demo/.wing
|
||||
/demo/.idea
|
||||
/demo/.vscode
|
||||
/source/docs
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 构建输出
|
||||
bin/
|
||||
dist/
|
||||
wasm/
|
||||
*.tgz
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
# IDE 配置
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# 日志文件
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 环境配置
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 测试覆盖率
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Rust 构建文件
|
||||
src/wasm/*/target/
|
||||
src/wasm/*/pkg/
|
||||
Cargo.lock
|
||||
|
||||
# 包管理器锁文件(保留npm的,忽略其他的)
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# 文档生成
|
||||
docs/api/
|
||||
docs/build/
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# 演示项目构建产物
|
||||
/demo/bin-debug/
|
||||
/demo/bin-release/
|
||||
/demo/.wing/
|
||||
/demo/.idea/
|
||||
/demo/.vscode/
|
||||
/demo_wxgame/
|
||||
|
||||
12
.gitmodules
vendored
12
.gitmodules
vendored
@@ -1,12 +0,0 @@
|
||||
[submodule "demo/egret_demo"]
|
||||
path = demo/egret_demo
|
||||
url = https://github.com/esengine/ecs-egret-demo
|
||||
[submodule "demo/laya_demo"]
|
||||
path = demo/laya_demo
|
||||
url = https://github.com/esengine/ecs-laya-demo.git
|
||||
[submodule "extensions/ecs-star"]
|
||||
path = extensions/ecs-star
|
||||
url = https://github.com/esengine/ecs-astar
|
||||
[submodule "extensions/behaviourTree-ai"]
|
||||
path = extensions/behaviourTree-ai
|
||||
url = https://github.com/esengine/BehaviourTree-ai
|
||||
46
.npmignore
Normal file
46
.npmignore
Normal file
@@ -0,0 +1,46 @@
|
||||
# 源代码文件
|
||||
src/
|
||||
tsconfig*.json
|
||||
*.ts
|
||||
!bin/**/*.d.ts
|
||||
|
||||
# 开发文件
|
||||
dev-bin/
|
||||
scripts/
|
||||
.vscode/
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# 测试文件
|
||||
**/*.test.*
|
||||
**/*.spec.*
|
||||
**/test/
|
||||
**/tests/
|
||||
|
||||
# 构建缓存
|
||||
node_modules/
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Rust 构建文件(保留编译后的WASM)
|
||||
src/wasm/rust-ecs-core/target/
|
||||
src/wasm/rust-ecs-core/Cargo.lock
|
||||
src/wasm/rust-ecs-core/pkg/
|
||||
!bin/wasm/
|
||||
|
||||
# 文档草稿
|
||||
docs/draft/
|
||||
*.draft.md
|
||||
|
||||
# 编辑器文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 环境文件
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
361
README.md
361
README.md
@@ -1,79 +1,326 @@
|
||||
ecs-framework 的目标是成为功能强大的框架。它为您构建游戏提供了坚实的基础。它包括的许多功能包括:
|
||||
# ECS Framework
|
||||
|
||||
- 完整的场景/实体/组件系统
|
||||
- SpatialHash用于超快速的广相物理学查找。您永远不会看到它,因为它在幕后起作用,但是您仍然会喜欢它,因为它可以通过射线广播或重叠检查迅速找到您附近的所有事物。
|
||||
- AABB,圆和多边形碰撞/触发检测
|
||||
- 高效的协程,可在多个帧或动画定时中分解大型任务(Core.startCoroutine)
|
||||
- 通过Astar和广度优先搜索提供寻路支持,以查找图块地图或您自己的自定义格式 ( 参见 https://github.com/esengine/ecs-astar )
|
||||
- tween系统。任何number / Vector / 矩形/字段或属性都可以tween。
|
||||
- 针对核心事件的优化的事件发射器(发射器类),您也可以将其添加到自己的任何类中
|
||||
- 延迟和重复任务的调度程序(核心调度方法)
|
||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
# 快速开始
|
||||
- [运行框架](docs/getting_start.md)
|
||||
- [创建实体与组件](docs/create_entity_component.md)
|
||||
- [创建系统](docs/system.md)
|
||||
- [全局时间Time](docs/time.md)
|
||||
- [协程Coroutine](docs/coroutine.md)
|
||||
- [Physics](docs/physics.md)
|
||||
- [Emitter](docs/emitter.md)
|
||||
- [Collision](docs/collision.md)
|
||||
一个专业级的 TypeScript ECS(Entity-Component-System)框架,采用现代化架构设计,专为高性能游戏开发打造。
|
||||
|
||||
## 交流群
|
||||
点击链接加入群聊【ecs游戏框架交流】:https://jq.qq.com/?_wv=1027&k=29w1Nud6
|
||||
## ✨ 核心特性
|
||||
|
||||
- 🏗️ **现代化 ECS 架构** - 完整的实体组件系统,提供清晰的代码结构
|
||||
- 📡 **类型安全事件系统** - 增强的事件总线,支持异步事件、优先级、批处理和装饰器
|
||||
- ⏰ **定时器管理系统** - 完整的定时器管理,支持延迟和重复任务
|
||||
- 🔍 **智能查询系统** - 支持复杂的实体查询,流式API设计
|
||||
- ⚡ **高性能优化** - 组件索引、Archetype系统、脏标记机制三重优化
|
||||
- 🛠️ **开发者友好** - 完整的TypeScript支持,丰富的调试工具
|
||||
- 📦 **轻量级设计** - 最小化依赖,适用于各种游戏引擎
|
||||
|
||||
## Scene/Entity/Component
|
||||
框架的大部分围绕着实体组件系统(ECS)。ECS与您可能使用过的任何其他ECS均不同,所以我为您再以下详细介绍。
|
||||
## 📦 安装
|
||||
|
||||
### Scene
|
||||
ECS的根源。可以将场景视为游戏的不同部分,在适当的时间调用它们的方法。您也可以使用场景通过findEntity和findEntitiesByTag方法定位实体。
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
场景可以包含一种称为场景组件的特殊类型的组件。 SceneComponent通过add / get / removeSceneComponent方法进行管理。可以将场景组件视为简化组件。它包含少量可重写的生命周期方法(onEnabled / onDisabled / update / onRemovedFromScene)。当您需要一个位于场景级别但不需要实体容器的对象时,可以使用这些对象。
|
||||
## 🚀 快速开始
|
||||
|
||||
### Entity
|
||||
将实体添加到场景中/从场景中删除,并由场景进行管理。 您可以子类化Entity,也可以只创建一个Entity实例,然后向其中添加任何必需的组件(通过addComponent,然后通过getComponent检索)。 在实体的最基本层次上,可以将其视为组件的容器。 实体具有一系列在整个生命周期中的不同时间被场景调用的方法。
|
||||
### 1. 基础设置
|
||||
|
||||
实体生命周期方法:
|
||||
- onAddedToScene:在将所有未决的实体更改提交后将实体添加到场景中时调用
|
||||
- onRemovedFromScene:当实体从场景中移除时调用
|
||||
- update:只要启用了实体,就会每帧调用
|
||||
```typescript
|
||||
import { Core, CoreEvents, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
实体上的一些关键/重要属性如下:
|
||||
// 创建 Core 实例
|
||||
const core = Core.create(true); // 开启调试模式
|
||||
|
||||
- updateOrder:控制实体的顺序。 这会影响在每个实体上调用更新的顺序以及标签列表的顺序。
|
||||
- tag:随便使用它。 以后可以使用它在场景中查询具有特定标签(Scene.findEntitiesByTag)的所有实体。
|
||||
- updateInterval:指定应多久调用一次此Entities更新方法。 1表示每帧,2表示每两帧,依此类推
|
||||
// 创建场景
|
||||
class GameScene extends Scene {
|
||||
public initialize() {
|
||||
// 场景初始化逻辑
|
||||
}
|
||||
}
|
||||
|
||||
### Component
|
||||
组件添加到实体并由实体管理。 它们构成了您游戏的重点,并且基本上是可重用的代码块,这些代码决定了实体的行为方式。
|
||||
// 在游戏循环中更新框架
|
||||
function gameLoop() {
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
}
|
||||
```
|
||||
|
||||
组件生命周期方法:
|
||||
### 2. 创建实体和组件
|
||||
|
||||
- initialize:在创建组件并分配Entity字段但在onAddedToEntity之前调用
|
||||
- onAddedToEntity:在将所有未决组件更改提交后将组件添加到实体时调用
|
||||
- onRemovedFromEntity:当组件从其实体中移除时调用。 在这里进行所有清理。
|
||||
- onEntityPositionChanged:在实体位置更改时调用。 这使组件可以知道它们是由于父实体移动而移动的。
|
||||
- update:只要启用了实体和组件并且组件实现IUpdatable,就会每帧调用
|
||||
- onEnabled:在启用父实体或组件时调用
|
||||
- onDisabled:在禁用父实体或组件时调用
|
||||
```typescript
|
||||
import { Component, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 定义组件
|
||||
class PositionComponent extends Component {
|
||||
constructor(public x: number = 0, public y: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
## Debug
|
||||
Debug类提供日志记录。 Insist类提供各种断言条件。 您可以在整个代码中自由使用它们。
|
||||
class VelocityComponent extends Component {
|
||||
constructor(public dx: number = 0, public dy: number = 0) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
## Flags
|
||||
您是否喜欢将大量数据打包为单个number的功能,但讨厌处理该数据的语法? Flags类可以为您提供帮助。 它包括用于处理number的辅助方法,以检查是否设置了位以及设置/取消设置了它们。 处理Collider.physicsLayer非常方便。
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("Player");
|
||||
entity.addComponent(new PositionComponent(100, 100));
|
||||
entity.addComponent(new VelocityComponent(10, 0));
|
||||
```
|
||||
|
||||
### 示例地址
|
||||
### 3. 创建处理系统
|
||||
|
||||
#### [laya-demo](https://github.com/esengine/ecs-laya-demo)
|
||||
#### [egret-demo](https://github.com/esengine/ecs-egret-demo)
|
||||
```typescript
|
||||
import { EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
### 渲染集成框架
|
||||
#### [cocos-framework](https://github.com/esengine/cocos-framework)
|
||||
class MovementSystem extends EntitySystem {
|
||||
public process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx;
|
||||
position.y += velocity.dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## 扩展库
|
||||
// 添加系统到场景
|
||||
scene.addEntityProcessor(new MovementSystem());
|
||||
```
|
||||
|
||||
#### [基于ecs-framework开发的astar/BreadthFirst/Dijkstra/GOAP目标导向计划 路径寻找库](https://github.com/esengine/ecs-astar)
|
||||
#### [基于ecs-framework开发的AI(BehaviourTree、UtilityAI)系统](https://github.com/esengine/BehaviourTree-ai))
|
||||
### 4. 实体查询和管理
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用EntityManager进行高级查询
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 查询具有特定组件的实体
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll(PositionComponent, VelocityComponent)
|
||||
.execute();
|
||||
|
||||
// 查询带标签的实体
|
||||
const enemies = entityManager.getEntitiesByTag(1);
|
||||
|
||||
// 批量创建实体
|
||||
const bullets = entityManager.createEntities(100, "bullet");
|
||||
```
|
||||
|
||||
### 5. 事件系统
|
||||
|
||||
```typescript
|
||||
import { EventBus, ECSEventType, EventHandler } from '@esengine/ecs-framework';
|
||||
|
||||
// 获取事件总线
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log(`Entity created: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 使用装饰器自动注册事件监听器
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
onEntityDestroyed(data) {
|
||||
console.log('Entity destroyed:', data.entityName);
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义事件
|
||||
eventBus.emit('player:levelup', { playerId: 123, newLevel: 5 });
|
||||
```
|
||||
|
||||
## 🆚 框架对比
|
||||
|
||||
与其他 TypeScript ECS 框架相比,我们的优势:
|
||||
|
||||
| 特性 | @esengine/ecs-framework | bitecs | ecsy | Miniplex |
|
||||
|------|-------------------------|-------|------|----------|
|
||||
| **TypeScript 支持** | ✅ 原生支持 | ✅ 完整支持 | ⚠️ 部分支持 | ✅ 原生支持 |
|
||||
| **事件系统** | ✅ 类型安全+装饰器 | ❌ 无内置事件系统 | ⚠️ 基础事件 | ✅ 响应式事件 |
|
||||
| **查询系统** | ✅ 智能查询+流式API | ✅ 高性能 | ✅ 基础查询 | ✅ 响应式查询 |
|
||||
| **性能优化** | ✅ 多层优化系统 | ✅ WASM优化 | ⚠️ 基础优化 | ✅ React集成优化 |
|
||||
| **实体管理器** | ✅ 统一管理接口 | ❌ 无统一接口 | ✅ 基础管理 | ✅ 响应式管理 |
|
||||
| **组件索引** | ✅ 哈希+位图索引 | ✅ 原生支持 | ❌ 无索引系统 | ✅ 自动索引 |
|
||||
| **Archetype系统** | ✅ 内置支持 | ✅ 内置支持 | ❌ 无Archetype | ❌ 无Archetype |
|
||||
| **脏标记系统** | ✅ 细粒度追踪 | ⚠️ 基础支持 | ❌ 无脏标记 | ✅ React级追踪 |
|
||||
| **批量操作** | ✅ 全面的批量API | ✅ 批量支持 | ⚠️ 有限支持 | ⚠️ 有限支持 |
|
||||
| **游戏引擎集成** | ✅ 通用设计 | ✅ 通用设计 | ✅ 通用设计 | ⚠️ 主要针对React |
|
||||
| **学习曲线** | 🟢 中等 | 🟡 较陡峭 | 🟢 简单 | 🟡 需要React知识 |
|
||||
| **社区生态** | 🟡 成长中 | 🟢 活跃 | 🟡 稳定 | 🟡 小众但精品 |
|
||||
|
||||
### 为什么选择我们?
|
||||
|
||||
**相比 bitecs**:
|
||||
- 更友好的 TypeScript API,无需手动管理内存
|
||||
- 完整的实体管理器,开发体验更佳
|
||||
- 内置类型安全事件系统,bitecs需要自己实现
|
||||
- 多种索引系统可选,适应不同场景
|
||||
|
||||
**相比 ecsy**:
|
||||
- 现代化的性能优化系统(组件索引、Archetype、脏标记)
|
||||
- 更完整的 TypeScript 类型定义
|
||||
- 增强的事件系统,支持装饰器和异步事件
|
||||
- 活跃的维护和功能更新
|
||||
|
||||
**相比 Miniplex**:
|
||||
- 不依赖 React 生态,可用于任何游戏引擎
|
||||
- 专门针对游戏开发优化
|
||||
- 更轻量级的核心设计
|
||||
- 传统事件模式,更适合游戏开发习惯
|
||||
|
||||
## 📚 核心概念
|
||||
|
||||
### Entity(实体)
|
||||
实体是游戏世界中的基本对象,可以挂载组件和运行系统。
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
// 设置实体属性
|
||||
entity.tag = 1;
|
||||
entity.updateOrder = 0;
|
||||
entity.enabled = true;
|
||||
|
||||
// 批量创建实体
|
||||
const entities = scene.createEntities(100, "Enemy");
|
||||
```
|
||||
|
||||
### Component(组件)
|
||||
组件存储数据,定义实体的属性和状态。
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加组件到实体
|
||||
entity.addComponent(new HealthComponent());
|
||||
```
|
||||
|
||||
### System(系统)
|
||||
系统处理具有特定组件的实体集合,实现游戏逻辑。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth <= 0) {
|
||||
// 处理实体死亡逻辑
|
||||
entity.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
npm run test
|
||||
|
||||
# 性能基准测试
|
||||
npm run benchmark
|
||||
```
|
||||
|
||||
## 📖 文档
|
||||
|
||||
- [快速入门](docs/getting-started.md) - 从零开始学习框架使用
|
||||
- [EntityManager 使用指南](docs/entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [事件系统使用指南](docs/event-system-example.md) - 学习类型安全事件系统的完整用法
|
||||
- [性能优化指南](docs/performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](docs/core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](docs/query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
|
||||
## 🔗 扩展库
|
||||
|
||||
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、广度优先、Dijkstra、GOAP 算法
|
||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI 系统
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
### 开发环境设置
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
cd ecs-framework
|
||||
|
||||
# 运行基准测试
|
||||
node benchmark.js
|
||||
|
||||
# 开发构建 (在source目录)
|
||||
cd source && npm install && npm run build
|
||||
```
|
||||
|
||||
### 构建要求
|
||||
|
||||
- Node.js >= 14.0.0
|
||||
- TypeScript >= 4.0.0
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT](LICENSE) 许可证。
|
||||
|
||||
## 💬 交流群
|
||||
|
||||
加入 QQ 群讨论:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||
|
||||
### 🚀 核心性能指标
|
||||
|
||||
```bash
|
||||
实体创建: 640,000+ 个/秒
|
||||
组件查询: O(1) 复杂度(使用索引)
|
||||
内存优化: 30-50% 减少分配
|
||||
批量操作: 显著提升处理效率
|
||||
```
|
||||
|
||||
### 🎯 性能优化技术
|
||||
|
||||
- **组件索引系统**: 哈希和位图双重索引,支持 O(1) 查询
|
||||
- **Archetype 系统**: 按组件组合分组,减少查询开销
|
||||
- **脏标记机制**: 细粒度变更追踪,避免不必要的计算
|
||||
- **批量操作 API**: 减少函数调用开销,提升大规模操作效率
|
||||
- **智能缓存**: 查询结果缓存和延迟清理机制
|
||||
|
||||
### 🔧 性能建议
|
||||
|
||||
1. **大规模场景**: 使用批量API和组件索引
|
||||
2. **频繁查询**: 启用Archetype系统进行快速筛选
|
||||
3. **实时游戏**: 利用脏标记减少无效更新
|
||||
4. **移动端**: 建议实体数量控制在20,000以内
|
||||
|
||||
运行 `npm run benchmark` 查看在您的环境中的具体性能表现。
|
||||
|
||||
---
|
||||
|
||||
**ECS Framework** - 让游戏开发更简单、更高效!
|
||||
60
SECURITY.md
60
SECURITY.md
@@ -1,21 +1,53 @@
|
||||
# Security Policy
|
||||
# 安全政策
|
||||
|
||||
## Supported Versions
|
||||
## 支持的版本
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
我们为以下版本提供安全更新:
|
||||
|
||||
| Version | Supported |
|
||||
| 版本 | 支持状态 |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
| 2.0.x | :white_check_mark: |
|
||||
| 1.0.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
## 报告漏洞
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
如果您发现了安全漏洞,请通过以下方式报告:
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
### 报告渠道
|
||||
|
||||
- **邮箱**: [安全邮箱将在实际部署时提供]
|
||||
- **GitHub**: 创建私有安全报告(推荐)
|
||||
|
||||
### 报告流程
|
||||
|
||||
1. **不要**在公开的 issue 中报告安全漏洞
|
||||
2. 提供详细的漏洞描述,包括:
|
||||
- 受影响的版本
|
||||
- 复现步骤
|
||||
- 潜在的影响范围
|
||||
- 如果可能,提供修复建议
|
||||
|
||||
### 响应时间
|
||||
|
||||
- **确认收到**: 72小时内
|
||||
- **初步评估**: 1周内
|
||||
- **修复发布**: 根据严重程度,通常在2-4周内
|
||||
|
||||
### 处理流程
|
||||
|
||||
1. 我们会确认漏洞的存在和严重程度
|
||||
2. 开发修复方案并进行测试
|
||||
3. 发布安全更新
|
||||
4. 在修复发布后,会在相关渠道公布漏洞详情
|
||||
|
||||
### 安全最佳实践
|
||||
|
||||
使用 ECS Framework 时,请遵循以下安全建议:
|
||||
|
||||
- 始终使用最新的稳定版本
|
||||
- 定期更新依赖项
|
||||
- 在生产环境中禁用调试模式
|
||||
- 验证所有外部输入数据
|
||||
- 不要在客户端存储敏感信息
|
||||
|
||||
感谢您帮助保持 ECS Framework 的安全性!
|
||||
|
||||
Submodule demo/egret_demo deleted from 7fd9460053
Submodule demo/laya_demo deleted from e4273ff0f5
@@ -1,70 +0,0 @@
|
||||
# Collision
|
||||
碰撞检测在大多数游戏中都很常见。框架内使用了一些更先进的碰撞/重叠检查方法,如Minkowski、分离轴定理和古老的三角法
|
||||
|
||||
## 线与线相交 [lineToLine]
|
||||
- 返回是否相交
|
||||
```typescript
|
||||
const a1 = new es.Vector2(0, 0);
|
||||
const a2 = new es.Vector2(100, 100);
|
||||
const b1 = new es.Vector2(-100, 0);
|
||||
const b2 = new es.Vector2(100, 200);
|
||||
const result = es.Collisions.lineToLine(a1, a2, b1, b2);
|
||||
```
|
||||
- 返回是否相交并获得相交的点
|
||||
```typescript
|
||||
const a1 = new es.Vector2(0, 0);
|
||||
const a2 = new es.Vector2(100, 100);
|
||||
const b1 = new es.Vector2(-100, 0);
|
||||
const b2 = new es.Vector2(100, 200);
|
||||
// 相交的点坐标
|
||||
const intersection = new es.Vector2();
|
||||
const result = es.Collisions.lineToLineIntersection(a1, a2, b1, b2, intersection);
|
||||
```
|
||||
|
||||
## 圆和圆相交 [circleToCircle]
|
||||
```typescript
|
||||
const center1 = new es.Vector2(0, 0);
|
||||
const radius1 = 50;
|
||||
const center2 = new es.Vector2(30, 30);
|
||||
const radius2 = 50;
|
||||
const result = es.Collisions.circleToCircle(center1, radius1, center2, radius2);
|
||||
```
|
||||
|
||||
## 圆和线相交 [circleToLine]
|
||||
```typescript
|
||||
const center1 = new es.Vector2(0, 0);
|
||||
const radius1 = 50;
|
||||
const a1 = new es.Vector2(0, 0);
|
||||
const a2 = new es.Vector2(100, 100);
|
||||
const result = es.Collisions.circleToLine(center1, radius1, a1, a2);
|
||||
```
|
||||
|
||||
## 点是否在圆内 [circleToPoint]
|
||||
```typescript
|
||||
const center1 = new es.Vector2(0, 0);
|
||||
const radius1 = 50;
|
||||
const point = new es.Vector2(0, 0);
|
||||
const result = es.Collisions.circleToPoint(center1, radius1, point);
|
||||
```
|
||||
|
||||
## 圆是否和矩形相交 [rectToCircle]
|
||||
```typescript
|
||||
const rect = new es.Rectangle(0, 0, 100, 100);
|
||||
const center = new es.Vector2(30, 30);
|
||||
const radius = 50;
|
||||
const result = es.Collisions.rectToCircle(rect, center, radius);
|
||||
```
|
||||
|
||||
## 矩形与线是否相交 [rectToLine]
|
||||
```typescript
|
||||
const rect = new es.Rectangle(0, 0, 100, 100);
|
||||
const a1 = new es.Vector2(0, 0);
|
||||
const a2 = new es.Vector2(100, 100);
|
||||
const result = es.Collisions.rectToLine(rect, a1, a2);
|
||||
```
|
||||
|
||||
## 点是否在矩形内 [rectToPoint]
|
||||
```typescript
|
||||
const point = new es.Vector2(100, 100);
|
||||
const result = es.Collisions.rectToPoint(0, 0, 100, 100, point);
|
||||
```
|
||||
622
docs/core-concepts.md
Normal file
622
docs/core-concepts.md
Normal file
@@ -0,0 +1,622 @@
|
||||
# 核心概念
|
||||
|
||||
ECS Framework 基于 Entity-Component-System 架构模式,这是一种高度模块化和可扩展的游戏开发架构。本文档将详细介绍框架的核心概念。
|
||||
|
||||
## ECS 架构概述
|
||||
|
||||
ECS 架构将传统的面向对象设计分解为三个核心部分:
|
||||
|
||||
- **Entity(实体)** - 游戏世界中的对象,包含基本属性如位置、旋转、缩放
|
||||
- **Component(组件)** - 包含数据和行为的功能模块
|
||||
- **System(系统)** - 处理实体集合的逻辑处理单元
|
||||
|
||||
## Core(核心)
|
||||
|
||||
Core 是框架的核心管理类,负责游戏的生命周期管理。
|
||||
|
||||
### 创建和配置
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建核心实例(调试模式)
|
||||
const core = Core.create(true);
|
||||
|
||||
// 创建核心实例(发布模式)
|
||||
const core = Core.create(false);
|
||||
```
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听核心事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
|
||||
|
||||
// 发送帧更新事件
|
||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
||||
|
||||
// 发送自定义事件
|
||||
Core.emitter.emit("customEvent", { data: "value" });
|
||||
```
|
||||
|
||||
### 定时器系统
|
||||
|
||||
```typescript
|
||||
// 延迟执行
|
||||
Core.schedule(2.0, false, this, (timer) => {
|
||||
console.log("2秒后执行");
|
||||
});
|
||||
|
||||
// 重复执行
|
||||
Core.schedule(1.0, true, this, (timer) => {
|
||||
console.log("每秒执行一次");
|
||||
});
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是游戏世界的容器,管理实体和系统的生命周期。
|
||||
|
||||
### 创建和使用场景
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景
|
||||
const scene = new Scene();
|
||||
scene.name = "GameScene";
|
||||
|
||||
// 设置为当前场景
|
||||
Core.scene = scene;
|
||||
|
||||
// 场景生命周期
|
||||
scene.begin(); // 开始场景
|
||||
scene.update(); // 更新场景
|
||||
scene.end(); // 结束场景
|
||||
```
|
||||
|
||||
### 批量实体管理
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量添加实体(延迟缓存清理)
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理缓存
|
||||
|
||||
// 获取性能统计
|
||||
const stats = scene.getPerformanceStats();
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
```
|
||||
|
||||
## Entity(实体)
|
||||
|
||||
实体是游戏世界中的基本对象,包含位置、旋转、缩放等基本属性。
|
||||
|
||||
### 实体的基本属性
|
||||
|
||||
```typescript
|
||||
import { Vector2 } from '@esengine/ecs-framework';
|
||||
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 位置
|
||||
entity.position = new Vector2(100, 200);
|
||||
entity.position = entity.position.add(new Vector2(10, 0));
|
||||
|
||||
// 旋转(弧度)
|
||||
entity.rotation = Math.PI / 4;
|
||||
|
||||
// 缩放
|
||||
entity.scale = new Vector2(2, 2);
|
||||
|
||||
// 标签(用于分类)
|
||||
entity.tag = 1;
|
||||
|
||||
// 启用状态
|
||||
entity.enabled = true;
|
||||
|
||||
// 活跃状态
|
||||
entity.active = true;
|
||||
|
||||
// 更新顺序
|
||||
entity.updateOrder = 10;
|
||||
```
|
||||
|
||||
### 实体层级关系
|
||||
|
||||
```typescript
|
||||
// 添加子实体
|
||||
const parent = scene.createEntity("Parent");
|
||||
const child = scene.createEntity("Child");
|
||||
parent.addChild(child);
|
||||
|
||||
// 获取父实体
|
||||
const parentEntity = child.parent;
|
||||
|
||||
// 获取所有子实体
|
||||
const children = parent.children;
|
||||
|
||||
// 查找子实体
|
||||
const foundChild = parent.findChild("Child");
|
||||
|
||||
// 按标签查找子实体
|
||||
const taggedChildren = parent.findChildrenByTag(1);
|
||||
|
||||
// 移除子实体
|
||||
parent.removeChild(child);
|
||||
|
||||
// 移除所有子实体
|
||||
parent.removeAllChildren();
|
||||
```
|
||||
|
||||
### 实体生命周期
|
||||
|
||||
```typescript
|
||||
// 检查实体是否被销毁
|
||||
if (!entity.isDestroyed) {
|
||||
// 实体仍然有效
|
||||
}
|
||||
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 获取实体调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log(debugInfo);
|
||||
```
|
||||
|
||||
## Component(组件)
|
||||
|
||||
组件包含数据和行为,定义了实体的特性和能力。
|
||||
|
||||
### 创建组件
|
||||
|
||||
```typescript
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
public takeDamage(damage: number) {
|
||||
this.currentHealth -= damage;
|
||||
if (this.currentHealth <= 0) {
|
||||
this.entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public heal(amount: number) {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件生命周期
|
||||
|
||||
```typescript
|
||||
class MyComponent extends Component {
|
||||
public onAddedToEntity() {
|
||||
// 组件被添加到实体时调用
|
||||
console.log("组件已添加到实体:", this.entity.name);
|
||||
}
|
||||
|
||||
public onRemovedFromEntity() {
|
||||
// 组件从实体移除时调用
|
||||
console.log("组件已从实体移除");
|
||||
}
|
||||
|
||||
public onEnabled() {
|
||||
// 组件启用时调用
|
||||
console.log("组件已启用");
|
||||
}
|
||||
|
||||
public onDisabled() {
|
||||
// 组件禁用时调用
|
||||
console.log("组件已禁用");
|
||||
}
|
||||
|
||||
public update() {
|
||||
// 每帧更新(如果组件启用)
|
||||
console.log("组件更新");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 组件管理
|
||||
|
||||
```typescript
|
||||
// 添加组件
|
||||
const health = entity.addComponent(new HealthComponent());
|
||||
|
||||
// 创建并添加组件
|
||||
const movement = entity.createComponent(MovementComponent, 200); // 传递构造参数
|
||||
|
||||
// 获取组件
|
||||
const healthComp = entity.getComponent(HealthComponent);
|
||||
|
||||
// 检查组件是否存在
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 处理逻辑
|
||||
}
|
||||
|
||||
// 获取或创建组件
|
||||
const weapon = entity.getOrCreateComponent(WeaponComponent);
|
||||
|
||||
// 获取多个同类型组件
|
||||
const allHealthComps = entity.getComponents(HealthComponent);
|
||||
|
||||
// 移除组件
|
||||
entity.removeComponent(healthComp);
|
||||
|
||||
// 按类型移除组件
|
||||
entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
### 组件对象池优化
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
class BulletComponent extends Component {
|
||||
public damage: number = 10;
|
||||
public speed: number = 300;
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.damage = 10;
|
||||
this.speed = 300;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册组件池
|
||||
ComponentPoolManager.getInstance().registerPool(BulletComponent, 1000);
|
||||
|
||||
// 使用对象池获取组件
|
||||
const bullet = ComponentPoolManager.getInstance().getComponent(BulletComponent);
|
||||
entity.addComponent(bullet);
|
||||
|
||||
// 释放组件回对象池
|
||||
ComponentPoolManager.getInstance().releaseComponent(bullet);
|
||||
|
||||
// 预热组件池
|
||||
ComponentPoolManager.getInstance().preWarmPools({
|
||||
BulletComponent: 1000,
|
||||
EffectComponent: 500
|
||||
});
|
||||
|
||||
// 获取池统计
|
||||
const stats = ComponentPoolManager.getInstance().getPoolStats();
|
||||
console.log('组件池统计:', stats);
|
||||
```
|
||||
|
||||
## Scene(场景)
|
||||
|
||||
场景是实体和系统的容器,管理游戏世界的状态。
|
||||
|
||||
### 场景生命周期
|
||||
|
||||
```typescript
|
||||
class GameScene extends es.Scene {
|
||||
public initialize() {
|
||||
// 场景初始化,创建实体和系统
|
||||
this.setupEntities();
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
public onStart() {
|
||||
// 场景开始运行时调用
|
||||
console.log("场景开始");
|
||||
}
|
||||
|
||||
public unload() {
|
||||
// 场景卸载时调用
|
||||
console.log("场景卸载");
|
||||
}
|
||||
|
||||
private setupEntities() {
|
||||
const player = this.createEntity("Player");
|
||||
player.addComponent(new PlayerComponent());
|
||||
}
|
||||
|
||||
private setupSystems() {
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体管理
|
||||
|
||||
```typescript
|
||||
// 创建实体
|
||||
const entity = scene.createEntity("MyEntity");
|
||||
|
||||
// 添加现有实体
|
||||
scene.addEntity(entity);
|
||||
|
||||
// 查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const entityById = scene.findEntityById(123);
|
||||
const entitiesByTag = scene.findEntitiesByTag(1);
|
||||
|
||||
// 销毁所有实体
|
||||
scene.destroyAllEntities();
|
||||
|
||||
// 获取场景统计信息
|
||||
const stats = scene.getStats();
|
||||
console.log("实体数量:", stats.entityCount);
|
||||
console.log("系统数量:", stats.processorCount);
|
||||
```
|
||||
|
||||
## System(系统)
|
||||
|
||||
系统处理实体集合,实现游戏的核心逻辑。
|
||||
|
||||
### EntitySystem
|
||||
|
||||
最常用的系统类型,处理实体集合:
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends es.EntitySystem {
|
||||
protected process(entities: es.Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const movement = entity.getComponent(MovementComponent);
|
||||
if (movement) {
|
||||
movement.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem
|
||||
|
||||
定期处理的系统:
|
||||
|
||||
```typescript
|
||||
class HealthRegenerationSystem extends es.ProcessingSystem {
|
||||
protected process(entities: es.Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health && health.currentHealth < health.maxHealth) {
|
||||
health.currentHealth += 10 * es.Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem
|
||||
|
||||
按时间间隔执行的系统:
|
||||
|
||||
```typescript
|
||||
class SpawnSystem extends es.IntervalSystem {
|
||||
constructor() {
|
||||
super(3.0); // 每3秒执行一次
|
||||
}
|
||||
|
||||
protected processSystem() {
|
||||
// 生成敌人
|
||||
const enemy = this.scene.createEntity("Enemy");
|
||||
enemy.addComponent(new EnemyComponent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem
|
||||
|
||||
被动系统,不自动处理实体:
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends es.PassiveSystem {
|
||||
public checkCollisions() {
|
||||
// 手动调用的碰撞检测逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Time(时间)
|
||||
|
||||
时间管理工具类,提供游戏时间相关功能:
|
||||
|
||||
```typescript
|
||||
// 获取时间信息
|
||||
console.log("帧时间:", es.Time.deltaTime);
|
||||
console.log("总时间:", es.Time.totalTime);
|
||||
console.log("帧数:", es.Time.frameCount);
|
||||
console.log("时间缩放:", es.Time.timeScale);
|
||||
|
||||
// 设置时间缩放(慢动作效果)
|
||||
es.Time.timeScale = 0.5;
|
||||
|
||||
// 检查时间间隔
|
||||
if (es.Time.checkEvery(1.0, lastCheckTime)) {
|
||||
// 每秒执行一次
|
||||
}
|
||||
```
|
||||
|
||||
## Vector2(二维向量)
|
||||
|
||||
二维向量类,提供数学运算:
|
||||
|
||||
```typescript
|
||||
// 创建向量
|
||||
const vec1 = new es.Vector2(10, 20);
|
||||
const vec2 = es.Vector2.zero;
|
||||
const vec3 = es.Vector2.one;
|
||||
|
||||
// 向量运算
|
||||
const sum = vec1.add(vec2);
|
||||
const diff = vec1.subtract(vec2);
|
||||
const scaled = vec1.multiply(2);
|
||||
const normalized = vec1.normalize();
|
||||
|
||||
// 向量属性
|
||||
console.log("长度:", vec1.length);
|
||||
console.log("长度平方:", vec1.lengthSquared);
|
||||
|
||||
// 静态方法
|
||||
const distance = es.Vector2.distance(vec1, vec2);
|
||||
const lerped = es.Vector2.lerp(vec1, vec2, 0.5);
|
||||
const fromAngle = es.Vector2.fromAngle(Math.PI / 4);
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
框架内置性能监控工具:
|
||||
|
||||
```typescript
|
||||
// 获取性能监控实例
|
||||
const monitor = es.PerformanceMonitor.instance;
|
||||
|
||||
// 查看性能数据
|
||||
console.log("平均FPS:", monitor.averageFPS);
|
||||
console.log("最小FPS:", monitor.minFPS);
|
||||
console.log("最大FPS:", monitor.maxFPS);
|
||||
console.log("内存使用:", monitor.memoryUsage);
|
||||
|
||||
// 重置性能数据
|
||||
monitor.reset();
|
||||
```
|
||||
|
||||
## 对象池
|
||||
|
||||
内存管理优化工具:
|
||||
|
||||
```typescript
|
||||
// 创建对象池
|
||||
class BulletPool extends es.Pool<Bullet> {
|
||||
protected createObject(): Bullet {
|
||||
return new Bullet();
|
||||
}
|
||||
}
|
||||
|
||||
const bulletPool = new BulletPool();
|
||||
|
||||
// 使用对象池
|
||||
const bullet = bulletPool.obtain();
|
||||
// 使用bullet...
|
||||
bulletPool.free(bullet);
|
||||
|
||||
// 清空对象池
|
||||
bulletPool.clear();
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 实体设计
|
||||
|
||||
- 实体只包含基本属性,功能通过组件实现
|
||||
- 合理使用实体层级关系
|
||||
- 及时销毁不需要的实体
|
||||
|
||||
### 2. 组件设计
|
||||
|
||||
- 组件保持单一职责
|
||||
- 使用生命周期方法进行初始化和清理
|
||||
- 避免组件间直接依赖
|
||||
|
||||
### 3. 系统设计
|
||||
|
||||
- 系统专注于特定逻辑处理
|
||||
- 合理设置系统更新顺序
|
||||
- 使用被动系统处理特殊逻辑
|
||||
|
||||
### 4. 性能优化
|
||||
|
||||
- 使用对象池减少内存分配
|
||||
- 监控性能数据
|
||||
- 合理使用时间缩放
|
||||
|
||||
## 高级性能优化功能
|
||||
|
||||
### 位掩码优化器
|
||||
|
||||
位掩码优化器可以预计算和缓存常用的组件掩码,提升查询性能。
|
||||
|
||||
```typescript
|
||||
import { BitMaskOptimizer } from '@esengine/ecs-framework';
|
||||
|
||||
const optimizer = BitMaskOptimizer.getInstance();
|
||||
|
||||
// 注册组件类型
|
||||
optimizer.registerComponentType(PositionComponent);
|
||||
optimizer.registerComponentType(VelocityComponent);
|
||||
optimizer.registerComponentType(RenderComponent);
|
||||
|
||||
// 预计算常用掩码组合
|
||||
optimizer.precomputeCommonMasks();
|
||||
|
||||
// 获取优化的掩码
|
||||
const positionMask = optimizer.getComponentMask(PositionComponent);
|
||||
const movementMask = optimizer.getCombinedMask([PositionComponent, VelocityComponent]);
|
||||
|
||||
// 掩码操作
|
||||
const hasBothComponents = optimizer.hasAllComponents(entityMask, movementMask);
|
||||
const hasAnyComponent = optimizer.hasAnyComponent(entityMask, movementMask);
|
||||
|
||||
// 获取掩码分析
|
||||
const analysis = optimizer.analyzeMask(entityMask);
|
||||
console.log('掩码包含的组件类型:', analysis.componentTypes);
|
||||
```
|
||||
|
||||
### 延迟索引更新器
|
||||
|
||||
批量更新索引可以显著提升大规模实体操作的性能。
|
||||
|
||||
```typescript
|
||||
import { IndexUpdateBatcher } from '@esengine/ecs-framework';
|
||||
|
||||
const batcher = new IndexUpdateBatcher((updates) => {
|
||||
// 处理批量更新
|
||||
console.log(`批量处理 ${updates.length} 个索引更新`);
|
||||
});
|
||||
|
||||
// 配置批量大小和延迟
|
||||
batcher.configure(100, 16); // 批量大小100,延迟16ms
|
||||
|
||||
// 添加更新任务
|
||||
batcher.addUpdate("add", entity, componentMask);
|
||||
batcher.addUpdate("remove", entity, componentMask);
|
||||
|
||||
// 强制刷新
|
||||
batcher.flush();
|
||||
```
|
||||
|
||||
### 批量操作API
|
||||
|
||||
```typescript
|
||||
// 批量创建实体 - 最高性能
|
||||
const entities = scene.createEntities(10000, "Bullets");
|
||||
|
||||
// 延迟缓存清理
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理
|
||||
|
||||
// 批量查询优化
|
||||
const movingEntities = scene.getEntitiesWithComponents([PositionComponent, VelocityComponent]);
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS Framework 提供了完整的实体组件系统架构:
|
||||
|
||||
- **Core** 管理游戏生命周期和全局功能
|
||||
- **Entity** 作为游戏对象的基础容器
|
||||
- **Component** 实现具体的功能模块,支持对象池优化
|
||||
- **System** 处理游戏逻辑
|
||||
- **Scene** 管理游戏世界状态,支持批量操作
|
||||
- **高级优化** 位掩码优化器、组件对象池、批量操作等
|
||||
|
||||
通过合理使用这些核心概念和优化功能,可以构建出高性能、结构清晰、易于维护的游戏代码。
|
||||
@@ -1,189 +0,0 @@
|
||||
# Coroutine
|
||||
## 协程介绍
|
||||
框架的协程系统是基于js的一个简单而强大的迭代器。这一点你不必关注太多,我们直接进入一个简单的例子来看看协程到底能干什么。首先,我们来看一下这段简单的代码
|
||||
|
||||
### 倒计时器
|
||||
这是一个简单的脚本组件,只做了倒计时,并且在到达0的时候log一个信息。
|
||||
```typescript
|
||||
export class AComponent extends es.Component implements es.IUpdatable {
|
||||
public timer = 3;
|
||||
update() {
|
||||
this.timer -= es.Time.deltaTime;
|
||||
if(this.timer <= 0) {
|
||||
console.Log("Timer has finished!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
还不错,代码简短实用,但问题是,如果我们需要复杂的脚本组件(像一个角色或者敌人的类),拥有多个计时器呢?刚开始的时候,我们的代码也许会是这样的:
|
||||
|
||||
```typescript
|
||||
export class AComponent extends es.Component implements es.IUpdatable
|
||||
{
|
||||
public firstTimer = 3;
|
||||
public secondTimer = 2;
|
||||
public thirdTimer = 1;
|
||||
update() {
|
||||
this.firstTimer -= es.Time.deltaTime;
|
||||
if(this.firstTimer <= 0)
|
||||
console.Log("First timer has finished!");
|
||||
this.secondTimer -= es.Time.deltaTime;
|
||||
if(this.secondTimer <= 0)
|
||||
console.Log("Second timer has finished!");
|
||||
this.thirdTimer -= es.Time.deltaTime;
|
||||
if(this.thirdTimer <= 0)
|
||||
console.Log("Third timer has finished!");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
尽管不是太糟糕,但是我个人不是很喜欢自己的代码中充斥着这些计时器变量,它们看上去很乱,而且当我需要重新开始计时的时候还得记得去重置它们(这活我经常忘记做)。
|
||||
|
||||
|
||||
|
||||
如果我只用一个for循环来做这些,看上去是否会好很多?
|
||||
|
||||
```typescript
|
||||
for(let timer = 3; timer >= 0; timer -= es.Time.deltaTime) {
|
||||
//Just do nothing...
|
||||
}
|
||||
console.Log("This happens after 5 seconds!");
|
||||
```
|
||||
|
||||
现在每一个计时器变量都成为for循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。
|
||||
|
||||
|
||||
|
||||
好的,你可能现在明白我的意思:协程可以做的正是这一点!
|
||||
|
||||
## 码入你的协程!
|
||||
|
||||
现在,这里提供了上面例子运用协程的版本!我建议你从这里开始跟着我来写一个简单的脚本组件,这样你可以在你自己的程序中看到它是如何工作的。
|
||||
|
||||
```typescript
|
||||
export class AComponent extends es.Component implements es.IUpdatable
|
||||
{
|
||||
onAddedToEntity() {
|
||||
es.Core.startCoroutine(this.countdown());
|
||||
}
|
||||
|
||||
*countdown() {
|
||||
for(let timer = 3; timer >= 0; timer -= es.Time.deltaTime)
|
||||
yield null;
|
||||
console.Log("This message appears after 3 seconds!");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这看上去有点不一样,没关系,接下来我会解释这里到底发生了什么。
|
||||
|
||||
```typescript
|
||||
es.Core.startCoroutine(this.countdown());
|
||||
```
|
||||
|
||||
这一行用来开始我们的countdown程序,注意,我并没有给它传入参数,但是这个方法调用了它自己(这是通过传递countdown的yield返回值来实现的)。
|
||||
|
||||
### Yield
|
||||
|
||||
为了能在连续的多帧中(在这个例子中,3秒钟等同于很多帧)调用该方法,框架必须通过某种方式来存储这个方法的状态,这是通过迭代器中使用yield语句得到的返回值,当你`yield`一个方法时,你相当于说了,**现在停止这个方法,然后在下一帧中从这里重新开始!**。
|
||||
|
||||
> 注意:用0或者null来yield的意思是告诉协程等待下一帧,直到继续执行为止。当然,同样的你可以继续yield其他协程,我会在下一个教程中讲到这些。
|
||||
|
||||
## 一些例子
|
||||
|
||||
协程在刚开始接触的时候是非常难以理解的,无论是新手还是经验丰富的程序员我都见过他们对于协程语句一筹莫展的时候。因此我认为通过例子来理解它是最好的方法,这里有一些简单的协程例子:
|
||||
|
||||
### 多次输出Hello
|
||||
|
||||
记住,yield是 **停止执行方法,并且在下一帧从这里重新开始**,这意味着你可以这样做:
|
||||
|
||||
```typescript
|
||||
//这将打招呼 5 次,每帧一次,持续 5 帧
|
||||
*sayHelloFiveTimes() {
|
||||
yield null;
|
||||
console.Log("Hello");
|
||||
yield null;
|
||||
console.Log("Hello");
|
||||
yield null;
|
||||
console.Log("Hello");
|
||||
yield null;
|
||||
console.Log("Hello");
|
||||
yield null;
|
||||
console.Log("Hello");
|
||||
}
|
||||
//这将做与上述功能完全相同的事情!
|
||||
*sayHello5Times() {
|
||||
for(let i = 0; i < 5; i++) {
|
||||
console.Log("Hello");
|
||||
yield null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 每一帧输出“Hello”,无限循环。。。
|
||||
|
||||
通过在一个while循环中使用yield,你可以得到一个无限循环的协程,这几乎就跟一个update()循环等同
|
||||
|
||||
```typescript
|
||||
//一旦启动,这将一直运行直到手动停止
|
||||
*sayHelloEveryFrame(){
|
||||
while(true) {
|
||||
console.Log("Hello");
|
||||
yield null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 计时
|
||||
不过跟update()不一样的是,你可以在协程中做一些更有趣的事
|
||||
|
||||
```typescript
|
||||
*countSeconds(){
|
||||
let seconds = 0;
|
||||
while(true)
|
||||
{
|
||||
// 1秒后执行下一帧
|
||||
yield 1;
|
||||
seconds++;
|
||||
console.Log("自协程启动以来已经过去了" + seconds + "秒钟.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个方法突出了协程一个非常酷的地方:方法的状态被存储了,这使得方法中定义的这些变量都会保存它们的值,即使是在不同的帧中。还记得这个教程开始时那些烦人的计时器变量吗?通过协程,我们再也不需要担心它们了,只需要把变量直接放到方法里面!
|
||||
|
||||
### 开始和终止协程
|
||||
|
||||
之前,我们已经学过了通过 es.Core.startCoroutine()方法来开始一个协程,就像这样:
|
||||
|
||||
```typescript
|
||||
const coroutine = es.Core.startCoroutine(this.countdown());
|
||||
```
|
||||
|
||||
我们可以像这样停止协程
|
||||
|
||||
```typescript
|
||||
coroutine.stop();
|
||||
```
|
||||
|
||||
或者你可以再迭代器内返回`yield "break"`方式中止协程
|
||||
|
||||
```typescript
|
||||
*countSeconds(){
|
||||
let seconds = 0;
|
||||
while(true)
|
||||
{
|
||||
for(let timer = 0; timer < 1; timer += es.Time.deltaTime)
|
||||
yield null;
|
||||
seconds++;
|
||||
console.Log("自协程启动以来已经过去了" + seconds + "秒钟.");
|
||||
|
||||
// 如果大于10秒,终止协程
|
||||
if (second > 10)
|
||||
yield "break";
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,121 +0,0 @@
|
||||
# 创建实体
|
||||
|
||||
实体必须依赖于场景,不能单独存在。创建实体方法由场景提供。
|
||||
|
||||
## 方式一
|
||||
```typescript
|
||||
// 通过全局快捷获取场景创建实体
|
||||
const playerEntity = es.Core.scene.createEntity("player");
|
||||
```
|
||||
|
||||
## 方式二
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
// 通过场景内创建
|
||||
const playerEntity = this.createEntity("player");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transform
|
||||
|
||||
框架中提供的实体不同于其他框架实体,它更偏向于游戏使用,实体内含有`Transform`属性。可用于快速访问位置,旋转,缩放等。如果需要应用于游戏引擎,请再组件重写`onTransformChanged`监听这些属性的变化。
|
||||
|
||||
> 实体内包含对transform里位置、旋转、缩放的快捷tween方法。`tweenPositionTo`/`tweenLocalPositionTo`/`tweenScaleTo`/`tweenLocalScaleTo`/`tweenRotationDegreesTo`/`tweenLocalRotationDegreesTo`
|
||||
|
||||
### tag / setTag
|
||||
|
||||
实体还提供`tag`属性及`setTag`方法来快速设置实体的标记,可再场景中使用`findEntitiesWithTag`快速查询拥有该标记的实体或使用`findEntityWithTag`来查找第一个拥有该标记的实体,你可以把它当作组来使用。
|
||||
|
||||
### detachFromScene / attachToScene
|
||||
当你不想实体与场景被销毁时一同被销毁。可先 `detachFromScene`,等待合适的时机再调用 `attachToScene` 放入新的场景。
|
||||
|
||||
|
||||
# 创建组件
|
||||
|
||||
组件一般配合实体使用。组件需要继承 `es.Component` 来标识为组件,如果想让组件拥有每帧更新能力则额外继承`es.IUpdatable` 接口。在实现的`update`方法当中进行更新逻辑。
|
||||
|
||||
```typescript
|
||||
// es.IUpdatable接口为可选接口,如果不需要更新能力则不必继承
|
||||
export class AComponent extends es.Component implements es.IUpdatable {
|
||||
update() {
|
||||
// 更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 加入组件
|
||||
|
||||
组件必须挂载于实体上,不能单独存在,如果需要单独于场景的组件则参考 [es.SceneComponent](scene_component.md) 组件。
|
||||
|
||||
- 方式一:将现有的AComponent加入实体
|
||||
```typescript
|
||||
const aCom = playerEntity.addComponent(new AComponent());
|
||||
```
|
||||
|
||||
- 方式二:在实体上直接创建组件
|
||||
```typescript
|
||||
const aCom = playerEntity.createComponent(AComponent);
|
||||
```
|
||||
|
||||
## 获取组件
|
||||
|
||||
- 方式一: 根据类型获取找到满足条件的第一个组件
|
||||
```typescript
|
||||
// 不能保证已经加入场景
|
||||
const aCom = playerEntity.getComponent(AComponent);
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 保证已经加入场景
|
||||
const aCom = playerEntity.getComponentInScene(AComponent);
|
||||
```
|
||||
|
||||
- 方式二: 尝试找到一个组件,返回是否找到组件标志,第二参数需要一个引用组件用于存储已找到的组件
|
||||
```typescript
|
||||
const outCom = new Ref<AComponent>();
|
||||
const find = playerEntity.tryGetComponent(AComponent, outCom);
|
||||
if (find) {
|
||||
const aCom = outCom.value;
|
||||
}
|
||||
```
|
||||
|
||||
- 方式三:获取该类型的组件,如果未找到则创建一个并返回
|
||||
```typescript
|
||||
const aCom = playerEntity.getOrCreateComponent(AComponent);
|
||||
```
|
||||
|
||||
- 方式四:根据第二参数中的列表找到该类型的所有组件并返回
|
||||
```typescript
|
||||
const findArray: Component[] = [
|
||||
new AComponent(),
|
||||
new BComponent(),
|
||||
new CComponent()
|
||||
];
|
||||
// findArray可不传,则在实体上寻找满足第一个条件的所有组件
|
||||
const coms = playerEntity.getComponents(AComponent, findArray);
|
||||
```
|
||||
|
||||
- 组件是否存在
|
||||
|
||||
```typescript
|
||||
const find = playerEntity.hasComponent(AComponent);
|
||||
```
|
||||
|
||||
## 移除组件
|
||||
|
||||
- 方式一: 移除已实例组件
|
||||
```typescript
|
||||
playerEntity.removeComponent(aCom);
|
||||
```
|
||||
|
||||
- 方式二:移除满足类型的第一个组件
|
||||
```typescript
|
||||
playerEntity.removeComponentForType(AComponent);
|
||||
```
|
||||
|
||||
- 方式三: 移除所有组件
|
||||
```typescript
|
||||
playerEntity.removeAllComponents();
|
||||
```
|
||||
@@ -1,39 +0,0 @@
|
||||
# Emitter
|
||||
Core提供了一个在某些关键时刻触发事件的发射器。 通过Core.emitter.addObserver和Core.emitter.removeObserver进行访问。 CoreEvents枚举定义了所有可用事件。
|
||||
|
||||
发射器类也可以在自己的类中使用。 您可以通过number,enum或任何结构键输入事件。
|
||||
|
||||
## 自定义事件发生器
|
||||
|
||||
- string为key的事件发生器
|
||||
```typescript
|
||||
export enum CustomEvent {
|
||||
enum1,
|
||||
enum2
|
||||
}
|
||||
|
||||
export class MainScene extends es.Scene {
|
||||
// string为key的事件发生器
|
||||
private str_emitter = new es.Emitter<string>();
|
||||
// number为key的事件发生器
|
||||
private num_emitter = new es.Emitter<number>();
|
||||
// enum为key的事件发生器
|
||||
private custom_emitter = new es.Emitter<CustomEvent>();
|
||||
|
||||
onStart() {
|
||||
// 监听触发器
|
||||
this.str_emitter.addObserver("test", this.onStrEmit, this);
|
||||
|
||||
// 触发监听器
|
||||
this.str_emitter.emit("test");
|
||||
|
||||
// 移除事件触发器
|
||||
this.str_emitter.removeObserver("test", this.onStrEmit);
|
||||
}
|
||||
|
||||
// args为emit传入的参数。不传则为空
|
||||
onStrEmit(...args: any[]) {
|
||||
console.log("test");
|
||||
}
|
||||
}
|
||||
```
|
||||
482
docs/entity-guide.md
Normal file
482
docs/entity-guide.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# 实体使用指南
|
||||
|
||||
本指南详细介绍 ECS Framework 中实体(Entity)的所有功能和使用方法。
|
||||
|
||||
## 实体概述
|
||||
|
||||
实体(Entity)是 ECS 架构中的核心概念之一,它作为组件的容器存在。实体本身不包含游戏逻辑,所有功能都通过添加不同的组件来实现。
|
||||
|
||||
### 实体的特点
|
||||
|
||||
- **轻量级容器**:实体只是组件的载体,不包含具体的游戏逻辑
|
||||
- **唯一标识**:每个实体都有唯一的ID和名称
|
||||
- **层次结构**:支持父子关系,可以构建复杂的实体层次
|
||||
- **高性能查询**:基于位掩码的组件查询系统
|
||||
- **生命周期管理**:完整的创建、更新、销毁流程
|
||||
|
||||
## 创建实体
|
||||
|
||||
### 基本创建方式
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通过场景创建实体
|
||||
const scene = new Scene();
|
||||
const entity = scene.createEntity("Player");
|
||||
|
||||
console.log(entity.name); // "Player"
|
||||
console.log(entity.id); // 唯一的数字ID
|
||||
```
|
||||
|
||||
### 批量创建实体(推荐)
|
||||
|
||||
```typescript
|
||||
import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
const scene = new Scene();
|
||||
|
||||
// 批量创建1000个实体 - 高性能
|
||||
const entities = scene.createEntities(1000, "Enemy");
|
||||
|
||||
// 批量配置
|
||||
entities.forEach((entity, index) => {
|
||||
entity.tag = 2; // 敌人标签
|
||||
// 添加组件...
|
||||
});
|
||||
```
|
||||
|
||||
### 使用流式API创建
|
||||
|
||||
```typescript
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
// 使用ECS流式API
|
||||
const entity = Core.ecsAPI
|
||||
?.entity("Enemy")
|
||||
.withComponent(new PositionComponent(100, 200))
|
||||
.withComponent(new HealthComponent(50))
|
||||
.withTag(2)
|
||||
.build();
|
||||
```
|
||||
|
||||
## 实体属性
|
||||
|
||||
### 基本属性
|
||||
|
||||
```typescript
|
||||
// 实体名称 - 用于调试和标识
|
||||
entity.name = "Player";
|
||||
|
||||
// 实体ID - 只读,场景内唯一
|
||||
console.log(entity.id); // 例如: 1
|
||||
|
||||
// 标签 - 用于分类和快速查询
|
||||
entity.tag = 1; // 玩家标签
|
||||
entity.tag = 2; // 敌人标签
|
||||
|
||||
// 更新顺序 - 控制实体在系统中的处理优先级
|
||||
entity.updateOrder = 0; // 数值越小优先级越高
|
||||
```
|
||||
|
||||
### 状态控制
|
||||
|
||||
```typescript
|
||||
// 启用状态 - 控制实体是否参与更新和处理
|
||||
entity.enabled = true; // 启用实体
|
||||
entity.enabled = false; // 禁用实体
|
||||
|
||||
// 激活状态 - 控制实体及其子实体的活跃状态
|
||||
entity.active = true; // 激活实体
|
||||
entity.active = false; // 停用实体
|
||||
|
||||
// 检查层次结构中的激活状态
|
||||
if (entity.activeInHierarchy) {
|
||||
// 实体在整个层次结构中都是激活的
|
||||
}
|
||||
|
||||
// 检查销毁状态
|
||||
if (entity.isDestroyed) {
|
||||
// 实体已被销毁
|
||||
}
|
||||
```
|
||||
|
||||
### 更新间隔
|
||||
|
||||
```typescript
|
||||
// 控制实体更新频率
|
||||
entity.updateInterval = 1; // 每帧更新
|
||||
entity.updateInterval = 2; // 每2帧更新一次
|
||||
entity.updateInterval = 5; // 每5帧更新一次
|
||||
```
|
||||
|
||||
## 组件管理
|
||||
|
||||
### 添加组件
|
||||
|
||||
```typescript
|
||||
// 创建并添加组件
|
||||
const healthComponent = entity.addComponent(new HealthComponent(100));
|
||||
|
||||
// 使用工厂方法创建组件
|
||||
const positionComponent = entity.createComponent(PositionComponent, 100, 200);
|
||||
|
||||
// 批量添加组件
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
```
|
||||
|
||||
### 获取组件
|
||||
|
||||
```typescript
|
||||
// 获取单个组件
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health) {
|
||||
console.log(`当前生命值: ${health.currentHealth}`);
|
||||
}
|
||||
|
||||
// 获取或创建组件(如果不存在则创建)
|
||||
const position = entity.getOrCreateComponent(PositionComponent, 0, 0);
|
||||
|
||||
// 获取多个同类型组件(如果组件可以重复添加)
|
||||
const allHealthComponents = entity.getComponents(HealthComponent);
|
||||
```
|
||||
|
||||
### 检查组件
|
||||
|
||||
```typescript
|
||||
// 检查是否拥有指定组件
|
||||
if (entity.hasComponent(HealthComponent)) {
|
||||
// 实体拥有生命值组件
|
||||
}
|
||||
|
||||
// 检查组件掩码(高性能)
|
||||
const mask = entity.componentMask;
|
||||
console.log(`组件掩码: ${mask.toString(2)}`);
|
||||
```
|
||||
|
||||
### 移除组件
|
||||
|
||||
```typescript
|
||||
// 移除指定组件实例
|
||||
const healthComponent = entity.getComponent(HealthComponent);
|
||||
if (healthComponent) {
|
||||
entity.removeComponent(healthComponent);
|
||||
}
|
||||
|
||||
// 按类型移除组件
|
||||
const removedHealth = entity.removeComponentByType(HealthComponent);
|
||||
|
||||
// 批量移除组件
|
||||
const removedComponents = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 移除所有组件
|
||||
entity.removeAllComponents();
|
||||
```
|
||||
|
||||
## 层次结构管理
|
||||
|
||||
### 父子关系
|
||||
|
||||
```typescript
|
||||
// 创建父子实体
|
||||
const player = scene.createEntity("Player");
|
||||
const weapon = scene.createEntity("Weapon");
|
||||
const shield = scene.createEntity("Shield");
|
||||
|
||||
// 添加子实体
|
||||
player.addChild(weapon);
|
||||
player.addChild(shield);
|
||||
|
||||
// 获取父实体
|
||||
console.log(weapon.parent === player); // true
|
||||
|
||||
// 获取所有子实体
|
||||
const children = player.children;
|
||||
console.log(children.length); // 2
|
||||
|
||||
// 获取子实体数量
|
||||
console.log(player.childCount); // 2
|
||||
```
|
||||
|
||||
### 查找子实体
|
||||
|
||||
```typescript
|
||||
// 按名称查找子实体
|
||||
const weapon = player.findChild("Weapon");
|
||||
|
||||
// 递归查找子实体
|
||||
const deepChild = player.findChild("DeepChild", true);
|
||||
|
||||
// 按标签查找子实体
|
||||
const enemies = player.findChildrenByTag(2); // 查找所有敌人标签的子实体
|
||||
|
||||
// 递归按标签查找
|
||||
const allEnemies = player.findChildrenByTag(2, true);
|
||||
```
|
||||
|
||||
### 层次结构操作
|
||||
|
||||
```typescript
|
||||
// 移除子实体
|
||||
const removed = player.removeChild(weapon);
|
||||
|
||||
// 移除所有子实体
|
||||
player.removeAllChildren();
|
||||
|
||||
// 获取根实体
|
||||
const root = weapon.getRoot();
|
||||
|
||||
// 检查祖先关系
|
||||
if (player.isAncestorOf(weapon)) {
|
||||
// player 是 weapon 的祖先
|
||||
}
|
||||
|
||||
// 检查后代关系
|
||||
if (weapon.isDescendantOf(player)) {
|
||||
// weapon 是 player 的后代
|
||||
}
|
||||
|
||||
// 获取实体在层次结构中的深度
|
||||
const depth = weapon.getDepth(); // 从根实体开始计算的深度
|
||||
```
|
||||
|
||||
### 遍历子实体
|
||||
|
||||
```typescript
|
||||
// 遍历直接子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name}`);
|
||||
});
|
||||
|
||||
// 递归遍历所有子实体
|
||||
player.forEachChild((child, index) => {
|
||||
console.log(`子实体 ${index}: ${child.name} (深度: ${child.getDepth()})`);
|
||||
}, true);
|
||||
```
|
||||
|
||||
## 实体生命周期
|
||||
|
||||
### 更新循环
|
||||
|
||||
```typescript
|
||||
// 手动更新实体(通常由场景自动调用)
|
||||
entity.update();
|
||||
|
||||
// 实体会自动调用所有组件的update方法
|
||||
class MyComponent extends Component {
|
||||
public update(): void {
|
||||
// 组件更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁实体
|
||||
|
||||
```typescript
|
||||
// 销毁实体
|
||||
entity.destroy();
|
||||
|
||||
// 检查是否已销毁
|
||||
if (entity.isDestroyed) {
|
||||
console.log("实体已被销毁");
|
||||
}
|
||||
|
||||
// 销毁实体时会自动:
|
||||
// 1. 移除所有组件
|
||||
// 2. 从父实体中移除
|
||||
// 3. 销毁所有子实体
|
||||
// 4. 从场景中移除
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 组件缓存
|
||||
|
||||
```typescript
|
||||
// 预热组件缓存(提高后续访问性能)
|
||||
entity.warmUpComponentCache();
|
||||
|
||||
// 清理组件缓存
|
||||
entity.cleanupComponentCache();
|
||||
|
||||
// 获取缓存统计信息
|
||||
const cacheStats = entity.getComponentCacheStats();
|
||||
console.log(`缓存命中率: ${cacheStats.cacheStats.hitRate}`);
|
||||
console.log(`组件访问统计:`, cacheStats.accessStats);
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量添加组件(比单个添加更高效)
|
||||
const components = entity.addComponents([
|
||||
new PositionComponent(0, 0),
|
||||
new VelocityComponent(50, 0),
|
||||
new HealthComponent(100)
|
||||
]);
|
||||
|
||||
// 批量移除组件
|
||||
const removed = entity.removeComponentsByTypes([
|
||||
HealthComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
```
|
||||
|
||||
## 调试和监控
|
||||
|
||||
### 调试信息
|
||||
|
||||
```typescript
|
||||
// 获取详细的调试信息
|
||||
const debugInfo = entity.getDebugInfo();
|
||||
console.log("实体调试信息:", debugInfo);
|
||||
|
||||
// 调试信息包含:
|
||||
// - 基本属性(名称、ID、状态等)
|
||||
// - 组件信息(数量、类型、掩码等)
|
||||
// - 层次结构信息(父子关系、深度等)
|
||||
// - 性能统计(缓存命中率、访问统计等)
|
||||
```
|
||||
|
||||
### 实体比较
|
||||
|
||||
```typescript
|
||||
// 比较两个实体的优先级
|
||||
const result = entity1.compareTo(entity2);
|
||||
if (result < 0) {
|
||||
// entity1 优先级更高
|
||||
} else if (result > 0) {
|
||||
// entity2 优先级更高
|
||||
} else {
|
||||
// 优先级相同
|
||||
}
|
||||
|
||||
// 实体的字符串表示
|
||||
console.log(entity.toString()); // "Entity[Player:1]"
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 合理使用标签
|
||||
|
||||
```typescript
|
||||
// 定义标签常量
|
||||
const Tags = {
|
||||
PLAYER: 1,
|
||||
ENEMY: 2,
|
||||
PROJECTILE: 3,
|
||||
PICKUP: 4
|
||||
} as const;
|
||||
|
||||
// 使用标签进行分类
|
||||
player.tag = Tags.PLAYER;
|
||||
enemy.tag = Tags.ENEMY;
|
||||
```
|
||||
|
||||
### 2. 优化更新顺序
|
||||
|
||||
```typescript
|
||||
// 设置合理的更新顺序
|
||||
player.updateOrder = 0; // 玩家最先更新
|
||||
enemy.updateOrder = 1; // 敌人其次
|
||||
projectile.updateOrder = 2; // 投射物最后
|
||||
```
|
||||
|
||||
### 3. 合理使用层次结构
|
||||
|
||||
```typescript
|
||||
// 创建复合实体
|
||||
const tank = scene.createEntity("Tank");
|
||||
const turret = scene.createEntity("Turret");
|
||||
const barrel = scene.createEntity("Barrel");
|
||||
|
||||
// 建立层次关系
|
||||
tank.addChild(turret);
|
||||
turret.addChild(barrel);
|
||||
|
||||
// 这样可以通过控制父实体来影响整个层次结构
|
||||
tank.active = false; // 整个坦克都会被停用
|
||||
```
|
||||
|
||||
### 4. 组件缓存优化
|
||||
|
||||
```typescript
|
||||
// 对于频繁访问的组件,预热缓存
|
||||
entity.warmUpComponentCache();
|
||||
|
||||
// 定期清理不常用的缓存
|
||||
setInterval(() => {
|
||||
entity.cleanupComponentCache();
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
### 5. 避免内存泄漏
|
||||
|
||||
```typescript
|
||||
// 确保正确销毁实体
|
||||
if (entity.isDestroyed) {
|
||||
return; // 避免操作已销毁的实体
|
||||
}
|
||||
|
||||
// 在适当的时候销毁不需要的实体
|
||||
if (enemy.getComponent(HealthComponent)?.isDead()) {
|
||||
enemy.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 实体可以在不同场景间移动吗?
|
||||
|
||||
A: 不可以。实体与场景紧密绑定,如果需要在场景间传递数据,应该序列化实体的组件数据,然后在新场景中重新创建。
|
||||
|
||||
### Q: 如何实现实体的位置、旋转、缩放?
|
||||
|
||||
A: 框架本身不提供这些属性,需要通过组件来实现:
|
||||
|
||||
```typescript
|
||||
class TransformComponent extends Component {
|
||||
public position = { x: 0, y: 0 };
|
||||
public rotation = 0;
|
||||
public scale = { x: 1, y: 1 };
|
||||
}
|
||||
|
||||
const transform = entity.addComponent(new TransformComponent());
|
||||
transform.position.x = 100;
|
||||
transform.rotation = Math.PI / 4;
|
||||
```
|
||||
|
||||
### Q: 实体的更新顺序如何影响性能?
|
||||
|
||||
A: 更新顺序主要影响游戏逻辑的执行顺序,对性能影响较小。但合理的更新顺序可以避免一些逻辑问题,比如确保输入处理在移动之前执行。
|
||||
|
||||
### Q: 如何处理大量实体的性能问题?
|
||||
|
||||
A:
|
||||
1. 使用对象池重用实体
|
||||
2. 合理使用组件缓存
|
||||
3. 避免不必要的组件查询
|
||||
4. 使用批量操作
|
||||
5. 定期清理销毁的实体
|
||||
|
||||
```typescript
|
||||
// 使用对象池
|
||||
class EntityPool extends Pool<Entity> {
|
||||
protected createObject(): Entity {
|
||||
return scene.createEntity("PooledEntity");
|
||||
}
|
||||
|
||||
protected resetObject(entity: Entity): void {
|
||||
entity.removeAllComponents();
|
||||
entity.active = true;
|
||||
entity.enabled = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
421
docs/entity-manager-example.md
Normal file
421
docs/entity-manager-example.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# EntityManager 使用指南
|
||||
|
||||
EntityManager 是 ECS Framework 的核心管理系统,提供统一的实体管理、高性能查询和自动优化功能。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 创建实体管理器
|
||||
|
||||
```typescript
|
||||
import { EntityManager, Scene } from '@esengine/ecs-framework';
|
||||
|
||||
// 通常在游戏管理器中创建
|
||||
const scene = new Scene();
|
||||
const entityManager = new EntityManager(scene);
|
||||
```
|
||||
|
||||
### 基础实体操作
|
||||
|
||||
```typescript
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = "player";
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = entityManager.createEntities(50, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(30));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
|
||||
// 销毁实体
|
||||
entityManager.destroyEntity(player);
|
||||
```
|
||||
|
||||
## 高性能查询系统
|
||||
|
||||
EntityManager 提供多种查询方式,自动选择最优的查询策略。
|
||||
|
||||
### 基础查询
|
||||
|
||||
```typescript
|
||||
// 通过ID查询(O(1))
|
||||
const entity = entityManager.getEntity(123);
|
||||
|
||||
// 通过名称查询(O(1) 哈希查找)
|
||||
const player = entityManager.getEntityByName("Player");
|
||||
|
||||
// 通过标签查询(O(1) 索引查找)
|
||||
const enemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// 组件查询(使用O(1)组件索引)
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 多组件查询(使用Archetype优化)
|
||||
const movingEntities = entityManager.getEntitiesWithComponents([
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
```
|
||||
|
||||
### 流式查询API
|
||||
|
||||
EntityManager 提供强大的流式查询构建器:
|
||||
|
||||
```typescript
|
||||
// 基础查询构建
|
||||
const results = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // 必须包含这些组件
|
||||
.withoutTag("dead") // 不能有死亡标签
|
||||
.active(true) // 必须激活
|
||||
.execute();
|
||||
|
||||
// 复杂条件查询
|
||||
const livingEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth > 0;
|
||||
})
|
||||
.execute();
|
||||
|
||||
// 查询变体
|
||||
const firstEnemy = entityManager
|
||||
.query()
|
||||
.withTag("enemy")
|
||||
.first(); // 获取第一个匹配
|
||||
|
||||
const enemyCount = entityManager
|
||||
.query()
|
||||
.withTag("enemy")
|
||||
.count(); // 获取数量
|
||||
|
||||
// 直接处理查询结果
|
||||
entityManager
|
||||
.query()
|
||||
.withAll([HealthComponent])
|
||||
.forEach(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 高级查询选项
|
||||
|
||||
```typescript
|
||||
// 组合条件查询
|
||||
const combatUnits = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // AND条件
|
||||
.withAny([WeaponComponent, MagicComponent]) // OR条件
|
||||
.without([DeadComponent]) // NOT条件
|
||||
.withTag("combatant")
|
||||
.withoutTag("peaceful")
|
||||
.active(true)
|
||||
.enabled(true)
|
||||
.execute();
|
||||
|
||||
// 使用自定义过滤器
|
||||
const nearbyEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent])
|
||||
.withTag("enemy")
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - playerPos.x, 2) +
|
||||
Math.pow(pos.y - playerPos.y, 2)
|
||||
);
|
||||
return distance < 100; // 距离玩家100像素内
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
## 批量操作
|
||||
|
||||
EntityManager 提供高效的批量操作方法:
|
||||
|
||||
```typescript
|
||||
// 遍历所有实体
|
||||
entityManager.forEachEntity(entity => {
|
||||
// 处理每个实体
|
||||
if (entity.position.x < 0) {
|
||||
entity.position.x = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 遍历特定组件的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
entity.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 批量创建并配置实体
|
||||
const bullets = entityManager.createEntities(100, "Bullet", (bullet, index) => {
|
||||
bullet.addComponent(new PositionComponent(
|
||||
100 + index * 10,
|
||||
100
|
||||
));
|
||||
bullet.addComponent(new VelocityComponent(0, -200));
|
||||
bullet.tag = "projectile";
|
||||
});
|
||||
```
|
||||
|
||||
## 性能优化系统
|
||||
|
||||
EntityManager 内置了三个性能优化系统:
|
||||
|
||||
### 1. 组件索引系统
|
||||
|
||||
自动为组件查询提供O(1)性能:
|
||||
|
||||
```typescript
|
||||
// 获取组件索引统计
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
const stats = componentIndex.getPerformanceStats();
|
||||
|
||||
console.log('组件索引统计:', {
|
||||
totalQueries: stats.totalQueries,
|
||||
indexHits: stats.indexHits,
|
||||
hitRate: (stats.indexHits / stats.totalQueries * 100).toFixed(2) + '%'
|
||||
});
|
||||
|
||||
// 手动优化(通常自动进行)
|
||||
componentIndex.optimize();
|
||||
```
|
||||
|
||||
### 2. Archetype系统
|
||||
|
||||
按组件组合分组实体,优化批量查询:
|
||||
|
||||
```typescript
|
||||
// 获取Archetype统计
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const archetypeStats = archetypeSystem.getStatistics();
|
||||
|
||||
console.log('Archetype统计:', {
|
||||
totalArchetypes: archetypeStats.totalArchetypes,
|
||||
totalEntities: archetypeStats.totalEntities,
|
||||
queryCacheSize: archetypeStats.queryCacheSize
|
||||
});
|
||||
|
||||
// 查看所有原型
|
||||
console.log('当前原型:', archetypeSystem.getAllArchetypes());
|
||||
```
|
||||
|
||||
### 3. 脏标记系统
|
||||
|
||||
追踪实体变更,避免不必要的更新:
|
||||
|
||||
```typescript
|
||||
// 获取脏标记统计
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
const dirtyStats = dirtyTracking.getPerformanceStats();
|
||||
|
||||
console.log('脏标记统计:', {
|
||||
totalMarks: dirtyStats.totalMarks,
|
||||
batchesProcessed: dirtyStats.batchesProcessed,
|
||||
listenersNotified: dirtyStats.listenersNotified
|
||||
});
|
||||
|
||||
// 手动处理脏标记
|
||||
dirtyTracking.processDirtyMarks();
|
||||
```
|
||||
|
||||
## 实体管理器统计
|
||||
|
||||
获取EntityManager的综合性能数据:
|
||||
|
||||
```typescript
|
||||
const stats = entityManager.getStatistics();
|
||||
|
||||
console.log('EntityManager统计:', {
|
||||
// 基础统计
|
||||
entityCount: stats.entityCount,
|
||||
activeEntityCount: stats.activeEntityCount,
|
||||
|
||||
// 查询统计
|
||||
totalQueries: stats.totalQueries,
|
||||
indexHits: stats.indexHits,
|
||||
archetypeHits: stats.archetypeHits,
|
||||
|
||||
// 性能指标
|
||||
averageQueryTime: stats.averageQueryTime,
|
||||
hitRate: (stats.indexHits / stats.totalQueries * 100).toFixed(2) + '%'
|
||||
});
|
||||
```
|
||||
|
||||
## 系统优化和清理
|
||||
|
||||
```typescript
|
||||
// 手动触发优化
|
||||
entityManager.optimize();
|
||||
|
||||
// 内存清理
|
||||
entityManager.cleanup();
|
||||
|
||||
// 压缩数据结构
|
||||
entityManager.compact();
|
||||
|
||||
// 获取内存使用情况
|
||||
const memoryStats = entityManager.getMemoryUsage();
|
||||
console.log('内存使用:', {
|
||||
entityIndexSize: memoryStats.entityIndex,
|
||||
componentIndexSize: memoryStats.componentIndex,
|
||||
archetypeSize: memoryStats.archetype
|
||||
});
|
||||
```
|
||||
|
||||
## 实际使用案例
|
||||
|
||||
### 游戏系统集成
|
||||
|
||||
```typescript
|
||||
class MovementSystem extends EntitySystem {
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor(scene: Scene) {
|
||||
super();
|
||||
this.entityManager = new EntityManager(scene);
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
// 使用高效查询获取移动实体
|
||||
const movingEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 批量处理
|
||||
movingEntities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂查询示例
|
||||
|
||||
```typescript
|
||||
// 战斗系统:查找攻击范围内的敌人
|
||||
class CombatSystem {
|
||||
private entityManager: EntityManager;
|
||||
|
||||
findTargetsInRange(attacker: Entity, range: number): Entity[] {
|
||||
const attackerPos = attacker.getComponent(PositionComponent);
|
||||
if (!attackerPos) return [];
|
||||
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pos.x - attackerPos.x, 2) +
|
||||
Math.pow(pos.y - attackerPos.y, 2)
|
||||
);
|
||||
return distance <= range;
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// 优化版本:使用空间分区(如果实现了的话)
|
||||
findTargetsInRangeOptimized(attacker: Entity, range: number): Entity[] {
|
||||
// 首先通过空间查询缩小范围
|
||||
const nearbyEntities = this.spatialIndex.queryRange(
|
||||
attackerPos.x - range,
|
||||
attackerPos.y - range,
|
||||
attackerPos.x + range,
|
||||
attackerPos.y + range
|
||||
);
|
||||
|
||||
// 然后使用EntityManager进行精确过滤
|
||||
return this.entityManager
|
||||
.query()
|
||||
.withAll([HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => nearbyEntities.includes(entity))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能建议
|
||||
|
||||
### 查询优化
|
||||
|
||||
1. **利用索引**: 优先使用组件查询和标签查询,它们具有O(1)性能
|
||||
2. **减少自定义过滤**: `where()`条件虽然灵活,但会降低性能
|
||||
3. **缓存查询结果**: 对于不经常变化的查询结果,考虑缓存
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:使用索引查询
|
||||
const enemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// ⚠️ 谨慎:自定义过滤
|
||||
const enemies = entityManager
|
||||
.query()
|
||||
.where(entity => entity.name.includes("Enemy"))
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 批量操作优化
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:批量创建
|
||||
const bullets = entityManager.createEntities(100, "Bullet");
|
||||
|
||||
// ❌ 避免:循环单独创建
|
||||
for (let i = 0; i < 100; i++) {
|
||||
entityManager.createEntity("Bullet");
|
||||
}
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
// 定期清理
|
||||
setInterval(() => {
|
||||
entityManager.cleanup();
|
||||
}, 30000); // 每30秒清理一次
|
||||
|
||||
// 监控性能
|
||||
const stats = entityManager.getStatistics();
|
||||
if (stats.indexHits / stats.totalQueries < 0.8) {
|
||||
console.warn('查询命中率较低,考虑优化查询策略');
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
EntityManager 提供了:
|
||||
|
||||
- **统一接口**: 所有实体操作通过一个管理器完成
|
||||
- **自动优化**: 内置三个性能优化系统
|
||||
- **灵活查询**: 从简单的ID查找到复杂的条件查询
|
||||
- **性能监控**: 完整的统计和诊断信息
|
||||
- **批量操作**: 高效的批量处理能力
|
||||
|
||||
通过合理使用EntityManager,您可以构建高性能、可维护的ECS游戏系统。
|
||||
496
docs/event-system-example.md
Normal file
496
docs/event-system-example.md
Normal file
@@ -0,0 +1,496 @@
|
||||
# ECS事件系统使用指南
|
||||
|
||||
本文档介绍如何使用ECS框架的增强事件系统,包括类型安全的事件发布订阅、预定义的ECS事件类型和高级功能。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [基础用法](#基础用法)
|
||||
2. [预定义ECS事件](#预定义ecs事件)
|
||||
3. [事件装饰器](#事件装饰器)
|
||||
4. [高级功能](#高级功能)
|
||||
5. [性能优化](#性能优化)
|
||||
6. [最佳实践](#最佳实践)
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 创建事件总线
|
||||
|
||||
```typescript
|
||||
import { EventBus, GlobalEventBus } from './ECS';
|
||||
|
||||
// 方式1:创建独立的事件总线
|
||||
const eventBus = new EventBus(true); // true启用调试模式
|
||||
|
||||
// 方式2:使用全局事件总线
|
||||
const globalEventBus = GlobalEventBus.getInstance(true);
|
||||
```
|
||||
|
||||
### 基本事件发布订阅
|
||||
|
||||
```typescript
|
||||
// 定义事件数据类型
|
||||
interface PlayerDiedEvent {
|
||||
playerId: number;
|
||||
cause: string;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
const listenerId = eventBus.on<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log(`Player ${data.playerId} died at (${data.position.x}, ${data.position.y})`);
|
||||
console.log(`Cause: ${data.cause}`);
|
||||
});
|
||||
|
||||
// 发射事件
|
||||
eventBus.emit('player:died', {
|
||||
playerId: 123,
|
||||
cause: 'enemy_attack',
|
||||
position: { x: 100, y: 200 }
|
||||
});
|
||||
|
||||
// 移除监听器
|
||||
eventBus.off('player:died', listenerId);
|
||||
```
|
||||
|
||||
### 一次性事件监听
|
||||
|
||||
```typescript
|
||||
// 只监听一次
|
||||
eventBus.once<PlayerDiedEvent>('player:died', (data) => {
|
||||
console.log('This will only be called once');
|
||||
});
|
||||
```
|
||||
|
||||
### 异步事件处理
|
||||
|
||||
```typescript
|
||||
// 异步事件监听
|
||||
eventBus.onAsync<PlayerDiedEvent>('player:died', async (data) => {
|
||||
await savePlayerDeathToDatabase(data);
|
||||
await updateLeaderboard(data.playerId);
|
||||
});
|
||||
|
||||
// 异步事件发射
|
||||
await eventBus.emitAsync('player:died', playerData);
|
||||
```
|
||||
|
||||
## 预定义ECS事件
|
||||
|
||||
框架提供了完整的ECS事件类型定义,支持实体、组件、系统等核心概念的事件。
|
||||
|
||||
### 实体事件
|
||||
|
||||
```typescript
|
||||
import { ECSEventType, IEntityEventData } from './ECS';
|
||||
|
||||
// 监听实体创建事件
|
||||
eventBus.onEntityCreated((data: IEntityEventData) => {
|
||||
console.log(`Entity created: ${data.entityName} (ID: ${data.entityId})`);
|
||||
});
|
||||
|
||||
// 监听实体销毁事件
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_DESTROYED, (data) => {
|
||||
console.log(`Entity destroyed: ${data.entityName}`);
|
||||
});
|
||||
|
||||
// 手动发射实体事件
|
||||
eventBus.emitEntityCreated({
|
||||
timestamp: Date.now(),
|
||||
source: 'GameManager',
|
||||
entityId: 123,
|
||||
entityName: 'Player',
|
||||
entityTag: 'player'
|
||||
});
|
||||
```
|
||||
|
||||
### 组件事件
|
||||
|
||||
```typescript
|
||||
import { IComponentEventData } from './ECS';
|
||||
|
||||
// 监听组件添加事件
|
||||
eventBus.onComponentAdded((data: IComponentEventData) => {
|
||||
console.log(`Component ${data.componentType} added to entity ${data.entityId}`);
|
||||
});
|
||||
|
||||
// 监听组件移除事件
|
||||
eventBus.on<IComponentEventData>(ECSEventType.COMPONENT_REMOVED, (data) => {
|
||||
console.log(`Component ${data.componentType} removed from entity ${data.entityId}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 系统事件
|
||||
|
||||
```typescript
|
||||
import { ISystemEventData } from './ECS';
|
||||
|
||||
// 监听系统错误
|
||||
eventBus.onSystemError((data: ISystemEventData) => {
|
||||
console.error(`System error in ${data.systemName}: ${data.systemType}`);
|
||||
});
|
||||
|
||||
// 监听系统处理开始/结束
|
||||
eventBus.on<ISystemEventData>(ECSEventType.SYSTEM_PROCESSING_START, (data) => {
|
||||
console.log(`System ${data.systemName} started processing`);
|
||||
});
|
||||
```
|
||||
|
||||
### 性能事件
|
||||
|
||||
```typescript
|
||||
import { IPerformanceEventData } from './ECS';
|
||||
|
||||
// 监听性能警告
|
||||
eventBus.onPerformanceWarning((data: IPerformanceEventData) => {
|
||||
console.warn(`Performance warning: ${data.operation} took ${data.executionTime}ms`);
|
||||
});
|
||||
|
||||
// 监听内存使用过高
|
||||
eventBus.on<IPerformanceEventData>(ECSEventType.MEMORY_USAGE_HIGH, (data) => {
|
||||
console.warn(`High memory usage: ${data.memoryUsage}MB`);
|
||||
});
|
||||
```
|
||||
|
||||
## 事件装饰器
|
||||
|
||||
使用装饰器可以自动注册事件监听器,简化代码编写。
|
||||
|
||||
### 基础装饰器
|
||||
|
||||
```typescript
|
||||
import { EventHandler, AsyncEventHandler, EventPriority } from './ECS';
|
||||
|
||||
class GameManager {
|
||||
@EventHandler(ECSEventType.ENTITY_CREATED, { priority: EventPriority.HIGH })
|
||||
onEntityCreated(data: IEntityEventData) {
|
||||
console.log(`New entity: ${data.entityName}`);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.ENTITY_DESTROYED)
|
||||
async onEntityDestroyed(data: IEntityEventData) {
|
||||
await this.cleanupEntityResources(data.entityId);
|
||||
}
|
||||
|
||||
@EventHandler('custom:game:event', { once: true })
|
||||
onGameStart(data: any) {
|
||||
console.log('Game started!');
|
||||
}
|
||||
|
||||
// 需要手动调用初始化方法
|
||||
constructor() {
|
||||
// 如果类有initEventListeners方法,会自动注册装饰器定义的监听器
|
||||
if (this.initEventListeners) {
|
||||
this.initEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupEntityResources(entityId: number) {
|
||||
// 清理实体相关资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优先级和配置
|
||||
|
||||
```typescript
|
||||
class SystemManager {
|
||||
@EventHandler(ECSEventType.SYSTEM_ERROR, {
|
||||
priority: EventPriority.CRITICAL,
|
||||
context: this
|
||||
})
|
||||
handleSystemError(data: ISystemEventData) {
|
||||
this.logError(data);
|
||||
this.restartSystem(data.systemName);
|
||||
}
|
||||
|
||||
@AsyncEventHandler(ECSEventType.PERFORMANCE_WARNING, {
|
||||
priority: EventPriority.LOW,
|
||||
async: true
|
||||
})
|
||||
async handlePerformanceWarning(data: IPerformanceEventData) {
|
||||
await this.optimizePerformance(data);
|
||||
}
|
||||
|
||||
private logError(data: ISystemEventData) {
|
||||
// 错误日志记录
|
||||
}
|
||||
|
||||
private restartSystem(systemName: string) {
|
||||
// 重启系统
|
||||
}
|
||||
|
||||
private async optimizePerformance(data: IPerformanceEventData) {
|
||||
// 性能优化逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件批处理
|
||||
|
||||
```typescript
|
||||
// 设置批处理配置
|
||||
eventBus.setBatchConfig('entity:update', 100, 16); // 批量100个,延迟16ms
|
||||
|
||||
// 发射事件(会被批处理)
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
eventBus.emit('entity:update', { entityId: i, data: 'update' });
|
||||
}
|
||||
|
||||
// 手动刷新批处理队列
|
||||
eventBus.flushBatch('entity:update');
|
||||
```
|
||||
|
||||
### 事件统计和监控
|
||||
|
||||
```typescript
|
||||
// 获取单个事件统计
|
||||
const stats = eventBus.getStats('entity:created');
|
||||
console.log(`Event triggered ${stats.triggerCount} times`);
|
||||
console.log(`Average execution time: ${stats.averageExecutionTime}ms`);
|
||||
|
||||
// 获取所有事件统计
|
||||
const allStats = eventBus.getStats();
|
||||
if (allStats instanceof Map) {
|
||||
allStats.forEach((stat, eventType) => {
|
||||
console.log(`${eventType}: ${stat.triggerCount} triggers`);
|
||||
});
|
||||
}
|
||||
|
||||
// 重置统计
|
||||
eventBus.resetStats('entity:created');
|
||||
```
|
||||
|
||||
### 事件类型验证
|
||||
|
||||
```typescript
|
||||
import { EventTypeValidator } from './ECS';
|
||||
|
||||
// 检查事件类型是否有效
|
||||
if (EventTypeValidator.isValid('entity:created')) {
|
||||
eventBus.emit('entity:created', data);
|
||||
}
|
||||
|
||||
// 添加自定义事件类型
|
||||
EventTypeValidator.addCustomType('game:custom:event');
|
||||
|
||||
// 获取所有有效事件类型
|
||||
const validTypes = EventTypeValidator.getAllValidTypes();
|
||||
console.log('Valid event types:', validTypes);
|
||||
```
|
||||
|
||||
### 调试和日志
|
||||
|
||||
```typescript
|
||||
// 启用调试模式
|
||||
eventBus.setDebugMode(true);
|
||||
|
||||
// 设置最大监听器数量
|
||||
eventBus.setMaxListeners(50);
|
||||
|
||||
// 检查是否有监听器
|
||||
if (eventBus.hasListeners('entity:created')) {
|
||||
console.log('Has listeners for entity:created');
|
||||
}
|
||||
|
||||
// 获取监听器数量
|
||||
const count = eventBus.getListenerCount('entity:created');
|
||||
console.log(`${count} listeners for entity:created`);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 事件过滤和条件监听
|
||||
|
||||
```typescript
|
||||
// 使用条件过滤减少不必要的事件处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
// 只处理玩家实体
|
||||
if (data.entityTag === 'player') {
|
||||
handlePlayerCreated(data);
|
||||
}
|
||||
});
|
||||
|
||||
// 更好的方式:使用具体的事件类型
|
||||
eventBus.on<IEntityEventData>('entity:player:created', handlePlayerCreated);
|
||||
```
|
||||
|
||||
### 内存管理
|
||||
|
||||
```typescript
|
||||
class EventManager {
|
||||
private listeners: string[] = [];
|
||||
|
||||
public setupListeners() {
|
||||
// 保存监听器ID以便清理
|
||||
this.listeners.push(
|
||||
eventBus.on('event1', this.handler1.bind(this)),
|
||||
eventBus.on('event2', this.handler2.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
// 清理所有监听器
|
||||
this.listeners.forEach(id => {
|
||||
eventBus.off('event1', id);
|
||||
eventBus.off('event2', id);
|
||||
});
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
|
||||
private handler1(data: any) { /* ... */ }
|
||||
private handler2(data: any) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 异步事件优化
|
||||
|
||||
```typescript
|
||||
// 使用Promise.all并行处理多个异步事件
|
||||
const promises = [
|
||||
eventBus.emitAsync('save:player', playerData),
|
||||
eventBus.emitAsync('update:leaderboard', scoreData),
|
||||
eventBus.emitAsync('notify:friends', notificationData)
|
||||
];
|
||||
|
||||
await Promise.all(promises);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 事件命名规范
|
||||
|
||||
```typescript
|
||||
// 推荐的事件命名格式:模块:对象:动作
|
||||
const EVENT_NAMES = {
|
||||
// 实体相关
|
||||
ENTITY_PLAYER_CREATED: 'entity:player:created',
|
||||
ENTITY_ENEMY_DESTROYED: 'entity:enemy:destroyed',
|
||||
|
||||
// 游戏逻辑相关
|
||||
GAME_LEVEL_COMPLETED: 'game:level:completed',
|
||||
GAME_SCORE_UPDATED: 'game:score:updated',
|
||||
|
||||
// UI相关
|
||||
UI_MENU_OPENED: 'ui:menu:opened',
|
||||
UI_BUTTON_CLICKED: 'ui:button:clicked'
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
```typescript
|
||||
// 定义强类型的事件数据接口
|
||||
interface GameEvents {
|
||||
'player:levelup': { playerId: number; newLevel: number; experience: number };
|
||||
'inventory:item:added': { itemId: string; quantity: number; playerId: number };
|
||||
'combat:damage:dealt': { attackerId: number; targetId: number; damage: number };
|
||||
}
|
||||
|
||||
// 创建类型安全的事件发射器
|
||||
class TypedEventBus {
|
||||
private eventBus = new EventBus();
|
||||
|
||||
emit<K extends keyof GameEvents>(eventType: K, data: GameEvents[K]) {
|
||||
this.eventBus.emit(eventType, data);
|
||||
}
|
||||
|
||||
on<K extends keyof GameEvents>(
|
||||
eventType: K,
|
||||
handler: (data: GameEvents[K]) => void
|
||||
) {
|
||||
return this.eventBus.on(eventType, handler);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```typescript
|
||||
// 在事件处理器中添加错误处理
|
||||
eventBus.on<IEntityEventData>(ECSEventType.ENTITY_CREATED, (data) => {
|
||||
try {
|
||||
processEntityCreation(data);
|
||||
} catch (error) {
|
||||
console.error('Error processing entity creation:', error);
|
||||
// 发射错误事件
|
||||
eventBus.emit(ECSEventType.ERROR_OCCURRED, {
|
||||
timestamp: Date.now(),
|
||||
source: 'EntityCreationHandler',
|
||||
error: error.message,
|
||||
context: data
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 模块化事件管理
|
||||
|
||||
```typescript
|
||||
// 为不同模块创建专门的事件管理器
|
||||
class PlayerEventManager {
|
||||
constructor(private eventBus: EventBus) {
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.eventBus.onEntityCreated(this.onPlayerCreated.bind(this));
|
||||
this.eventBus.on('player:levelup', this.onPlayerLevelUp.bind(this));
|
||||
this.eventBus.on('player:died', this.onPlayerDied.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerCreated(data: IEntityEventData) {
|
||||
if (data.entityTag === 'player') {
|
||||
// 处理玩家创建逻辑
|
||||
}
|
||||
}
|
||||
|
||||
private onPlayerLevelUp(data: any) {
|
||||
// 处理玩家升级逻辑
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any) {
|
||||
// 处理玩家死亡逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 与EntityManager集成
|
||||
|
||||
```typescript
|
||||
import { EntityManager } from './ECS';
|
||||
|
||||
// EntityManager会自动设置事件总线
|
||||
const entityManager = new EntityManager();
|
||||
|
||||
// 获取事件总线实例
|
||||
const eventBus = entityManager.eventBus;
|
||||
|
||||
// 监听自动发射的ECS事件
|
||||
eventBus.onEntityCreated((data) => {
|
||||
console.log('Entity created automatically:', data);
|
||||
});
|
||||
|
||||
eventBus.onComponentAdded((data) => {
|
||||
console.log('Component added automatically:', data);
|
||||
});
|
||||
|
||||
// 创建实体时会自动发射事件
|
||||
const entity = entityManager.createEntity('Player');
|
||||
|
||||
// 添加组件时会自动发射事件
|
||||
entity.addComponent(new HealthComponent(100));
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
ECS框架的事件系统提供了:
|
||||
|
||||
- **类型安全**:完整的TypeScript类型支持
|
||||
- **高性能**:批处理、缓存和优化机制
|
||||
- **易用性**:装饰器、预定义事件类型
|
||||
- **可扩展**:自定义事件类型和验证
|
||||
- **调试友好**:详细的统计信息和调试模式
|
||||
|
||||
通过合理使用事件系统,可以实现松耦合的模块化架构,提高代码的可维护性和扩展性。
|
||||
549
docs/getting-started.md
Normal file
549
docs/getting-started.md
Normal file
@@ -0,0 +1,549 @@
|
||||
# 快速入门
|
||||
|
||||
本指南将帮助您快速上手 ECS Framework,这是一个专业级的实体组件系统框架,采用现代化架构设计,专为高性能游戏开发打造。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
ecs-framework/
|
||||
├── source/
|
||||
│ ├── src/ # 源代码
|
||||
│ │ ├── ECS/ # ECS核心系统
|
||||
│ │ ├── Types/ # 类型定义
|
||||
│ │ ├── Utils/ # 工具类
|
||||
│ │ └── Testing/ # 测试文件
|
||||
│ ├── scripts/ # 构建脚本
|
||||
│ └── tsconfig.json # TypeScript配置
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 安装和使用
|
||||
|
||||
### NPM 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/ecs-framework
|
||||
```
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/esengine/ecs-framework.git
|
||||
|
||||
# 进入源码目录
|
||||
cd ecs-framework/source
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 编译TypeScript
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 基础设置
|
||||
|
||||
### 1. 导入框架
|
||||
|
||||
```typescript
|
||||
// 导入核心类
|
||||
import {
|
||||
Core,
|
||||
Entity,
|
||||
Component,
|
||||
Scene,
|
||||
EntitySystem,
|
||||
EntityManager,
|
||||
ComponentIndexManager,
|
||||
ArchetypeSystem,
|
||||
DirtyTrackingSystem
|
||||
} from '@esengine/ecs-framework';
|
||||
```
|
||||
|
||||
### 2. 创建基础管理器
|
||||
|
||||
```typescript
|
||||
class GameManager {
|
||||
private core: Core;
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
// 创建核心实例
|
||||
this.core = Core.create(true);
|
||||
|
||||
// 创建场景
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "GameScene";
|
||||
|
||||
// 设置当前场景
|
||||
Core.scene = this.scene;
|
||||
|
||||
// 初始化实体管理器
|
||||
this.entityManager = new EntityManager(this.scene);
|
||||
|
||||
// 初始化性能优化
|
||||
this.setupPerformanceOptimizations();
|
||||
}
|
||||
|
||||
private setupPerformanceOptimizations() {
|
||||
// 启用组件索引(自动优化查询性能)
|
||||
// EntityManager内部已自动启用
|
||||
|
||||
// 可选:手动配置优化系统
|
||||
const componentIndex = this.entityManager.getComponentIndex();
|
||||
const archetypeSystem = this.entityManager.getArchetypeSystem();
|
||||
const dirtyTracking = this.entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 优化系统会自动工作,通常无需手动配置
|
||||
}
|
||||
|
||||
public update(deltaTime: number): void {
|
||||
// 更新场景
|
||||
this.scene.update();
|
||||
|
||||
// 处理系统逻辑
|
||||
this.updateSystems(deltaTime);
|
||||
}
|
||||
|
||||
private updateSystems(deltaTime: number): void {
|
||||
// 在这里添加您的系统更新逻辑
|
||||
}
|
||||
|
||||
// 提供实体管理器访问
|
||||
public getEntityManager(): EntityManager {
|
||||
return this.entityManager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 游戏循环
|
||||
|
||||
```typescript
|
||||
const gameManager = new GameManager();
|
||||
let lastTime = performance.now();
|
||||
|
||||
function gameLoop() {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000; // 转换为秒
|
||||
lastTime = currentTime;
|
||||
|
||||
gameManager.update(deltaTime);
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// 启动游戏循环
|
||||
gameLoop();
|
||||
```
|
||||
|
||||
## 创建实体和组件
|
||||
|
||||
### 1. 定义组件
|
||||
|
||||
```typescript
|
||||
import { Component, ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 位置组件
|
||||
class PositionComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
// 对象池重置方法
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 速度组件
|
||||
class VelocityComponent extends Component {
|
||||
public x: number = 0;
|
||||
public y: number = 0;
|
||||
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
super();
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 生命值组件
|
||||
class HealthComponent extends Component {
|
||||
public maxHealth: number = 100;
|
||||
public currentHealth: number = 100;
|
||||
|
||||
constructor(maxHealth: number = 100) {
|
||||
super();
|
||||
this.maxHealth = maxHealth;
|
||||
this.currentHealth = maxHealth;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.maxHealth = 100;
|
||||
this.currentHealth = 100;
|
||||
}
|
||||
|
||||
public takeDamage(damage: number): void {
|
||||
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||
}
|
||||
|
||||
public heal(amount: number): void {
|
||||
this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount);
|
||||
}
|
||||
|
||||
public isDead(): boolean {
|
||||
return this.currentHealth <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的组件定义
|
||||
// 注:框架会自动优化组件的存储和查询
|
||||
```
|
||||
|
||||
## 使用 EntityManager
|
||||
|
||||
EntityManager 是框架的核心功能,提供统一的实体管理和高性能查询接口。
|
||||
|
||||
### 1. 基础用法
|
||||
|
||||
```typescript
|
||||
// 获取EntityManager实例(在GameManager中已创建)
|
||||
const entityManager = gameManager.getEntityManager();
|
||||
|
||||
// 创建单个实体
|
||||
const player = entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(100, 100));
|
||||
player.addComponent(new VelocityComponent(50, 0));
|
||||
|
||||
// 批量创建实体
|
||||
const enemies = entityManager.createEntities(50, "Enemy");
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(30));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 高性能查询
|
||||
|
||||
```typescript
|
||||
// 流式查询API - 支持复杂查询条件
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.withoutTag("dead")
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 快速组件查询(使用O(1)索引)
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 标签查询
|
||||
const allEnemies = entityManager.getEntitiesByTag("enemy");
|
||||
|
||||
// 名称查询
|
||||
const specificEnemy = entityManager.getEntityByName("BossEnemy");
|
||||
|
||||
// 复合查询
|
||||
const livingEnemies = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent])
|
||||
.withTag("enemy")
|
||||
.withoutTag("dead")
|
||||
.where(entity => {
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health && health.currentHealth > 0;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
|
||||
```typescript
|
||||
// 批量处理实体
|
||||
entityManager.forEachEntity(entity => {
|
||||
// 处理所有实体
|
||||
if (entity.tag === "bullet" && entity.position.y < 0) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// 批量处理特定组件的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.currentHealth <= 0) {
|
||||
entity.addTag("dead");
|
||||
entity.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取统计信息
|
||||
const stats = entityManager.getStatistics();
|
||||
console.log(`总实体数: ${stats.entityCount}`);
|
||||
console.log(`索引命中率: ${stats.indexHits}/${stats.totalQueries}`);
|
||||
```
|
||||
|
||||
### 4. 性能优化功能
|
||||
|
||||
```typescript
|
||||
// 获取性能优化系统
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 查看性能统计
|
||||
console.log('组件索引统计:', componentIndex.getPerformanceStats());
|
||||
console.log('Archetype统计:', archetypeSystem.getStatistics());
|
||||
console.log('脏标记统计:', dirtyTracking.getPerformanceStats());
|
||||
|
||||
// 手动优化(通常自动进行)
|
||||
entityManager.optimize();
|
||||
|
||||
// 内存清理
|
||||
entityManager.cleanup();
|
||||
```
|
||||
|
||||
## 创建系统
|
||||
|
||||
系统处理具有特定组件的实体集合,实现游戏逻辑。
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
// 使用EntityManager进行高效查询
|
||||
const entityManager = new EntityManager(this.scene);
|
||||
const movingEntities = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.execute();
|
||||
|
||||
movingEntities.forEach(entity => {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
const velocity = entity.getComponent(VelocityComponent);
|
||||
|
||||
if (position && velocity) {
|
||||
// 更新位置
|
||||
position.x += velocity.x * 0.016; // 假设60FPS
|
||||
position.y += velocity.y * 0.016;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HealthSystem extends EntitySystem {
|
||||
protected process(entities: Entity[]): void {
|
||||
const entityManager = new EntityManager(this.scene);
|
||||
|
||||
// 查找所有有生命值的实体
|
||||
entityManager.forEachEntityWithComponent(HealthComponent, (entity, health) => {
|
||||
if (health.isDead()) {
|
||||
entity.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统到场景
|
||||
gameManager.scene.addEntityProcessor(new MovementSystem());
|
||||
gameManager.scene.addEntityProcessor(new HealthSystem());
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 事件系统
|
||||
|
||||
```typescript
|
||||
import { Core, CoreEvents } from '@esengine/ecs-framework';
|
||||
|
||||
// 监听框架事件
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onFrameUpdate, this);
|
||||
|
||||
// 发射自定义事件
|
||||
Core.emitter.emit("playerDied", { player: entity, score: 1000 });
|
||||
|
||||
// 移除监听
|
||||
Core.emitter.removeObserver(CoreEvents.frameUpdated, this.onFrameUpdate);
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
// 获取EntityManager性能统计
|
||||
const stats = entityManager.getStatistics();
|
||||
console.log(`总实体数: ${stats.entityCount}`);
|
||||
console.log(`索引命中率: ${stats.indexHits}/${stats.totalQueries}`);
|
||||
|
||||
// 获取各优化系统的统计
|
||||
console.log('组件索引:', entityManager.getComponentIndex().getPerformanceStats());
|
||||
console.log('Archetype:', entityManager.getArchetypeSystem().getStatistics());
|
||||
console.log('脏标记:', entityManager.getDirtyTrackingSystem().getPerformanceStats());
|
||||
```
|
||||
|
||||
## 简单示例
|
||||
|
||||
以下是一个完整的示例,展示了框架的主要功能:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Core,
|
||||
Entity,
|
||||
Component,
|
||||
Scene,
|
||||
EntitySystem,
|
||||
EntityManager
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// 游戏管理器
|
||||
class SimpleGame {
|
||||
private core: Core;
|
||||
private scene: Scene;
|
||||
private entityManager: EntityManager;
|
||||
|
||||
constructor() {
|
||||
this.core = Core.create(true);
|
||||
this.scene = new Scene();
|
||||
this.scene.name = "GameScene";
|
||||
Core.scene = this.scene;
|
||||
|
||||
this.entityManager = new EntityManager(this.scene);
|
||||
this.setupSystems();
|
||||
}
|
||||
|
||||
private setupSystems(): void {
|
||||
this.scene.addEntityProcessor(new MovementSystem());
|
||||
this.scene.addEntityProcessor(new HealthSystem());
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// 创建游戏实体
|
||||
this.createPlayer();
|
||||
this.createEnemies(50);
|
||||
|
||||
// 启动游戏循环
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
private createPlayer(): Entity {
|
||||
const player = this.entityManager.createEntity("Player");
|
||||
player.addComponent(new PositionComponent(400, 300));
|
||||
player.addComponent(new VelocityComponent(0, 0));
|
||||
player.addComponent(new HealthComponent(100));
|
||||
player.tag = "player";
|
||||
return player;
|
||||
}
|
||||
|
||||
private createEnemies(count: number): Entity[] {
|
||||
const enemies = this.entityManager.createEntities(count, "Enemy");
|
||||
|
||||
enemies.forEach((enemy, index) => {
|
||||
enemy.addComponent(new PositionComponent(
|
||||
Math.random() * 800,
|
||||
Math.random() * 600
|
||||
));
|
||||
enemy.addComponent(new VelocityComponent(
|
||||
(Math.random() - 0.5) * 100,
|
||||
(Math.random() - 0.5) * 100
|
||||
));
|
||||
enemy.addComponent(new HealthComponent(50));
|
||||
enemy.tag = "enemy";
|
||||
});
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
private gameLoop(): void {
|
||||
const update = () => {
|
||||
// 更新场景
|
||||
this.scene.update();
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// 启动游戏
|
||||
const game = new SimpleGame();
|
||||
game.start();
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 大规模实体处理
|
||||
- 使用 `EntityManager.createEntities()` 批量创建实体
|
||||
- 利用组件索引系统进行高效查询
|
||||
- 启用Archetype系统减少查询遍历
|
||||
|
||||
### 2. 查询优化
|
||||
- 使用 `EntityManager.query()` 流式API构建复杂查询
|
||||
- 缓存频繁查询的结果
|
||||
- 利用脏标记系统避免不必要的更新
|
||||
|
||||
### 3. 性能监控
|
||||
- 定期检查 `EntityManager.getStatistics()` 获取性能数据
|
||||
- 监控组件索引命中率
|
||||
- 使用框架提供的性能统计功能
|
||||
|
||||
## 下一步
|
||||
|
||||
现在您已经掌握了 ECS Framework 的基础用法,可以继续学习:
|
||||
|
||||
- [EntityManager 使用指南](entity-manager-example.md) - 详细了解实体管理器的高级功能
|
||||
- [性能优化指南](performance-optimization.md) - 深入了解三大性能优化系统
|
||||
- [核心概念](core-concepts.md) - 深入了解 ECS 架构和设计原理
|
||||
- [查询系统使用指南](query-system-usage.md) - 学习高性能查询系统的详细用法
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何在不同游戏引擎中集成?
|
||||
|
||||
A: ECS Framework 是引擎无关的,您只需要:
|
||||
1. 通过npm安装框架 `npm install @esengine/ecs-framework`
|
||||
2. 在游戏引擎的主循环中调用 `scene.update()`
|
||||
3. 根据需要集成渲染、输入等引擎特定功能
|
||||
|
||||
### Q: 如何处理输入?
|
||||
|
||||
A: 框架本身不提供输入处理,建议:
|
||||
1. 创建一个输入组件来存储输入状态
|
||||
2. 在游戏引擎的输入回调中更新输入组件
|
||||
3. 创建输入处理系统来响应输入状态
|
||||
|
||||
### Q: 如何优化大规模实体性能?
|
||||
|
||||
A: 关键优化策略:
|
||||
1. 使用 `EntityManager` 的高级查询功能
|
||||
2. 启用组件索引系统进行快速查询
|
||||
3. 利用Archetype系统减少查询遍历
|
||||
4. 使用脏标记系统避免不必要的更新
|
||||
|
||||
### Q: EntityManager 有什么优势?
|
||||
|
||||
A: EntityManager 提供了:
|
||||
- O(1) 复杂度的组件查询(使用索引)
|
||||
- 流式API的复杂查询构建
|
||||
- 自动的性能优化系统集成
|
||||
- 统一的实体管理接口
|
||||
@@ -1,64 +0,0 @@
|
||||
# 如何开始
|
||||
|
||||
## 初始化框架
|
||||
|
||||
```typescript
|
||||
// 参数为false则开启debug模式
|
||||
es.Core.create(false);
|
||||
```
|
||||
|
||||
## 分发帧事件
|
||||
|
||||
```typescript
|
||||
// 放置于引擎每帧更新处
|
||||
// dt为可选参数,传入引擎的deltaTime代替框架内的es.Time.deltaTime
|
||||
es.Core.emitter.emit(es.CoreEvents.frameUpdated, dt);
|
||||
```
|
||||
|
||||
> 尽可能使用引擎的dt,以免再游戏暂停继续时由于dt导致的跳帧问题
|
||||
|
||||
> **您还需要一个默认的场景以使得游戏可以进行使用ecs框架以及物理类或tween系统**
|
||||
|
||||
## 创建场景类
|
||||
|
||||
场景类需要继承框架中的 `es.Scene`
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
/**
|
||||
* 可重写方法,从contructor中调用这个函数
|
||||
*/
|
||||
initialize() {
|
||||
console.log('initialize');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可重写方法。当Core将这个场景设置为活动场景时调用
|
||||
*/
|
||||
onStart() {
|
||||
console.log('onStart');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可重写方法。当Core把这个场景从活动槽中移除时调用。
|
||||
*/
|
||||
unload() {
|
||||
console.log('unload');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可重写方法。
|
||||
*/
|
||||
update() {
|
||||
// 如果重写update方法 一定要调用该方法
|
||||
// 不调用将导致实体无法加入/组件无法更新
|
||||
super.update();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要想激活该场景需要通过核心类 `Core` 来设置当前 `MainScene` 为使用的场景
|
||||
|
||||
```typescript
|
||||
es.Core.scene = new MainScene();
|
||||
```
|
||||
497
docs/performance-optimization.md
Normal file
497
docs/performance-optimization.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# 性能优化指南
|
||||
|
||||
ECS Framework 提供了多层性能优化系统,确保在各种规模的游戏中都能提供卓越的性能表现。
|
||||
|
||||
## 性能优化架构
|
||||
|
||||
### 三大核心优化系统
|
||||
|
||||
1. **组件索引系统 (ComponentIndex)** - 提供 O(1) 组件查询性能
|
||||
2. **Archetype系统** - 按组件组合分组实体,减少查询遍历
|
||||
3. **脏标记系统 (DirtyTracking)** - 细粒度变更追踪,避免不必要更新
|
||||
|
||||
这三个系统协同工作,为不同场景提供最优的性能表现。
|
||||
|
||||
## 性能基准
|
||||
|
||||
### 核心操作性能
|
||||
|
||||
```
|
||||
实体创建: 640,000+ 个/秒
|
||||
组件查询: O(1) 复杂度(使用索引)
|
||||
内存优化: 30-50% 减少分配
|
||||
批量操作: 显著提升处理效率
|
||||
```
|
||||
|
||||
### 查询性能对比
|
||||
|
||||
| 查询类型 | 传统方式 | 使用索引 | 性能提升 |
|
||||
|----------|----------|----------|----------|
|
||||
| 单组件查询 | O(n) | O(1) | 1000x+ |
|
||||
| 多组件查询 | O(n*m) | O(k) | 100x+ |
|
||||
| 标签查询 | O(n) | O(1) | 1000x+ |
|
||||
| 复合查询 | O(n*m*k) | O(min(k1,k2)) | 500x+ |
|
||||
|
||||
*n=实体数量, m=组件种类, k=匹配实体数量*
|
||||
|
||||
## 组件索引系统
|
||||
|
||||
### 索引类型选择
|
||||
|
||||
框架提供两种索引实现:
|
||||
|
||||
#### 哈希索引 (HashComponentIndex)
|
||||
- **适用场景**: 通用查询,平衡的读写性能
|
||||
- **优势**: O(1) 查询,较低内存开销
|
||||
- **缺点**: 哈希冲突时性能下降
|
||||
|
||||
```typescript
|
||||
// 自动选择最优索引类型
|
||||
const componentIndex = entityManager.getComponentIndex();
|
||||
|
||||
// 手动配置哈希索引
|
||||
componentIndex.setIndexType(HealthComponent, 'hash');
|
||||
```
|
||||
|
||||
#### 位图索引 (BitmapComponentIndex)
|
||||
- **适用场景**: 大规模实体,频繁的组合查询
|
||||
- **优势**: 超快的 AND/OR 操作,空间压缩
|
||||
- **缺点**: 更新成本较高,内存开销随实体数量增长
|
||||
|
||||
```typescript
|
||||
// 配置位图索引用于大规模查询
|
||||
componentIndex.setIndexType(PositionComponent, 'bitmap');
|
||||
```
|
||||
|
||||
### 智能索引管理
|
||||
|
||||
ComponentIndexManager 会根据使用模式自动优化:
|
||||
|
||||
```typescript
|
||||
// 获取索引性能统计
|
||||
const stats = componentIndex.getPerformanceStats();
|
||||
console.log('索引性能:', {
|
||||
queriesPerSecond: stats.queriesPerSecond,
|
||||
hitRate: stats.hitRate,
|
||||
indexType: stats.recommendedType
|
||||
});
|
||||
|
||||
// 自动优化索引类型
|
||||
componentIndex.optimize(); // 根据使用模式切换索引类型
|
||||
```
|
||||
|
||||
## Archetype系统优化
|
||||
|
||||
### 原型分组策略
|
||||
|
||||
Archetype系统将实体按组件组合分组,实现快速批量操作:
|
||||
|
||||
```typescript
|
||||
// 获取Archetype统计
|
||||
const archetypeSystem = entityManager.getArchetypeSystem();
|
||||
const stats = archetypeSystem.getStatistics();
|
||||
|
||||
console.log('Archetype优化:', {
|
||||
totalArchetypes: stats.totalArchetypes, // 原型数量
|
||||
avgEntitiesPerArchetype: stats.averageEntitiesPerArchetype,
|
||||
queryCacheHits: stats.queryCacheHits // 缓存命中次数
|
||||
});
|
||||
```
|
||||
|
||||
### 查询缓存机制
|
||||
|
||||
```typescript
|
||||
// 启用查询缓存(默认开启)
|
||||
archetypeSystem.enableQueryCache(true);
|
||||
|
||||
// 缓存大小限制(避免内存泄漏)
|
||||
archetypeSystem.setMaxCacheSize(1000);
|
||||
|
||||
// 清理过期缓存
|
||||
archetypeSystem.cleanCache();
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **组件设计**: 避免创建过多单独的原型
|
||||
2. **批量操作**: 利用原型批量处理相同组件组合的实体
|
||||
3. **缓存管理**: 定期清理查询缓存
|
||||
|
||||
```typescript
|
||||
// ✅ 好的设计:复用组件组合
|
||||
class MovementSystem extends EntitySystem {
|
||||
process() {
|
||||
// 一次查询处理所有移动实体
|
||||
const movingEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, VelocityComponent])
|
||||
.execute(); // 利用Archetype快速获取
|
||||
|
||||
// 批量处理
|
||||
movingEntities.forEach(entity => {
|
||||
// 更新逻辑
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免:频繁查询不同组合
|
||||
class BadSystem extends EntitySystem {
|
||||
process() {
|
||||
// 多次小查询,无法充分利用Archetype
|
||||
const players = this.queryPlayers();
|
||||
const enemies = this.queryEnemies();
|
||||
const bullets = this.queryBullets();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 脏标记系统优化
|
||||
|
||||
### 脏标记类型
|
||||
|
||||
系统提供细粒度的脏标记追踪:
|
||||
|
||||
```typescript
|
||||
enum DirtyType {
|
||||
COMPONENT_ADDED, // 组件添加
|
||||
COMPONENT_REMOVED, // 组件移除
|
||||
COMPONENT_MODIFIED, // 组件修改
|
||||
ENTITY_ENABLED, // 实体启用
|
||||
ENTITY_DISABLED, // 实体禁用
|
||||
TAG_ADDED, // 标签添加
|
||||
TAG_REMOVED // 标签移除
|
||||
}
|
||||
```
|
||||
|
||||
### 批量处理配置
|
||||
|
||||
```typescript
|
||||
const dirtyTracking = entityManager.getDirtyTrackingSystem();
|
||||
|
||||
// 配置批量处理参数
|
||||
dirtyTracking.configure({
|
||||
batchSize: 100, // 每批处理100个脏标记
|
||||
timeSliceMs: 16, // 每帧最多处理16ms
|
||||
processingInterval: 1 // 每帧处理一次
|
||||
});
|
||||
|
||||
// 监听脏标记事件
|
||||
dirtyTracking.addListener(DirtyType.COMPONENT_MODIFIED, (entity, component) => {
|
||||
// 响应组件修改
|
||||
this.invalidateRenderCache(entity);
|
||||
}, { priority: 10 });
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
|
||||
```typescript
|
||||
const dirtyStats = dirtyTracking.getPerformanceStats();
|
||||
console.log('脏标记性能:', {
|
||||
totalMarks: dirtyStats.totalMarks,
|
||||
batchesProcessed: dirtyStats.batchesProcessed,
|
||||
averageBatchTime: dirtyStats.averageBatchTime,
|
||||
queueSize: dirtyStats.currentQueueSize
|
||||
});
|
||||
```
|
||||
|
||||
## 查询优化策略
|
||||
|
||||
### 查询层次选择
|
||||
|
||||
根据查询复杂度选择最优方法:
|
||||
|
||||
```typescript
|
||||
// 1. 简单查询:直接使用索引
|
||||
const healthEntities = entityManager.getEntitiesWithComponent(HealthComponent);
|
||||
|
||||
// 2. 双组件查询:使用Archetype
|
||||
const movingEntities = entityManager.getEntitiesWithComponents([
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
]);
|
||||
|
||||
// 3. 复杂查询:组合使用
|
||||
const combatants = entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, HealthComponent]) // Archetype预筛选
|
||||
.withTag("combat") // 索引过滤
|
||||
.where(entity => { // 自定义精确过滤
|
||||
const health = entity.getComponent(HealthComponent);
|
||||
return health.currentHealth > health.maxHealth * 0.3;
|
||||
})
|
||||
.execute();
|
||||
```
|
||||
|
||||
### 查询缓存策略
|
||||
|
||||
```typescript
|
||||
class CombatSystem extends EntitySystem {
|
||||
private cachedEnemies: Entity[] = [];
|
||||
private lastEnemyCacheUpdate = 0;
|
||||
|
||||
process() {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// 每200ms更新一次敌人缓存
|
||||
if (currentTime - this.lastEnemyCacheUpdate > 200) {
|
||||
this.cachedEnemies = this.entityManager
|
||||
.getEntitiesByTag("enemy");
|
||||
this.lastEnemyCacheUpdate = currentTime;
|
||||
}
|
||||
|
||||
// 使用缓存的结果
|
||||
this.processCombat(this.cachedEnemies);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内存优化
|
||||
|
||||
### 内存使用监控
|
||||
|
||||
```typescript
|
||||
// 获取各系统内存使用情况
|
||||
const memoryStats = entityManager.getMemoryUsage();
|
||||
console.log('内存使用情况:', {
|
||||
entityIndex: memoryStats.entityIndex, // 实体索引
|
||||
componentIndex: memoryStats.componentIndex, // 组件索引
|
||||
archetype: memoryStats.archetype, // 原型系统
|
||||
dirtyTracking: memoryStats.dirtyTracking, // 脏标记
|
||||
total: memoryStats.total
|
||||
});
|
||||
```
|
||||
|
||||
### 内存清理策略
|
||||
|
||||
```typescript
|
||||
// 定期内存清理
|
||||
setInterval(() => {
|
||||
entityManager.cleanup(); // 清理无效引用
|
||||
entityManager.compact(); // 压缩数据结构
|
||||
}, 30000); // 每30秒清理一次
|
||||
|
||||
// 游戏场景切换时的深度清理
|
||||
function switchScene() {
|
||||
entityManager.destroyAllEntities();
|
||||
entityManager.cleanup();
|
||||
entityManager.compact();
|
||||
|
||||
// 重置优化系统
|
||||
entityManager.getComponentIndex().reset();
|
||||
entityManager.getArchetypeSystem().clearCache();
|
||||
entityManager.getDirtyTrackingSystem().clear();
|
||||
}
|
||||
```
|
||||
|
||||
## 实战优化案例
|
||||
|
||||
### 大规模射击游戏优化
|
||||
|
||||
```typescript
|
||||
class BulletSystem extends EntitySystem {
|
||||
private bulletPool: Entity[] = [];
|
||||
private maxBullets = 1000;
|
||||
|
||||
constructor(entityManager: EntityManager) {
|
||||
super();
|
||||
this.prewarmBulletPool();
|
||||
}
|
||||
|
||||
private prewarmBulletPool() {
|
||||
// 预创建子弹池
|
||||
this.bulletPool = this.entityManager.createEntities(
|
||||
this.maxBullets,
|
||||
"Bullet"
|
||||
);
|
||||
|
||||
// 初始化为非激活状态
|
||||
this.bulletPool.forEach(bullet => {
|
||||
bullet.enabled = false;
|
||||
bullet.addComponent(new PositionComponent());
|
||||
bullet.addComponent(new VelocityComponent());
|
||||
bullet.addComponent(new BulletComponent());
|
||||
});
|
||||
}
|
||||
|
||||
public spawnBullet(x: number, y: number, vx: number, vy: number): Entity | null {
|
||||
// 从池中获取非激活子弹(使用索引快速查询)
|
||||
const availableBullet = this.entityManager
|
||||
.query()
|
||||
.withAll([BulletComponent])
|
||||
.active(false)
|
||||
.first();
|
||||
|
||||
if (availableBullet) {
|
||||
// 重用现有子弹
|
||||
const pos = availableBullet.getComponent(PositionComponent);
|
||||
const vel = availableBullet.getComponent(VelocityComponent);
|
||||
|
||||
pos.x = x; pos.y = y;
|
||||
vel.x = vx; vel.y = vy;
|
||||
availableBullet.enabled = true;
|
||||
|
||||
return availableBullet;
|
||||
}
|
||||
|
||||
return null; // 池已满
|
||||
}
|
||||
|
||||
process() {
|
||||
// 批量处理所有激活的子弹
|
||||
this.entityManager.forEachEntityWithComponent(
|
||||
BulletComponent,
|
||||
(entity, bullet) => {
|
||||
if (!entity.enabled) return;
|
||||
|
||||
// 更新位置
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const vel = entity.getComponent(VelocityComponent);
|
||||
|
||||
pos.x += vel.x * Time.deltaTime;
|
||||
pos.y += vel.y * Time.deltaTime;
|
||||
|
||||
// 边界检查,回收到池中
|
||||
if (pos.x < 0 || pos.x > 800 || pos.y < 0 || pos.y > 600) {
|
||||
entity.enabled = false; // 回收而不是销毁
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI系统性能优化
|
||||
|
||||
```typescript
|
||||
class AISystem extends EntitySystem {
|
||||
private spatialGrid: SpatialGrid;
|
||||
private updateFrequency = 60; // 60Hz更新频率
|
||||
private lastUpdate = 0;
|
||||
|
||||
process() {
|
||||
const currentTime = performance.now();
|
||||
|
||||
// 控制更新频率
|
||||
if (currentTime - this.lastUpdate < 1000 / this.updateFrequency) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用空间分区优化邻居查询
|
||||
const aiEntities = this.entityManager
|
||||
.query()
|
||||
.withAll([PositionComponent, AIComponent])
|
||||
.active(true)
|
||||
.execute();
|
||||
|
||||
// 分批处理AI实体
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < aiEntities.length; i += batchSize) {
|
||||
const batch = aiEntities.slice(i, i + batchSize);
|
||||
this.processBatch(batch);
|
||||
|
||||
// 时间片控制,避免单帧卡顿
|
||||
if (performance.now() - currentTime > 10) { // 10ms时间片
|
||||
break; // 下一帧继续处理
|
||||
}
|
||||
}
|
||||
|
||||
this.lastUpdate = currentTime;
|
||||
}
|
||||
|
||||
private processBatch(entities: Entity[]) {
|
||||
entities.forEach(entity => {
|
||||
const pos = entity.getComponent(PositionComponent);
|
||||
const ai = entity.getComponent(AIComponent);
|
||||
|
||||
// 空间查询优化邻居搜索
|
||||
const neighbors = this.spatialGrid.queryRadius(pos.x, pos.y, ai.sightRange);
|
||||
|
||||
// AI决策逻辑
|
||||
ai.update(neighbors);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能监控工具
|
||||
|
||||
### 实时性能仪表板
|
||||
|
||||
```typescript
|
||||
class PerformanceDashboard {
|
||||
private stats: any = {};
|
||||
private updateInterval = 1000; // 1秒更新一次
|
||||
|
||||
constructor(private entityManager: EntityManager) {
|
||||
setInterval(() => this.updateStats(), this.updateInterval);
|
||||
}
|
||||
|
||||
private updateStats() {
|
||||
this.stats = {
|
||||
// 基础统计
|
||||
entities: this.entityManager.getStatistics(),
|
||||
|
||||
// 组件索引
|
||||
componentIndex: this.entityManager.getComponentIndex().getPerformanceStats(),
|
||||
|
||||
// Archetype系统
|
||||
archetype: this.entityManager.getArchetypeSystem().getStatistics(),
|
||||
|
||||
// 脏标记系统
|
||||
dirtyTracking: this.entityManager.getDirtyTrackingSystem().getPerformanceStats(),
|
||||
|
||||
// 内存使用
|
||||
memory: this.entityManager.getMemoryUsage(),
|
||||
|
||||
// 计算性能指标
|
||||
performance: this.calculatePerformanceMetrics()
|
||||
};
|
||||
|
||||
this.displayStats();
|
||||
}
|
||||
|
||||
private calculatePerformanceMetrics() {
|
||||
const componentStats = this.stats.componentIndex;
|
||||
const archetypeStats = this.stats.archetype;
|
||||
|
||||
return {
|
||||
queryHitRate: componentStats.hitRate,
|
||||
archetypeEfficiency: archetypeStats.averageEntitiesPerArchetype,
|
||||
memoryEfficiency: this.stats.memory.compressionRatio,
|
||||
overallPerformance: this.calculateOverallScore()
|
||||
};
|
||||
}
|
||||
|
||||
private displayStats() {
|
||||
console.log('=== ECS性能仪表板 ===');
|
||||
console.log('查询命中率:', this.stats.performance.queryHitRate.toFixed(2) + '%');
|
||||
console.log('内存使用:', (this.stats.memory.total / 1024 / 1024).toFixed(2) + 'MB');
|
||||
console.log('整体性能评分:', this.stats.performance.overallPerformance.toFixed(1) + '/10');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 优化检查清单
|
||||
|
||||
### 开发阶段
|
||||
|
||||
- [ ] 使用EntityManager而不是直接操作Scene
|
||||
- [ ] 优先使用组件查询和标签查询
|
||||
- [ ] 设计合理的组件组合,避免过度碎片化
|
||||
- [ ] 实现对象池机制减少频繁创建/销毁
|
||||
|
||||
### 运行时优化
|
||||
|
||||
- [ ] 监控查询命中率,保持在80%以上
|
||||
- [ ] 控制Archetype数量,避免过度分散
|
||||
- [ ] 配置适当的脏标记批量处理参数
|
||||
- [ ] 定期进行内存清理和数据压缩
|
||||
|
||||
### 性能监控
|
||||
|
||||
- [ ] 定期检查性能统计数据
|
||||
- [ ] 监控内存使用趋势
|
||||
- [ ] 设置性能预警阈值
|
||||
- [ ] 在不同设备上进行性能测试
|
||||
|
||||
通过系统性地应用这些优化策略,您可以构建出在各种规模下都能提供卓越性能的ECS游戏系统。
|
||||
306
docs/performance.md
Normal file
306
docs/performance.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# ECS框架性能基准
|
||||
|
||||
本文档展示了ECS框架的真实性能数据和瓶颈分析。
|
||||
|
||||
## 🚀 快速测试
|
||||
|
||||
```bash
|
||||
# 快速性能基准测试
|
||||
npm run benchmark
|
||||
|
||||
# 完整性能测试
|
||||
npm run test:performance
|
||||
|
||||
# 单元测试
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
## 📊 性能基准数据
|
||||
|
||||
> 测试环境: Node.js, Windows 10, 现代桌面CPU
|
||||
|
||||
### 1. 实体创建性能
|
||||
|
||||
| 实体数量 | 创建时间 | 创建速度 | 每个实体耗时 | 性能等级 |
|
||||
|---------|---------|---------|-------------|---------|
|
||||
| 1,000 | 1.56ms | 640,697个/秒 | 0.0016ms | 🚀 极致 |
|
||||
| 5,000 | 19.47ms | 256,805个/秒 | 0.0039ms | 🚀 极致 |
|
||||
| 10,000 | 39.94ms | 250,345个/秒 | 0.0040ms | 🚀 极致 |
|
||||
| 50,000 | 258.17ms | 193,673个/秒 | 0.0052ms | ✅ 优秀 |
|
||||
| 100,000 | 463.04ms | 215,963个/秒 | 0.0046ms | ✅ 优秀 |
|
||||
| 500,000 | 3,087ms | 161,990个/秒 | 0.0062ms | ✅ 优秀 |
|
||||
|
||||
**结论**: 🚀 实体创建性能达到极致水平,大规模创建50万实体仅需3秒
|
||||
|
||||
### 2. 性能瓶颈分析 (500,000个实体)
|
||||
|
||||
**当前瓶颈分布**:
|
||||
```
|
||||
实体创建: 46.3% (1,429ms)
|
||||
组件添加: 53.5% (1,651ms) ← 主要瓶颈
|
||||
标签分配: 0.2% (7ms)
|
||||
```
|
||||
|
||||
**特征**: 框架实现了均衡的性能分布,各部分开销相对合理
|
||||
|
||||
### 3. 组件添加性能详细分析
|
||||
|
||||
| 组件类型 | 添加速度 | 平均耗时 | 性能等级 |
|
||||
|---------|---------|---------|---------|
|
||||
| PositionComponent | 596,929组件/秒 | 0.0017ms | 🚀 极致 |
|
||||
| VelocityComponent | 1,186,770组件/秒 | 0.0008ms | 🚀 极致 |
|
||||
| HealthComponent | 841,982组件/秒 | 0.0012ms | 🚀 极致 |
|
||||
| RenderComponent | 763,351组件/秒 | 0.0013ms | 🚀 极致 |
|
||||
| AIComponent | 185,964组件/秒 | 0.0054ms | ✅ 优秀 |
|
||||
|
||||
### 4. 优化技术性能影响
|
||||
|
||||
| 优化技术 | 性能提升 | 内存影响 | 适用场景 |
|
||||
|---------|---------|---------|---------|
|
||||
| 组件对象池 | 30-50% | 减少分配 | 频繁创建/销毁 |
|
||||
| 位掩码优化器 | 20-40% | 缓存开销 | 大量查询操作 |
|
||||
| 批量操作 | 显著提升 | 无明显影响 | 大规模实体创建 |
|
||||
| 延迟索引更新 | 60-80% | 临时内存增加 | 批量实体操作 |
|
||||
| 索引去重优化 | 避免O(n) | 轻微内存增加 | 防止重复实体 |
|
||||
|
||||
### 5. 查询系统性能
|
||||
|
||||
#### 5.1 基础查询性能
|
||||
| 查询类型 | 查询速度 | 每次查询耗时 | 性能等级 |
|
||||
|---------|---------|-------------|---------|
|
||||
| 单组件查询 | 12,178次/秒 | 0.082ms | ✅ 优秀 |
|
||||
| 多组件查询 | 9,439次/秒 | 0.106ms | ✅ 优秀 |
|
||||
| 复合查询 | 7,407次/秒 | 0.135ms | ✅ 良好 |
|
||||
|
||||
#### 5.2 缓存查询性能
|
||||
| 缓存状态 | 访问速度 | 性能特征 |
|
||||
|---------|---------|---------|
|
||||
| 缓存命中 | 零延迟 | 🚀 即时响应 |
|
||||
| 缓存未命中 | 标准查询 | ✅ 自动构建 |
|
||||
| 缓存清理 | 批量延迟 | 🔧 优化策略 |
|
||||
|
||||
### 6. 新功能性能基准
|
||||
|
||||
#### 6.1 组件对象池性能
|
||||
```
|
||||
📊 对象池 vs 直接创建 (10,000次操作)
|
||||
对象池获取: 1.65ms (6,060,606次/秒)
|
||||
直接创建: 1.51ms (6,622,516次/秒)
|
||||
|
||||
⚠️ 小规模测试中对象池可能略慢,但在大规模应用中:
|
||||
- 减少30-50%的内存分配
|
||||
- 避免垃圾回收压力
|
||||
- 提升长期运行稳定性
|
||||
```
|
||||
|
||||
#### 6.2 位掩码优化器性能
|
||||
```
|
||||
🔥 位掩码操作性能 (100,000次操作)
|
||||
单个掩码创建: 20.00ms (5,000,000次/秒)
|
||||
组合掩码创建: 53.69ms (1,862,285次/秒)
|
||||
缓存掩码访问: <1ms (近零延迟)
|
||||
```
|
||||
|
||||
## 🎯 性能扩展性分析
|
||||
|
||||
### 实体创建扩展性
|
||||
```
|
||||
📈 创建速度趋势分析
|
||||
1K-10K实体: 250,000-640,000 实体/秒 (优秀)
|
||||
10K-100K实体: 200,000-250,000 实体/秒 (良好)
|
||||
100K-500K实体: 160,000-220,000 实体/秒 (稳定)
|
||||
|
||||
结论: 性能随规模稳定下降,无突然性能悬崖
|
||||
```
|
||||
|
||||
### 内存使用效率
|
||||
| 实体数量 | 内存使用 | 每实体内存 | 内存效率 |
|
||||
|---------|---------|-----------|---------|
|
||||
| 1,000 | 3.5MB | 3.5KB | 🚀 极致 |
|
||||
| 5,000 | 7.1MB | 1.4KB | 🚀 极致 |
|
||||
| 10,000 | 20.8MB | 2.1KB | ✅ 优秀 |
|
||||
| 50,000 | ~100MB | ~2KB | ✅ 优秀 |
|
||||
|
||||
## 💡 性能优化建议
|
||||
|
||||
### 1. 实体创建最佳实践
|
||||
|
||||
**✅ 推荐做法**:
|
||||
```typescript
|
||||
// 使用批量创建API
|
||||
const entities = scene.createEntities(10000, "Enemies");
|
||||
|
||||
// 延迟缓存清理
|
||||
entities.forEach(entity => {
|
||||
scene.addEntity(entity, false); // 延迟清理
|
||||
});
|
||||
scene.querySystem.clearCache(); // 手动清理
|
||||
```
|
||||
|
||||
**❌ 避免做法**:
|
||||
```typescript
|
||||
// 避免循环单个创建
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
scene.createEntity("Enemy" + i); // 每次触发缓存清理
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 组件池优化策略
|
||||
|
||||
**预热策略**:
|
||||
```typescript
|
||||
// 预热常用组件池
|
||||
ComponentPoolManager.getInstance().preWarmPools({
|
||||
BulletComponent: 2000, // 子弹大量创建
|
||||
EffectComponent: 1000, // 特效频繁使用
|
||||
PickupComponent: 500 // 道具适量缓存
|
||||
});
|
||||
```
|
||||
|
||||
**使用模式**:
|
||||
```typescript
|
||||
// 高效的组件复用
|
||||
const bullet = ComponentPoolManager.getInstance().getComponent(BulletComponent);
|
||||
bullet.reset(); // 重置状态
|
||||
entity.addComponent(bullet);
|
||||
|
||||
// 销毁时释放到池
|
||||
ComponentPoolManager.getInstance().releaseComponent(bullet);
|
||||
```
|
||||
|
||||
### 3. 查询优化策略
|
||||
|
||||
**缓存策略**:
|
||||
```typescript
|
||||
// 缓存频繁查询结果
|
||||
class MovementSystem extends EntitySystem {
|
||||
private cachedMovingEntities: Entity[];
|
||||
private lastCacheFrame: number = 0;
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
// 每5帧更新一次缓存
|
||||
if (Time.frameCount - this.lastCacheFrame > 5) {
|
||||
this.cachedMovingEntities = scene.getEntitiesWithComponents([Position, Velocity]);
|
||||
this.lastCacheFrame = Time.frameCount;
|
||||
}
|
||||
|
||||
// 使用缓存结果
|
||||
this.processMovement(this.cachedMovingEntities);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 不同规模应用建议
|
||||
|
||||
#### 小型游戏 (< 5,000实体)
|
||||
- ✅ 可以随意使用所有功能
|
||||
- ✅ 不需要特殊优化
|
||||
- ✅ 专注于游戏逻辑开发
|
||||
|
||||
#### 中型游戏 (5,000-50,000实体)
|
||||
- ✅ 使用批量操作API
|
||||
- ✅ 启用组件对象池
|
||||
- ⚠️ 注意查询频率
|
||||
|
||||
#### 大型游戏 (50,000+实体)
|
||||
- 🚀 必须使用批量操作
|
||||
- 🚀 必须启用对象池
|
||||
- 🚀 必须缓存查询结果
|
||||
- 🚀 考虑分区处理
|
||||
|
||||
## 🌍 平台性能对比
|
||||
|
||||
### Windows 桌面端 (测试平台)
|
||||
- **实体创建**: 640,697实体/秒
|
||||
- **组件操作**: 596,929组件/秒
|
||||
- **推荐实体数**: ≤ 200,000
|
||||
|
||||
### 预估其他平台性能
|
||||
|
||||
| 平台类型 | 预估性能比例 | 推荐实体数 | 特殊注意 |
|
||||
|---------|-------------|-----------|---------|
|
||||
| macOS桌面 | 90-100% | ≤ 180,000 | 内存管理优秀 |
|
||||
| Linux桌面 | 95-105% | ≤ 200,000 | 性能最优 |
|
||||
| Chrome浏览器 | 60-80% | ≤ 100,000 | V8引擎优化 |
|
||||
| Firefox浏览器 | 50-70% | ≤ 80,000 | SpiderMonkey限制 |
|
||||
| Safari浏览器 | 55-75% | ≤ 90,000 | JavaScriptCore |
|
||||
| Node.js服务器 | 100-110% | ≤ 500,000 | 服务器级性能 |
|
||||
| Android Chrome | 30-50% | ≤ 30,000 | 移动端限制 |
|
||||
| iOS Safari | 40-60% | ≤ 40,000 | iOS优化较好 |
|
||||
|
||||
## 🔬 测试环境详情
|
||||
|
||||
### 硬件环境
|
||||
- **操作系统**: Windows 10 (Build 26100)
|
||||
- **处理器**: 现代桌面CPU
|
||||
- **内存**: 充足RAM
|
||||
- **存储**: SSD高速存储
|
||||
|
||||
### 软件环境
|
||||
- **Node.js**: v16+
|
||||
- **TypeScript**: v5.8.3
|
||||
- **ECS框架版本**: v2.0.6
|
||||
- **测试工具**: 内置基准测试套件
|
||||
|
||||
### 测试方法
|
||||
- **实体配置**: 位置、速度、生命值、渲染、AI组件随机分配
|
||||
- **测试迭代**: 多次测试取平均值
|
||||
- **内存监控**: 实时内存使用情况
|
||||
- **性能指标**: performance.now()高精度计时
|
||||
|
||||
## 📋 性能测试清单
|
||||
|
||||
### 运行完整性能测试
|
||||
|
||||
```bash
|
||||
# 1. 快速基准测试 (2-3分钟)
|
||||
npm run benchmark
|
||||
|
||||
# 2. 完整性能测试 (10-15分钟)
|
||||
npm run test:performance
|
||||
|
||||
# 3. 单元测试验证 (30秒)
|
||||
npm run test:unit
|
||||
|
||||
# 4. 所有测试 (15-20分钟)
|
||||
npm run test
|
||||
```
|
||||
|
||||
### 自定义性能测试
|
||||
|
||||
```typescript
|
||||
import { runEntityCreationBenchmark } from '@esengine/ecs-framework/Testing/Performance/benchmark';
|
||||
|
||||
// 自定义规模测试
|
||||
await runEntityCreationBenchmark([1000, 5000, 10000]);
|
||||
|
||||
// 组件性能测试
|
||||
await runComponentPerformanceTest();
|
||||
|
||||
// 查询性能测试
|
||||
await runQueryPerformanceTest();
|
||||
```
|
||||
|
||||
## 🏆 性能总结
|
||||
|
||||
### 🎯 核心能力
|
||||
1. **实体创建速度**: 最高64万实体/秒
|
||||
2. **大规模处理**: 50万实体仅需3秒创建
|
||||
3. **均衡性能**: 各组件开销分布合理
|
||||
4. **扩展性**: 性能随规模线性下降,无突然悬崖
|
||||
|
||||
### 🔧 技术特点
|
||||
1. **批量操作架构** - 大幅减少单次操作开销
|
||||
2. **智能缓存策略** - 延迟清理机制
|
||||
3. **索引系统优化** - 避免O(n)操作
|
||||
4. **内存管理优化** - 对象池和位掩码缓存
|
||||
|
||||
### 🌟 实际应用价值
|
||||
- **小型游戏**: 性能过剩,专注玩法
|
||||
- **中型游戏**: 性能充足,适度优化
|
||||
- **大型游戏**: 需要优化策略,但完全可行
|
||||
- **服务器端**: 可处理大规模实体管理
|
||||
|
||||
---
|
||||
|
||||
**结论**: ECS框架达到了产品级性能标准,能够满足从休闲小游戏到复杂RTS游戏的各种需求。框架层面的性能已经充分优化,为开发者提供了坚实的性能基础。
|
||||
@@ -1,73 +0,0 @@
|
||||
## 关于 Physics/Collision
|
||||
框架中的物理不是一个真实的物理模拟。它只提供了游戏物理。您可以执行一些操作,如检测碰撞器、重叠检查、碰撞检查、扫描测试等。不是一个完整的刚体模拟。
|
||||
|
||||
### Colliders 物理系统的根本
|
||||
没有Collider,在物理系统中什么也不会发生。 碰撞器存在于实体类中,有几种类型:BoxCollider,CircleCollider和PolygonCollider。 您可以像这样添加Collider:`entity.addComponent(new BoxCollider())`. 将碰撞器添加到Entity时,它们会自动添加到SpatialHash中。
|
||||
|
||||
### SpatialHash:你永远不会用到它,但它仍然对你很重要
|
||||
SpatialHash类是一个隐藏类,该类为您的游戏全局管理 `collider`。静态物理类是SpatialHash的公共包装器。 SpatialHash没有设置大小限制,用于快速进行碰撞/线投射/重叠检查。例如,如果你有一个英雄在世界各地移动,而不必检查每个对撞机(可能是数百个)是否发生碰撞,则只需向SpatialHash询问英雄附近的所有collider即可。这大大缩小了您的碰撞检查范围。
|
||||
|
||||
SpatialHash有一个可配置的方面,它可以极大地影响其性能:单元大小。 SpatialHash将空间分成一个网格,选择适当的网格大小可以将可能发生的碰撞查询减到最少。默认情况下,网格大小为100像素。您可以通过在创建场景之前设置`Physics.SpatialHashCellSize`来更改此设置。选择比您的平均玩家/敌人人数稍大的尺寸通常效果最佳。
|
||||
|
||||
### Physics 类
|
||||
物理类是物理的核心类。 您可以设置一些属性,例如前面提到的spatialHashCellSize,raycastsHitTriggers和raycastsStartInColliders。
|
||||
- linecast:从开始到结束投射一条线,并返回与layerMask相匹配的碰撞器的第一次命中
|
||||
- overlapRectangle:检查是否有任何collider在矩形区域内
|
||||
- overlapCircle:检查是否有任何collider在圆形区域内
|
||||
- boxcastBroadphase:返回边界与collider.bounds相交的所有碰撞器。 请注意,这是一个broadphase检查,因此它只检查边界,不执行单个碰撞器到碰撞器的检查!
|
||||
|
||||
会注意到上面提到的layerMask。 layerMask允许您确定与哪些碰撞器碰撞。 每个collider都可以设置其物理层,以便在查询物理系统时可以选择仅检索与传入的layerMask匹配的对撞机。 所有物理方法都接受默认为所有图层的图层蒙版参数。 使用此选项可以过滤冲突检查,并通过不执行不必要的冲突检查来使性能保持最佳状态。
|
||||
|
||||
### 使用物理系统
|
||||
射线检测对于检查敌人的视线、探测实体的空间环境、快速移动的子弹等各种事情都非常有用。下面是一个从头到尾投射线条的示例,如果击中某个物体,它只会记录数据:
|
||||
```ts
|
||||
const hit = Physics.linecast( start, end );
|
||||
if( hit.Collider != null )
|
||||
console.log( `ray hit ${hit}, entity: {hit.collider.entity}`);
|
||||
```
|
||||
|
||||
我们使用了一些更先进的碰撞/重叠检查方法,如Minkowski和、分离轴定理和古老的三角法。这些都被包装在Collider类上的简单易用的方法中。让我们看一些例子。
|
||||
|
||||
第一个例子是处理碰撞的最简单方法。deltaMovement是您希望移动实体的量,通常是velocity*Time.deltaTime. collidesWithAny方法将检查所有碰撞并调整deltaMovement以解决任何碰撞。
|
||||
|
||||
```ts
|
||||
// 碰撞结果将包含一些非常有用的信息,例如被撞的collider,表面命中的法线和最小平移矢量(MTV)。 MTV可用于将碰撞实体直接移动到命中的碰撞器附近。
|
||||
let collisionResult = null;
|
||||
|
||||
// 进行检查以查看entity.getComponent(Collider)(实体上的第一个碰撞器)是否与场景中的任何其他碰撞器发生碰撞。请注意,如果您有多个碰撞器,则可以获取并遍历它们,而不是仅检查第一个碰撞器。
|
||||
if( entity.getComponent(Collider).collidesWithAny( deltaMovement, collisionResult ) )
|
||||
{
|
||||
// 记录CollisionResult。 您可能需要使用它来添加一些粒子效果或与您的游戏相关的任何其他内容。
|
||||
console.log( `collision result: ${collisionResult}` );
|
||||
}
|
||||
|
||||
// 将实体移到新位置。 已经调整了deltaMovement为我们解决冲突。
|
||||
entity.position = entity.position.add(deltaMovement);
|
||||
```
|
||||
|
||||
如果您需要对碰撞发生时的情况进行更多控制,则也可以手动检查是否与其他collider发生碰撞。 请注意,执行此操作时,deltaMovement不会为您调整。 解决冲突时,您需要考虑最小平移矢量。
|
||||
|
||||
```ts
|
||||
let collisionResult = null;
|
||||
|
||||
// 进行检查以查看entity.getComponent<Collider>是否与一些其他Collider发生碰撞
|
||||
if( entity.getComponent(Collider).collidesWith( someOtherCollider, deltaMovement, collisionResult ) )
|
||||
{
|
||||
// 将实体移动到与命中Collider相邻的位置,然后记录CollisionResult
|
||||
entity.position = entity.position.add(deltaMovement.sub(collisionResult.minimumTranslationVector));
|
||||
console.log( `collision result: ${collisionResult}` );
|
||||
}
|
||||
```
|
||||
我们可以使用前面提到的Physics.boxcastBroadphase方法或更具体地讲,将自身排除在查询之外的版本,使上述示例更进一步。 该方法将为我们提供场景中所有在我们附近的collider,然后我们可以使用这些对撞机进行实际的碰撞检查。
|
||||
|
||||
```ts
|
||||
// 在我们自身以外的位置获取可能与之重叠的任何东西
|
||||
let neighborColliders = Physics.boxcastBroadphaseExcludingSelf( entity.getComponent(Collider) );
|
||||
|
||||
// 遍历并检查每个对撞机是否重叠
|
||||
for( let collider of neighborColliders )
|
||||
{
|
||||
if( entity.getComponent(Collider).overlaps( collider ) )
|
||||
console.log( `我们正在重叠一个collider : ${collider}` );
|
||||
}
|
||||
```
|
||||
324
docs/query-system-usage.md
Normal file
324
docs/query-system-usage.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# QuerySystem 使用指南
|
||||
|
||||
QuerySystem 是 ECS Framework 中的高性能实体查询系统,支持多级索引、智能缓存和类型安全的查询操作。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 获取查询系统
|
||||
|
||||
```typescript
|
||||
import { Scene, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
// 创建场景,查询系统会自动创建
|
||||
const scene = new Scene();
|
||||
const querySystem = scene.querySystem;
|
||||
|
||||
// 或者从Core获取当前场景的查询系统
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
const currentQuerySystem = Core.scene?.querySystem;
|
||||
```
|
||||
|
||||
### 2. 基本查询操作
|
||||
|
||||
```typescript
|
||||
// 查询包含所有指定组件的实体
|
||||
const result = querySystem.queryAll(PositionComponent, VelocityComponent);
|
||||
console.log(`找到 ${result.count} 个实体`);
|
||||
|
||||
// 查询包含任意指定组件的实体
|
||||
const anyResult = querySystem.queryAny(HealthComponent, ManaComponent);
|
||||
|
||||
// 查询不包含指定组件的实体
|
||||
const noneResult = querySystem.queryNone(DeadComponent);
|
||||
```
|
||||
|
||||
### 3. 类型安全查询
|
||||
|
||||
```typescript
|
||||
// 类型安全的查询,返回实体和对应的组件
|
||||
const typedResult = querySystem.queryAllTyped(PositionComponent, VelocityComponent);
|
||||
for (let i = 0; i < typedResult.entities.length; i++) {
|
||||
const entity = typedResult.entities[i];
|
||||
const [position, velocity] = typedResult.components[i];
|
||||
// position 和 velocity 都是类型安全的
|
||||
}
|
||||
|
||||
// 查询单个组件类型
|
||||
const healthEntities = querySystem.queryComponentTyped(HealthComponent);
|
||||
healthEntities.forEach(({ entity, component }) => {
|
||||
console.log(`实体 ${entity.name} 的生命值: ${component.value}`);
|
||||
});
|
||||
|
||||
// 查询两个组件类型
|
||||
const movableEntities = querySystem.queryTwoComponents(PositionComponent, VelocityComponent);
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x;
|
||||
position.y += velocity.y;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 使用查询构建器
|
||||
|
||||
```typescript
|
||||
// 创建复杂查询
|
||||
const query = querySystem.createQuery()
|
||||
.withAll(PositionComponent, RenderComponent)
|
||||
.without(HiddenComponent)
|
||||
.withTag(1) // 特定标签
|
||||
.orderByName()
|
||||
.limit(10);
|
||||
|
||||
const result = query.execute();
|
||||
|
||||
// 链式操作
|
||||
const visibleEnemies = querySystem.createQuery()
|
||||
.withAll(EnemyComponent, PositionComponent)
|
||||
.without(DeadComponent, HiddenComponent)
|
||||
.filter(entity => entity.name.startsWith('Boss'))
|
||||
.orderBy((a, b) => a.id - b.id);
|
||||
|
||||
// 迭代结果
|
||||
visibleEnemies.forEach((entity, index) => {
|
||||
console.log(`敌人 ${index}: ${entity.name}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 高级查询功能
|
||||
|
||||
```typescript
|
||||
// 复合查询
|
||||
const complexResult = querySystem.queryComplex(
|
||||
{
|
||||
type: QueryConditionType.ALL,
|
||||
componentTypes: [PositionComponent, VelocityComponent],
|
||||
mask: /* 位掩码 */
|
||||
},
|
||||
{
|
||||
type: QueryConditionType.NONE,
|
||||
componentTypes: [DeadComponent],
|
||||
mask: /* 位掩码 */
|
||||
}
|
||||
);
|
||||
|
||||
// 批量查询
|
||||
const batchResults = querySystem.batchQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [HealthComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [ManaComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
|
||||
// 并行查询
|
||||
const parallelResults = await querySystem.parallelQuery([
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
]);
|
||||
```
|
||||
|
||||
## 场景级别的实体查询
|
||||
|
||||
除了使用QuerySystem,您还可以直接使用Scene提供的便捷查询方法:
|
||||
|
||||
### 基本场景查询
|
||||
|
||||
```typescript
|
||||
// 按名称查找实体
|
||||
const player = scene.findEntity("Player");
|
||||
const playerAlt = scene.getEntityByName("Player"); // 别名方法
|
||||
|
||||
// 按ID查找实体
|
||||
const entity = scene.findEntityById(123);
|
||||
|
||||
// 按标签查找实体
|
||||
const enemies = scene.findEntitiesByTag(2);
|
||||
const enemiesAlt = scene.getEntitiesByTag(2); // 别名方法
|
||||
|
||||
// 获取所有实体
|
||||
const allEntities = scene.entities.buffer;
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 缓存管理
|
||||
|
||||
```typescript
|
||||
// 设置缓存配置
|
||||
querySystem.setCacheConfig(200, 2000); // 最大200个缓存项,2秒超时
|
||||
|
||||
// 清空缓存
|
||||
querySystem.clearCache();
|
||||
|
||||
// 预热常用查询
|
||||
const commonQueries = [
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PositionComponent], mask: /* 位掩码 */ },
|
||||
{ type: QueryConditionType.ALL, componentTypes: [VelocityComponent], mask: /* 位掩码 */ }
|
||||
];
|
||||
querySystem.warmUpCache(commonQueries);
|
||||
```
|
||||
|
||||
### 2. 索引优化
|
||||
|
||||
```typescript
|
||||
// 自动优化索引配置
|
||||
querySystem.optimizeIndexes();
|
||||
|
||||
// 获取性能统计
|
||||
const stats = querySystem.getStats();
|
||||
console.log(`缓存命中率: ${(stats.hitRate * 100).toFixed(1)}%`);
|
||||
console.log(`实体数量: ${stats.entityCount}`);
|
||||
|
||||
// 获取详细性能报告
|
||||
const report = querySystem.getPerformanceReport();
|
||||
console.log(report);
|
||||
```
|
||||
|
||||
### 3. 查询监听和快照
|
||||
|
||||
```typescript
|
||||
// 监听查询结果变更
|
||||
const unwatch = querySystem.watchQuery(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [EnemyComponent], mask: /* 位掩码 */ },
|
||||
(entities, changeType) => {
|
||||
console.log(`敌人实体${changeType}: ${entities.length}个`);
|
||||
}
|
||||
);
|
||||
|
||||
// 取消监听
|
||||
unwatch();
|
||||
|
||||
// 创建查询快照
|
||||
const snapshot1 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 稍后创建另一个快照
|
||||
const snapshot2 = querySystem.createSnapshot(
|
||||
{ type: QueryConditionType.ALL, componentTypes: [PlayerComponent], mask: /* 位掩码 */ }
|
||||
);
|
||||
|
||||
// 比较快照
|
||||
const diff = querySystem.compareSnapshots(snapshot1, snapshot2);
|
||||
console.log(`新增: ${diff.added.length}, 移除: ${diff.removed.length}`);
|
||||
```
|
||||
|
||||
## 实际使用示例
|
||||
|
||||
### 移动系统示例
|
||||
|
||||
```typescript
|
||||
import { EntitySystem } from '@esengine/ecs-framework';
|
||||
|
||||
class MovementSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 查询所有可移动的实体
|
||||
const movableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
VelocityComponent
|
||||
);
|
||||
|
||||
movableEntities.forEach(({ entity, component1: position, component2: velocity }) => {
|
||||
// 更新位置
|
||||
position.x += velocity.x * Time.deltaTime;
|
||||
position.y += velocity.y * Time.deltaTime;
|
||||
|
||||
// 边界检查
|
||||
if (position.x < 0 || position.x > 800) {
|
||||
velocity.x = -velocity.x;
|
||||
}
|
||||
if (position.y < 0 || position.y > 600) {
|
||||
velocity.y = -velocity.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 碰撞检测示例
|
||||
|
||||
```typescript
|
||||
class CollisionSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 获取所有具有碰撞器的实体
|
||||
const collidableEntities = this.scene.querySystem.queryTwoComponents(
|
||||
PositionComponent,
|
||||
ColliderComponent
|
||||
);
|
||||
|
||||
// 检测碰撞
|
||||
for (let i = 0; i < collidableEntities.length; i++) {
|
||||
for (let j = i + 1; j < collidableEntities.length; j++) {
|
||||
const entityA = collidableEntities[i];
|
||||
const entityB = collidableEntities[j];
|
||||
|
||||
if (this.checkCollision(entityA, entityB)) {
|
||||
this.handleCollision(entityA.entity, entityB.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: any, entityB: any): boolean {
|
||||
// 简单的距离检测
|
||||
const posA = entityA.component1;
|
||||
const posB = entityB.component1;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(posA.x - posB.x, 2) + Math.pow(posA.y - posB.y, 2)
|
||||
);
|
||||
return distance < 50;
|
||||
}
|
||||
|
||||
private handleCollision(entityA: Entity, entityB: Entity): void {
|
||||
console.log(`碰撞检测: ${entityA.name} 与 ${entityB.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生命值管理示例
|
||||
|
||||
```typescript
|
||||
class HealthSystem extends EntitySystem {
|
||||
public update(): void {
|
||||
// 查询所有具有生命值的实体
|
||||
const healthEntities = this.scene.querySystem.queryComponentTyped(HealthComponent);
|
||||
const deadEntities: Entity[] = [];
|
||||
|
||||
healthEntities.forEach(({ entity, component: health }) => {
|
||||
// 检查死亡
|
||||
if (health.currentHealth <= 0) {
|
||||
deadEntities.push(entity);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除死亡实体
|
||||
deadEntities.forEach(entity => {
|
||||
console.log(`实体 ${entity.name} 已死亡`);
|
||||
entity.destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 查询优化
|
||||
|
||||
- 尽量使用类型安全的查询方法
|
||||
- 避免在每帧都创建新的查询
|
||||
- 合理使用缓存机制
|
||||
|
||||
### 2. 性能监控
|
||||
|
||||
- 定期检查查询性能报告
|
||||
- 监控缓存命中率
|
||||
- 优化频繁使用的查询
|
||||
|
||||
### 3. 内存管理
|
||||
|
||||
- 及时清理不需要的查询监听器
|
||||
- 合理设置缓存大小
|
||||
- 避免创建过多的查询快照
|
||||
|
||||
### 4. 代码组织
|
||||
|
||||
- 将复杂查询封装到专门的方法中
|
||||
- 使用查询构建器创建可读性更好的查询
|
||||
- 在系统中合理组织查询逻辑
|
||||
@@ -1,71 +0,0 @@
|
||||
# scene_component
|
||||
这是一个场景组件的基类,如果您需要一个不在实体上的组件则继承它 `es.SceneComponent`。场景组件默认包含`update`/`onEnabled`/`onDisabled`/`onRemovedFromScene`,你可以对他们进行重载。
|
||||
|
||||
```typescript
|
||||
export class ASceneComponent extends es.SceneComponent {
|
||||
/**
|
||||
* 在启用此SceneComponent时调用
|
||||
*/
|
||||
onEnabled() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当禁用此SceneComponent时调用
|
||||
*/
|
||||
onDisabled() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当该SceneComponent从场景中移除时调用
|
||||
*/
|
||||
onRemovedFromScene() {
|
||||
|
||||
}
|
||||
|
||||
update() {
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 场景组件需要添加至场景上, 通过场景中的 `addSceneComponent` 方法加入。
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
const aSceneCom = this.addSceneComponent(new ASceneComponent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 如果想要获取该场景组件则通过`getSceneComponent`方法获取
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
const aSceneCom = this.getSceneComponent(ASceneComponent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 如果获取时发现没有可以自动创建则通过 `getOrCreateSceneComponent` 方法
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
const aSceneCom = this.getOrCreateSceneComponent(ASceneComponent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 删除场景组件
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
this.removeSceneComponent(aSceneCom);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,67 +0,0 @@
|
||||
# system
|
||||
系统是ecs的核心。你的游戏逻辑应该在这里进行处理,所有的实体及组件也会在这里进行集中处理。用于处理实体的系统叫做 `es.EntityProcessingSystem`。 你需要继承他并实现`processEntity(entity: Entity)`方法。
|
||||
|
||||
```typescript
|
||||
export class ASystem extends es.EntityProcessingSystem {
|
||||
processEntity(entity: Entity){
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
系统也依赖于场景,如果想要系统被激活则需要使用场景中`addEntityProcessor`方法。系统被实例化需要传入一个`es.Matcher` 参数。
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
this.addEntityProcessor(new ASystem(es.Matcher.empty().all(AComponent)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Matcher
|
||||
matcher是系统的匹配器,用于匹配满足条件的实体传入系统进行处理。如果想要一个空的匹配器则直接 `es.Matcher.empty()`
|
||||
|
||||
- all
|
||||
同时拥有多个组件
|
||||
|
||||
```typescript
|
||||
es.Matcher.empty().all(AComponent, BComponent);
|
||||
```
|
||||
|
||||
- one
|
||||
拥有任意一个组件
|
||||
|
||||
```typescript
|
||||
es.Matcher.empty().one(AComponent, BComponent);
|
||||
```
|
||||
|
||||
- exclude
|
||||
拥有某些组件,并且不包含某些组件
|
||||
```typescript
|
||||
// 不包含CComponent或者DComponent
|
||||
es.Matcher.empty().all(AComponent, BComponent).exclude(CComponent, DComponent);
|
||||
|
||||
// 不同时包含CComponent和DComponent
|
||||
es.Matcher.empty().all(AComponent, BComponent).exclude(CComponent).exclude(DComponent);
|
||||
```
|
||||
|
||||
## 获取系统
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
const aSystem = this.getEntityProcessor(ASystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 移除系统
|
||||
|
||||
```typescript
|
||||
export class MainScene extends es.Scene {
|
||||
onStart() {
|
||||
this.removeEntityProcessor(aSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/time.md
23
docs/time.md
@@ -1,23 +0,0 @@
|
||||
# Time
|
||||
游戏中会经常使用到关于时间类。框架内提供了关于时间的多个属性
|
||||
|
||||
## 游戏运行的总时间
|
||||
`es.Time.totalTime`
|
||||
|
||||
## deltaTime的未缩放版本。不受时间尺度的影响
|
||||
`es.Time.unscaledDeltaTime`
|
||||
|
||||
## 前一帧到当前帧的时间增量(按时间刻度进行缩放)
|
||||
`es.Time.deltaTime`
|
||||
|
||||
## 时间刻度缩放
|
||||
`es.Time.timeScale`
|
||||
|
||||
## DeltaTime可以为的最大值
|
||||
`es.Time.maxDeltaTime` 默认为Number.MAX_VALUE
|
||||
|
||||
## 已传递的帧总数
|
||||
`es.Time.frameCount`
|
||||
|
||||
## 自场景加载以来的总时间
|
||||
`es.Time.timeSinceSceneLoad`
|
||||
Submodule extensions/behaviourTree-ai deleted from 192de123d3
@@ -1,319 +0,0 @@
|
||||
declare module es {
|
||||
abstract class AbstractTweenable implements ITweenable {
|
||||
protected _isPaused: boolean;
|
||||
protected _isCurrentlyManagedByTweenManager: boolean;
|
||||
abstract tick(): boolean;
|
||||
recycleSelf(): void;
|
||||
isRunning(): boolean;
|
||||
start(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
stop(bringToCompletion?: boolean): void;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
class PropertyTweens {
|
||||
static NumberPropertyTo(self: any, memberName: string, to: number, duration: number): ITween<number>;
|
||||
static Vector2PropertyTo(self: any, memeberName: string, to: Vector2, duration: number): ITween<Vector2>;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
class TransformSpringTween extends AbstractTweenable {
|
||||
readonly targetType: TransformTargetType;
|
||||
private _transform;
|
||||
private _targetType;
|
||||
private _targetValue;
|
||||
private _velocity;
|
||||
dampingRatio: number;
|
||||
angularFrequency: number;
|
||||
constructor(transform: Transform, targetType: TransformTargetType, targetValue: Vector2);
|
||||
setTargetValue(targetValue: Vector2): void;
|
||||
updateDampingRatioWithHalfLife(lambda: number): void;
|
||||
tick(): boolean;
|
||||
private setTweenedValue;
|
||||
private getCurrentValueOfTweenedTargetType;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
enum LoopType {
|
||||
none = 0,
|
||||
restartFromBeginning = 1,
|
||||
pingpong = 2
|
||||
}
|
||||
enum TweenState {
|
||||
running = 0,
|
||||
paused = 1,
|
||||
complete = 2
|
||||
}
|
||||
abstract class Tween<T> implements ITweenable, ITween<T> {
|
||||
protected _target: ITweenTarget<T>;
|
||||
protected _isFromValueOverridden: boolean;
|
||||
protected _fromValue: T;
|
||||
protected _toValue: T;
|
||||
protected _easeType: EaseType;
|
||||
protected _shouldRecycleTween: boolean;
|
||||
protected _isRelative: boolean;
|
||||
protected _completionHandler: (tween: ITween<T>) => void;
|
||||
protected _loopCompleteHandler: (tween: ITween<T>) => void;
|
||||
protected _nextTween: ITweenable;
|
||||
protected _tweenState: TweenState;
|
||||
private _isTimeScaleIndependent;
|
||||
protected _delay: number;
|
||||
protected _duration: number;
|
||||
protected _timeScale: number;
|
||||
protected _elapsedTime: number;
|
||||
protected _loopType: LoopType;
|
||||
protected _loops: number;
|
||||
protected _delayBetweenLoops: number;
|
||||
private _isRunningInReverse;
|
||||
context: any;
|
||||
setEaseType(easeType: EaseType): ITween<T>;
|
||||
setDelay(delay: number): ITween<T>;
|
||||
setDuration(duration: number): ITween<T>;
|
||||
setTimeScale(timeSclae: number): ITween<T>;
|
||||
setIsTimeScaleIndependent(): ITween<T>;
|
||||
setCompletionHandler(completeHandler: (tween: ITween<T>) => void): ITween<T>;
|
||||
setLoops(loopType: LoopType, loops?: number, delayBetweenLoops?: number): ITween<T>;
|
||||
setLoopCompletionHanlder(loopCompleteHandler: (tween: ITween<T>) => void): ITween<T>;
|
||||
setFrom(from: T): ITween<T>;
|
||||
prepareForReuse(from: T, to: T, duration: number): ITween<T>;
|
||||
setRecycleTween(shouldRecycleTween: boolean): ITween<T>;
|
||||
abstract setIsRelative(): ITween<T>;
|
||||
setContext(context: any): ITween<T>;
|
||||
setNextTween(nextTween: ITweenable): ITween<T>;
|
||||
tick(): boolean;
|
||||
recycleSelf(): void;
|
||||
isRunning(): boolean;
|
||||
start(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
stop(bringToCompletion?: boolean): void;
|
||||
jumpToElapsedTime(elapsedTime: any): void;
|
||||
reverseTween(): void;
|
||||
waitForCompletion(): IterableIterator<any>;
|
||||
getTargetObject(): any;
|
||||
private resetState;
|
||||
initialize(target: ITweenTarget<T>, to: T, duration: number): void;
|
||||
private handleLooping;
|
||||
protected abstract updateValue(): any;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
class NumberTween extends Tween<number> {
|
||||
static create(): NumberTween;
|
||||
constructor(target?: ITweenTarget<number>, to?: number, duration?: number);
|
||||
setIsRelative(): ITween<number>;
|
||||
protected updateValue(): void;
|
||||
recycleSelf(): void;
|
||||
}
|
||||
class Vector2Tween extends Tween<Vector2> {
|
||||
static create(): Vector2Tween;
|
||||
constructor(target?: ITweenTarget<Vector2>, to?: Vector2, duration?: number);
|
||||
setIsRelative(): ITween<Vector2>;
|
||||
protected updateValue(): void;
|
||||
recycleSelf(): void;
|
||||
}
|
||||
class RectangleTween extends Tween<Rectangle> {
|
||||
static create(): RectangleTween;
|
||||
constructor(target?: ITweenTarget<Rectangle>, to?: Rectangle, duration?: number);
|
||||
setIsRelative(): ITween<Rectangle>;
|
||||
protected updateValue(): void;
|
||||
recycleSelf(): void;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
enum TransformTargetType {
|
||||
position = 0,
|
||||
localPosition = 1,
|
||||
scale = 2,
|
||||
localScale = 3,
|
||||
rotationDegrees = 4,
|
||||
localRotationDegrees = 5
|
||||
}
|
||||
class TransformVector2Tween extends Vector2Tween implements ITweenTarget<Vector2> {
|
||||
private _transform;
|
||||
private _targetType;
|
||||
setTweenedValue(value: Vector2): void;
|
||||
getTweenedValue(): Vector2;
|
||||
getTargetObject(): Transform;
|
||||
setTargetAndType(transform: Transform, targetType: TransformTargetType): void;
|
||||
protected updateValue(): void;
|
||||
recycleSelf(): void;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
enum EaseType {
|
||||
linear = 0,
|
||||
sineIn = 1,
|
||||
sineOut = 2,
|
||||
sineInOut = 3,
|
||||
quadIn = 4,
|
||||
quadOut = 5,
|
||||
quadInOut = 6,
|
||||
quintIn = 7,
|
||||
quintOut = 8,
|
||||
quintInOut = 9,
|
||||
cubicIn = 10,
|
||||
cubicOut = 11,
|
||||
cubicInOut = 12,
|
||||
quartIn = 13,
|
||||
quartOut = 14,
|
||||
quartInOut = 15,
|
||||
expoIn = 16,
|
||||
expoOut = 17,
|
||||
expoInOut = 18,
|
||||
circleIn = 19,
|
||||
circleOut = 20,
|
||||
circleInOut = 21,
|
||||
elasticIn = 22,
|
||||
elasticOut = 23,
|
||||
elasticInOut = 24,
|
||||
punch = 25,
|
||||
backIn = 26,
|
||||
backOut = 27,
|
||||
backInOut = 28,
|
||||
bounceIn = 29,
|
||||
bounceOut = 30,
|
||||
bounceInOut = 31
|
||||
}
|
||||
class EaseHelper {
|
||||
static oppositeEaseType(easeType: EaseType): EaseType.linear | EaseType.sineIn | EaseType.sineOut | EaseType.sineInOut | EaseType.quadIn | EaseType.quadOut | EaseType.quadInOut | EaseType.quintIn | EaseType.quintOut | EaseType.quintInOut | EaseType.cubicIn | EaseType.cubicOut | EaseType.cubicInOut | EaseType.quartIn | EaseType.quartInOut | EaseType.expoIn | EaseType.expoOut | EaseType.expoInOut | EaseType.circleIn | EaseType.circleOut | EaseType.circleInOut | EaseType.elasticIn | EaseType.elasticOut | EaseType.elasticInOut | EaseType.punch | EaseType.backIn | EaseType.backOut | EaseType.backInOut | EaseType.bounceIn | EaseType.bounceOut | EaseType.bounceInOut;
|
||||
static ease(easeType: EaseType, t: number, duration: number): number;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
class TweenManager extends GlobalManager {
|
||||
static defaultEaseType: EaseType;
|
||||
static removeAllTweensOnLevelLoad: boolean;
|
||||
static cacheNumberTweens: boolean;
|
||||
static cacheVector2Tweens: boolean;
|
||||
static cacheRectTweens: boolean;
|
||||
private _activeTweens;
|
||||
private _tempTweens;
|
||||
private _isUpdating;
|
||||
private static _instance;
|
||||
constructor();
|
||||
update(): void;
|
||||
static addTween(tween: ITweenable): void;
|
||||
static removeTween(tween: ITweenable): void;
|
||||
static stopAllTweens(bringToCompletion?: boolean): void;
|
||||
static allTweensWithContext(context: any): ITweenable[];
|
||||
static stopAllTweensWithContext(context: any, bringToCompletion?: boolean): void;
|
||||
static allTweenWithTarget(target: any): ITweenable[];
|
||||
static stopAllTweensWithTarget(target: any, bringToCompletion?: boolean): void;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
module Easing {
|
||||
class Linear {
|
||||
static easeNone(t: number, d: number): number;
|
||||
}
|
||||
class Quadratic {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Back {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Bounce {
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Circular {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Cubic {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Elastic {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
static punch(t: number, d: number): number;
|
||||
}
|
||||
class Exponential {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Quartic {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Quintic {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
class Sinusoidal {
|
||||
static easeIn(t: number, d: number): number;
|
||||
static easeOut(t: number, d: number): number;
|
||||
static easeInOut(t: number, d: number): number;
|
||||
}
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
class Lerps {
|
||||
static lerp(from: number, to: number, t: number): number;
|
||||
static lerpVector2(from: Vector2, to: Vector2, t: number): Vector2;
|
||||
static lerpRectangle(from: Rectangle, to: Rectangle, t: number): Rectangle;
|
||||
static angleLerp(from: Vector2, to: Vector2, t: number): Vector2;
|
||||
static ease(easeType: EaseType, from: number, to: number, t: number, duration: number): number;
|
||||
static easeVector2(easeType: EaseType, from: Vector2, to: Vector2, t: number, duration: number): Vector2;
|
||||
static easeRectangle(easeType: EaseType, from: Rectangle, to: Rectangle, t: number, duration: number): Rectangle;
|
||||
static easeAngle(easeType: EaseType, from: Vector2, to: Vector2, t: number, duration: number): Vector2;
|
||||
static fastSpring(currentValue: Vector2, targetValue: Vector2, velocity: Vector2, dampingRatio: number, angularFrequency: number): Vector2;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
interface ITween<T> extends ITweenControl {
|
||||
setEaseType(easeType: EaseType): ITween<T>;
|
||||
setDelay(delay: number): ITween<T>;
|
||||
setDuration(duration: number): ITween<T>;
|
||||
setTimeScale(timeScale: number): ITween<T>;
|
||||
setIsTimeScaleIndependent(): ITween<T>;
|
||||
setCompletionHandler(completionHandler: (tween: ITween<T>) => void): ITween<T>;
|
||||
setLoops(loopType: LoopType, loops: number, delayBetweenLoops: number): ITween<T>;
|
||||
setFrom(from: T): ITween<T>;
|
||||
prepareForReuse(from: T, to: T, duration: number): ITween<T>;
|
||||
setRecycleTween(shouldRecycleTween: boolean): ITween<T>;
|
||||
setIsRelative(): ITween<T>;
|
||||
setContext(context: any): ITween<T>;
|
||||
setNextTween(nextTween: ITweenable): ITween<T>;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
interface ITweenControl extends ITweenable {
|
||||
context: any;
|
||||
jumpToElapsedTime(elapsedTime: number): any;
|
||||
waitForCompletion(): any;
|
||||
getTargetObject(): any;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
interface ITweenTarget<T> {
|
||||
setTweenedValue(value: T): any;
|
||||
getTweenedValue(): T;
|
||||
getTargetObject(): any;
|
||||
}
|
||||
}
|
||||
declare module es {
|
||||
interface ITweenable {
|
||||
tick(): boolean;
|
||||
recycleSelf(): any;
|
||||
isRunning(): boolean;
|
||||
start(): any;
|
||||
pause(): any;
|
||||
resume(): any;
|
||||
stop(bringToCompletion: boolean): any;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Submodule extensions/ecs-star deleted from 0380608377
6514
extensions/ecs-tween/lib/framework.d.ts
vendored
6514
extensions/ecs-tween/lib/framework.d.ts
vendored
File diff suppressed because it is too large
Load Diff
1333
package-lock.json
generated
Normal file
1333
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
59
package.json
Normal file
59
package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.1.11",
|
||||
"description": "用于Laya、Cocos等游戏引擎的高性能ECS框架",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"files": [
|
||||
"bin/**/*",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"entity-component-system",
|
||||
"game-engine",
|
||||
"typescript",
|
||||
"laya",
|
||||
"cocos",
|
||||
"egret"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin wasm dist",
|
||||
"clean:wasm": "rimraf src/wasm/rust-ecs-core/pkg src/wasm/rust-ecs-core/target",
|
||||
"build:wasm": "cd src/wasm/rust-ecs-core && wasm-pack build --target web --out-dir ../../../bin/wasm --release",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:wasm && npm run build:ts",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run clean:wasm && npm run build",
|
||||
"build:npm": "npm run build && node scripts/build-rollup.js",
|
||||
"test:benchmark": "npm run build && node bin/Testing/Performance/benchmark.js",
|
||||
"test:unit": "npm run build && node bin/Testing/test-runner.js",
|
||||
"benchmark": "node scripts/benchmark.js",
|
||||
"preversion": "npm run rebuild",
|
||||
"publish:patch": "npm version patch && npm run build:npm && cd dist && npm publish",
|
||||
"publish:minor": "npm version minor && npm run build:npm && cd dist && npm publish",
|
||||
"publish:major": "npm version major && npm run build:npm && cd dist && npm publish",
|
||||
"publish:npm": "npm run build:npm && cd dist && npm publish"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/node": "^20.19.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
"rollup-plugin-dts": "^6.2.1",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git"
|
||||
}
|
||||
}
|
||||
99
rollup.config.js
Normal file
99
rollup.config.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const resolve = require('@rollup/plugin-node-resolve');
|
||||
const commonjs = require('@rollup/plugin-commonjs');
|
||||
const terser = require('@rollup/plugin-terser');
|
||||
const dts = require('rollup-plugin-dts').default;
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const banner = `/**
|
||||
* @esengine/ecs-framework v${pkg.version}
|
||||
* 高性能ECS框架 - 适用于Cocos Creator和Laya引擎
|
||||
*
|
||||
* @author ${pkg.author}
|
||||
* @license ${pkg.license}
|
||||
*/`;
|
||||
|
||||
const external = [];
|
||||
|
||||
const commonPlugins = [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs({
|
||||
include: /node_modules/
|
||||
})
|
||||
];
|
||||
|
||||
module.exports = [
|
||||
// ES模块构建
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'es',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
unknownGlobalSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// UMD构建(可选)
|
||||
{
|
||||
input: 'bin/index.js',
|
||||
output: {
|
||||
file: 'dist/index.umd.js',
|
||||
format: 'umd',
|
||||
name: 'ECSFramework',
|
||||
banner,
|
||||
sourcemap: true,
|
||||
exports: 'named'
|
||||
},
|
||||
plugins: [
|
||||
...commonPlugins,
|
||||
terser({
|
||||
format: {
|
||||
comments: /^!/
|
||||
}
|
||||
})
|
||||
],
|
||||
external,
|
||||
treeshake: {
|
||||
moduleSideEffects: false
|
||||
}
|
||||
},
|
||||
|
||||
// 类型定义构建
|
||||
{
|
||||
input: 'bin/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
banner: `/**
|
||||
* @esengine/ecs-framework v${pkg.version}
|
||||
* TypeScript definitions
|
||||
*/`
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
respectExternal: true
|
||||
})
|
||||
],
|
||||
external: []
|
||||
}
|
||||
];
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB |
39
scripts/benchmark.js
Normal file
39
scripts/benchmark.js
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* ECS框架性能基准测试入口
|
||||
*
|
||||
* 使用方法:
|
||||
* node benchmark.js
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
console.log('🚀 启动ECS框架性能基准测试...\n');
|
||||
|
||||
const sourceDir = path.join(__dirname, '..');
|
||||
|
||||
try {
|
||||
console.log('📦 准备构建项目...');
|
||||
|
||||
// 构建TypeScript代码
|
||||
console.log('🔨 构建TypeScript代码...');
|
||||
execSync('npm run build', {
|
||||
stdio: 'inherit',
|
||||
cwd: sourceDir
|
||||
});
|
||||
console.log('✅ TypeScript构建完成\n');
|
||||
|
||||
// 运行性能测试
|
||||
console.log('🏃 运行性能基准测试...');
|
||||
execSync('node bin/Testing/Performance/benchmark.js', {
|
||||
stdio: 'inherit',
|
||||
cwd: sourceDir
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 性能测试失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
148
scripts/build-rollup.js
Normal file
148
scripts/build-rollup.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🚀 使用 Rollup 构建npm包...');
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 清理旧的dist目录
|
||||
if (fs.existsSync('./dist')) {
|
||||
console.log('🧹 清理旧的构建文件...');
|
||||
execSync('rimraf ./dist', { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
// 执行Rollup构建
|
||||
console.log('📦 执行 Rollup 构建...');
|
||||
execSync('rollup -c', { stdio: 'inherit' });
|
||||
|
||||
// 生成package.json
|
||||
console.log('📋 生成 package.json...');
|
||||
generatePackageJson();
|
||||
|
||||
// 复制其他文件
|
||||
console.log('📁 复制必要文件...');
|
||||
copyFiles();
|
||||
|
||||
// 输出构建结果
|
||||
showBuildResults();
|
||||
|
||||
console.log('✅ 构建完成!');
|
||||
console.log('\n🚀 发布命令:');
|
||||
console.log('cd dist && npm publish');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 构建失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const sourcePackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
|
||||
const distPackage = {
|
||||
name: sourcePackage.name,
|
||||
version: sourcePackage.version,
|
||||
description: sourcePackage.description,
|
||||
main: 'index.js',
|
||||
module: 'index.js',
|
||||
types: 'index.d.ts',
|
||||
type: 'module',
|
||||
exports: {
|
||||
'.': {
|
||||
import: './index.js',
|
||||
types: './index.d.ts'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'index.js',
|
||||
'index.js.map',
|
||||
'index.umd.js',
|
||||
'index.umd.js.map',
|
||||
'index.d.ts',
|
||||
'wasm',
|
||||
'README.md',
|
||||
'LICENSE',
|
||||
'SECURITY.md',
|
||||
'.npmignore'
|
||||
],
|
||||
keywords: [
|
||||
'ecs',
|
||||
'entity-component-system',
|
||||
'game-engine',
|
||||
'typescript',
|
||||
'cocos-creator',
|
||||
'laya',
|
||||
'rollup'
|
||||
],
|
||||
author: sourcePackage.author,
|
||||
license: sourcePackage.license,
|
||||
repository: sourcePackage.repository,
|
||||
bugs: sourcePackage.bugs,
|
||||
homepage: sourcePackage.homepage,
|
||||
engines: {
|
||||
node: '>=16.0.0'
|
||||
},
|
||||
sideEffects: false
|
||||
};
|
||||
|
||||
fs.writeFileSync('./dist/package.json', JSON.stringify(distPackage, null, 2));
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
const filesToCopy = [
|
||||
{ src: './README.md', dest: './dist/README.md' },
|
||||
{ src: './LICENSE', dest: './dist/LICENSE' },
|
||||
{ src: './SECURITY.md', dest: './dist/SECURITY.md' },
|
||||
{ src: './.npmignore', dest: './dist/.npmignore' }
|
||||
];
|
||||
|
||||
filesToCopy.forEach(({ src, dest }) => {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` ✓ 复制: ${path.basename(dest)}`);
|
||||
} else {
|
||||
console.log(` ⚠️ 文件不存在: ${src}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 复制WASM文件(过滤.gitignore)
|
||||
const wasmDir = './bin/wasm';
|
||||
if (fs.existsSync(wasmDir)) {
|
||||
const distWasmDir = './dist/wasm';
|
||||
if (!fs.existsSync(distWasmDir)) {
|
||||
fs.mkdirSync(distWasmDir);
|
||||
}
|
||||
|
||||
let copiedCount = 0;
|
||||
fs.readdirSync(wasmDir).forEach(file => {
|
||||
// 过滤掉.gitignore文件
|
||||
if (file !== '.gitignore') {
|
||||
fs.copyFileSync(
|
||||
path.join(wasmDir, file),
|
||||
path.join(distWasmDir, file)
|
||||
);
|
||||
copiedCount++;
|
||||
}
|
||||
});
|
||||
if (copiedCount > 0) {
|
||||
console.log(` ✓ 复制: ${copiedCount}个WASM文件`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showBuildResults() {
|
||||
const distDir = './dist';
|
||||
const files = ['index.js', 'index.umd.js', 'index.d.ts'];
|
||||
|
||||
console.log('\n📊 构建结果:');
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(distDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const size = fs.statSync(filePath).size;
|
||||
console.log(` ${file}: ${(size / 1024).toFixed(1)}KB`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
2
source/.idea/.gitignore
generated
vendored
2
source/.idea/.gitignore
generated
vendored
@@ -1,2 +0,0 @@
|
||||
# Default ignored files
|
||||
/workspace.xml
|
||||
6
source/.idea/misc.xml
generated
6
source/.idea/misc.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
||||
8
source/.idea/modules.xml
generated
8
source/.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/source.iml" filepath="$PROJECT_DIR$/.idea/source.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
source/.idea/source.iml
generated
12
source/.idea/source.iml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
source/.idea/vcs.xml
generated
6
source/.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
13
source/.vscode/tasks.json
vendored
13
source/.vscode/tasks.json
vendored
@@ -1,13 +0,0 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "gulp",
|
||||
"task": "build",
|
||||
"group": "build",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"typescript.tsdk": "./node_modules/typescript/lib"
|
||||
}
|
||||
6514
source/bin/framework.d.ts
vendored
6514
source/bin/framework.d.ts
vendored
File diff suppressed because it is too large
Load Diff
16757
source/bin/framework.js
16757
source/bin/framework.js
File diff suppressed because it is too large
Load Diff
1
source/bin/framework.min.js
vendored
1
source/bin/framework.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,43 +0,0 @@
|
||||
'use strict';
|
||||
const gulp = require("gulp");
|
||||
const minify = require('gulp-minify');
|
||||
const inject = require("gulp-inject-string");
|
||||
const ts = require('gulp-typescript');
|
||||
const merge = require('merge2');
|
||||
const tsProject = ts.createProject('tsconfig.json');
|
||||
|
||||
gulp.task('buildJs', () => {
|
||||
return tsProject.src()
|
||||
.pipe(tsProject())
|
||||
.js.pipe(inject.replace('var es;', ''))
|
||||
.pipe(inject.prepend('window.es = {};\n'))
|
||||
.pipe(inject.replace('var __extends =', 'window.__extends ='))
|
||||
.pipe(minify({ ext: { min: ".min.js" } }))
|
||||
.pipe(gulp.dest('./bin'));
|
||||
});
|
||||
|
||||
gulp.task("buildDts", ["buildJs"], () => {
|
||||
return tsProject.src()
|
||||
.pipe(tsProject())
|
||||
// .dts.pipe(inject.append('import e = framework;'))
|
||||
.pipe(gulp.dest('./bin'));
|
||||
});
|
||||
|
||||
gulp.task("copy", ["buildDts"], () => {
|
||||
return gulp.src('bin/**/*')
|
||||
.pipe(gulp.dest('../demo/egret_demo/libs/framework/'))
|
||||
.pipe(gulp.dest('../extensions/behaviourTree-ai/egret-demo/libs/framework/'))
|
||||
});
|
||||
|
||||
gulp.task('build', ['copy'], () => {
|
||||
return merge([
|
||||
gulp.src('bin/*.js')
|
||||
.pipe(gulp.dest('../demo/laya_demo/bin/libs/')),
|
||||
gulp.src('bin/*.ts')
|
||||
.pipe(gulp.dest('../demo/laya_demo/libs/')),
|
||||
gulp.src('bin/framework.d.ts')
|
||||
.pipe(gulp.dest('../extensions/behaviourTree-ai/source/lib/'))
|
||||
.pipe(gulp.dest('../extensions/ecs-star/lib/'))
|
||||
.pipe(gulp.dest('../extensions/ecs-tween/lib/'))
|
||||
])
|
||||
});
|
||||
6201
source/package-lock.json
generated
6201
source/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "@esengine/egret-framework",
|
||||
"version": "1.0.1",
|
||||
"description": "用于egret 包含众多高性能方法以供使用",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --recursive --reporter tap --growl",
|
||||
"eslint": "eslint src --ext .ts"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"browserify": "^14.3.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-inject-string": "^1.1.2",
|
||||
"gulp-minify": "^3.1.0",
|
||||
"gulp-string-replace": "^1.1.2",
|
||||
"gulp-typescript": "^3.1.6",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"tsify": "^3.0.1",
|
||||
"typedoc": "^0.19.2",
|
||||
"typescript": "^2.2.2",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.9.0",
|
||||
"merge2": "^1.4.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com/359807859@qq.com"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 全局核心类
|
||||
*/
|
||||
export class Core {
|
||||
/**
|
||||
* 核心发射器。只发出核心级别的事件
|
||||
*/
|
||||
public static emitter: Emitter<CoreEvents>;
|
||||
public static paused = false;
|
||||
/**
|
||||
* 是否启用调试渲染
|
||||
*/
|
||||
public static debugRenderEndabled = false;
|
||||
/**
|
||||
* 简化对内部类的全局内容实例的访问
|
||||
*/
|
||||
private static _instance: Core;
|
||||
/**
|
||||
* 用于确定是否应该使用EntitySystems
|
||||
*/
|
||||
public static entitySystemsEnabled: boolean;
|
||||
/**
|
||||
* 是否正在debug模式
|
||||
* 仅允许在create时进行更改
|
||||
*/
|
||||
public readonly debug: boolean;
|
||||
public _nextScene: Scene;
|
||||
/**
|
||||
* 用于凝聚GraphicsDeviceReset事件
|
||||
*/
|
||||
public _graphicsDeviceChangeTimer: ITimer;
|
||||
/**
|
||||
* 全局访问系统
|
||||
*/
|
||||
public _globalManagers: GlobalManager[] = [];
|
||||
public _coroutineManager: CoroutineManager = new CoroutineManager();
|
||||
public _timerManager: TimerManager = new TimerManager();
|
||||
|
||||
private constructor(debug: boolean = true, enableEntitySystems: boolean = true) {
|
||||
Core._instance = this;
|
||||
Core.emitter = new Emitter<CoreEvents>();
|
||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.update, this);
|
||||
|
||||
Core.registerGlobalManager(this._coroutineManager);
|
||||
Core.registerGlobalManager(new TweenManager());
|
||||
Core.registerGlobalManager(this._timerManager);
|
||||
Core.entitySystemsEnabled = enableEntitySystems;
|
||||
|
||||
this.debug = debug;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供对单例/游戏实例的访问
|
||||
* @constructor
|
||||
*/
|
||||
public static get Instance() {
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
public _frameCounterElapsedTime: number = 0;
|
||||
public _frameCounter: number = 0;
|
||||
public _totalMemory: number = 0;
|
||||
public _titleMemory: (totalMemory: number, frameCounter: number) => void;
|
||||
public _scene: Scene;
|
||||
|
||||
/**
|
||||
* 当前活动的场景。注意,如果设置了该设置,在更新结束之前场景实际上不会改变
|
||||
*/
|
||||
public static get scene() {
|
||||
if (!this._instance)
|
||||
return null;
|
||||
return this._instance._scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前活动的场景。注意,如果设置了该设置,在更新结束之前场景实际上不会改变
|
||||
* @param value
|
||||
*/
|
||||
public static set scene(value: Scene) {
|
||||
Insist.isNotNull(value, "场景不能为空");
|
||||
|
||||
if (this._instance._scene == null) {
|
||||
this._instance._scene = value;
|
||||
this._instance.onSceneChanged();
|
||||
this._instance._scene.begin();
|
||||
} else {
|
||||
this._instance._nextScene = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认实现创建核心
|
||||
*/
|
||||
public static create(debug: boolean = true): Core {
|
||||
if (this._instance == null) {
|
||||
this._instance = new es.Core(debug);
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个全局管理器对象,它的更新方法将调用场景前的每一帧。
|
||||
* @param manager
|
||||
*/
|
||||
public static registerGlobalManager(manager: es.GlobalManager) {
|
||||
this._instance._globalManagers.push(manager);
|
||||
manager.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全局管理器对象
|
||||
* @param manager
|
||||
*/
|
||||
public static unregisterGlobalManager(manager: es.GlobalManager) {
|
||||
new es.List(this._instance._globalManagers).remove(manager);
|
||||
manager.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型为T的全局管理器
|
||||
* @param type
|
||||
*/
|
||||
public static getGlobalManager<T extends es.GlobalManager>(type: new (...args) => T): T {
|
||||
for (let i = 0, s = Core._instance._globalManagers.length; i < s; ++ i) {
|
||||
let manager = Core._instance._globalManagers[i];
|
||||
if (manager instanceof type)
|
||||
return manager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个coroutine。Coroutine可以将number延时几秒或延时到其他startCoroutine.Yielding
|
||||
* null将使coroutine在下一帧被执行。
|
||||
* @param enumerator
|
||||
*/
|
||||
public static startCoroutine(enumerator): ICoroutine {
|
||||
return this._instance._coroutineManager.startCoroutine(enumerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度一个一次性或重复的计时器,该计时器将调用已传递的动作
|
||||
* @param timeInSeconds
|
||||
* @param repeats
|
||||
* @param context
|
||||
* @param onTime
|
||||
*/
|
||||
public static schedule(timeInSeconds: number, repeats: boolean = false, context: any = null, onTime: (timer: ITimer) => void) {
|
||||
return this._instance._timerManager.schedule(timeInSeconds, repeats, context, onTime);
|
||||
}
|
||||
|
||||
public startDebugDraw() {
|
||||
if (!this.debug) return;
|
||||
this._frameCounter++;
|
||||
this._frameCounterElapsedTime += Time.deltaTime;
|
||||
if (this._frameCounterElapsedTime >= 1) {
|
||||
let memoryInfo = window.performance["memory"];
|
||||
if (memoryInfo != null) {
|
||||
this._totalMemory = Number((memoryInfo.totalJSHeapSize / 1048576).toFixed(2));
|
||||
}
|
||||
if (this._titleMemory) this._titleMemory(this._totalMemory, this._frameCounter);
|
||||
this._frameCounter = 0;
|
||||
this._frameCounterElapsedTime -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在一个场景结束后,下一个场景开始之前调用
|
||||
*/
|
||||
public onSceneChanged() {
|
||||
Time.sceneChanged();
|
||||
}
|
||||
|
||||
protected initialize() {
|
||||
|
||||
}
|
||||
|
||||
protected async update(currentTime: number = -1) {
|
||||
if (Core.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
Time.update(currentTime);
|
||||
if (this._scene != null) {
|
||||
for (let i = this._globalManagers.length - 1; i >= 0; i--) {
|
||||
if (this._globalManagers[i].enabled)
|
||||
this._globalManagers[i].update();
|
||||
}
|
||||
|
||||
this._scene.update();
|
||||
|
||||
if (this._nextScene != null) {
|
||||
this._scene.end();
|
||||
|
||||
this._scene = this._nextScene;
|
||||
this._nextScene = null;
|
||||
this.onSceneChanged();
|
||||
|
||||
this._scene.begin();
|
||||
}
|
||||
}
|
||||
|
||||
this.startDebugDraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
module es {
|
||||
export enum LogType {
|
||||
error,
|
||||
warn,
|
||||
log,
|
||||
info,
|
||||
trace
|
||||
}
|
||||
|
||||
export class Debug {
|
||||
public static warnIf(condition: boolean, format: string, ...args: any[]) {
|
||||
if (condition)
|
||||
this.log(LogType.warn, format, args);
|
||||
}
|
||||
|
||||
public static warn(format: string, ...args: any[]) {
|
||||
this.log(LogType.warn, format, args);
|
||||
}
|
||||
|
||||
public static error(format: string, ...args: any[]) {
|
||||
this.log(LogType.error, format, args);
|
||||
}
|
||||
|
||||
public static log(type: LogType, format: string, ...args: any[]) {
|
||||
switch(type) {
|
||||
case LogType.error:
|
||||
console.error(`${type}: ${StringUtils.format(format, args)}`);
|
||||
break;
|
||||
case LogType.warn:
|
||||
console.warn(`${type}: ${StringUtils.format(format, args)}`);
|
||||
break;
|
||||
case LogType.log:
|
||||
console.log(`${type}: ${StringUtils.format(format, args)}`);
|
||||
break;
|
||||
case LogType.info:
|
||||
console.info(`${type}: ${StringUtils.format(format, args)}`);
|
||||
break;
|
||||
case LogType.trace:
|
||||
console.trace(`${type}: ${StringUtils.format(format, args)}`);
|
||||
break;
|
||||
default:
|
||||
throw new Error('argument out of range');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 我们在这里存储了各种系统的默认颜色,如对撞机调试渲染、Debug.drawText等。
|
||||
* 命名方式尽可能采用CLASS-THING,以明确它的使用位置
|
||||
*/
|
||||
export class DebugDefault {
|
||||
public static debugText: number = 0xffffff;
|
||||
|
||||
public static colliderBounds: number = 0xffffff * 0.3;
|
||||
public static colliderEdge: number = 0x8B0000;
|
||||
public static colliderPosition: number = 0xFFFF00;
|
||||
public static colliderCenter: number = 0xFF0000;
|
||||
|
||||
public static renderableBounds: number = 0xFFFF00;
|
||||
public static renderableCenter: number = 0x9932CC;
|
||||
|
||||
public static verletParticle: number = 0xDC345E;
|
||||
public static verletConstraintEdge: number = 0x433E36;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
module es {
|
||||
export class Insist {
|
||||
public static fail(message: string = null, ...args: any[]) {
|
||||
if (message == null) {
|
||||
console.assert(false);
|
||||
} else {
|
||||
console.assert(false, StringUtils.format(message, args));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static isTrue(condition: boolean, message: string = null, ...args: any[]) {
|
||||
if (!condition) {
|
||||
if (message == null) {
|
||||
this.fail();
|
||||
} else {
|
||||
this.fail(message, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static isFalse(condition: boolean, message: string = null, ...args: any[]) {
|
||||
if (message == null) {
|
||||
this.isTrue(!condition);
|
||||
} else {
|
||||
this.isTrue(!condition, message, args);
|
||||
}
|
||||
}
|
||||
|
||||
public static isNull(obj, message: string = null, ...args: any[]) {
|
||||
if (message == null) {
|
||||
this.isTrue(obj == null);
|
||||
} else {
|
||||
this.isTrue(obj == null, message, args);
|
||||
}
|
||||
}
|
||||
|
||||
public static isNotNull(obj, message: string = null, ...args: any[]) {
|
||||
if (message == null) {
|
||||
this.isTrue(obj != null);
|
||||
} else {
|
||||
this.isTrue(obj != null, message, args);
|
||||
}
|
||||
}
|
||||
|
||||
public static areEqual(first, second, message: string, ...args: any[]) {
|
||||
if (first != second)
|
||||
this.fail(message, args);
|
||||
}
|
||||
|
||||
public static areNotEqual(first, second, message: string, ...args: any[]) {
|
||||
if (first == second)
|
||||
this.fail(message, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 执行顺序
|
||||
* - onAddedToEntity
|
||||
* - OnEnabled
|
||||
*
|
||||
* 删除执行顺序
|
||||
* - onRemovedFromEntity
|
||||
*/
|
||||
export abstract class Component {
|
||||
public static _idGenerator: number = 0;
|
||||
/**
|
||||
* 此组件的唯一标识
|
||||
*/
|
||||
public readonly id: number;
|
||||
/**
|
||||
* 此组件附加的实体
|
||||
*/
|
||||
public entity: Entity;
|
||||
|
||||
constructor() {
|
||||
this.id = Component._idGenerator++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速访问 this.entity.transform
|
||||
*/
|
||||
public get transform(): Transform {
|
||||
return this.entity.transform;
|
||||
}
|
||||
|
||||
private _enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 如果组件和实体都已启用,则为。当启用该组件时,将调用该组件的生命周期方法。状态的改变会导致调用onEnabled/onDisable。
|
||||
*/
|
||||
public get enabled() {
|
||||
return this.entity ? this.entity.enabled && this._enabled : this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果组件和实体都已启用,则为。当启用该组件时,将调用该组件的生命周期方法。状态的改变会导致调用onEnabled/onDisable。
|
||||
* @param value
|
||||
*/
|
||||
public set enabled(value: boolean) {
|
||||
this.setEnabled(value);
|
||||
}
|
||||
|
||||
private _updateOrder = 0;
|
||||
|
||||
/** 更新此实体上组件的顺序 */
|
||||
public get updateOrder() {
|
||||
return this._updateOrder;
|
||||
}
|
||||
|
||||
/** 更新此实体上组件的顺序 */
|
||||
public set updateOrder(value: number) {
|
||||
this.setUpdateOrder(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当此组件已分配其实体,但尚未添加到实体的活动组件列表时调用。有用的东西,如物理组件,需要访问转换来修改碰撞体的属性。
|
||||
*/
|
||||
public initialize() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在提交所有挂起的组件更改后,将该组件添加到场景时调用。此时,设置了实体字段和实体。场景也设定好了。
|
||||
*/
|
||||
public onAddedToEntity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当此组件从其实体中移除时调用。在这里做所有的清理工作。
|
||||
*/
|
||||
public onRemovedFromEntity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体的位置改变时调用。这允许组件知道它们由于父实体的移动而移动了。
|
||||
* @param comp
|
||||
*/
|
||||
public onEntityTransformChanged(comp: ComponentTransform) {
|
||||
}
|
||||
|
||||
public debugRender(batcher: IBatcher) {}
|
||||
|
||||
/**
|
||||
*当父实体或此组件启用时调用
|
||||
*/
|
||||
public onEnabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用父实体或此组件时调用
|
||||
*/
|
||||
public onDisabled() {
|
||||
}
|
||||
|
||||
public setEnabled(isEnabled: boolean) {
|
||||
if (this._enabled != isEnabled) {
|
||||
this._enabled = isEnabled;
|
||||
|
||||
if (this._enabled) {
|
||||
this.onEnabled();
|
||||
} else {
|
||||
this.onDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setUpdateOrder(updateOrder: number) {
|
||||
if (this._updateOrder != updateOrder) {
|
||||
this._updateOrder = updateOrder;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
module es {
|
||||
export class ComponentType {
|
||||
public static INDEX = 0;
|
||||
|
||||
private index_ = 0;
|
||||
private type_: Class;
|
||||
|
||||
constructor(type: Class, index?: number) {
|
||||
if (index !== undefined) {
|
||||
this.index_ = ComponentType.INDEX++;
|
||||
} else {
|
||||
this.index_ = index;
|
||||
}
|
||||
this.type_ = type;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
return getClassName(this.type_);
|
||||
}
|
||||
|
||||
public getIndex(): number {
|
||||
return this.index_;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return "ComponentType[" + getClassName(ComponentType) + "] (" + this.index_ + ")";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 接口,当添加到一个Component时,只要Component和实体被启用,它就会在每个框架中调用更新方法。
|
||||
*/
|
||||
export interface IUpdatable {
|
||||
enabled: boolean;
|
||||
updateOrder: number;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于比较组件更新排序
|
||||
*/
|
||||
export class IUpdatableComparer implements IComparer<IUpdatable> {
|
||||
public compare(a: IUpdatable, b: IUpdatable) {
|
||||
return a.updateOrder - b.updateOrder;
|
||||
}
|
||||
}
|
||||
|
||||
export var isIUpdatable = (props: any): props is IUpdatable => typeof (props as IUpdatable)['update'] !== 'undefined';
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 请注意,这不是一个完整的、多迭代的物理系统!它可以用于简单的、街机风格的物理。
|
||||
* 这可以用于简单的,街机风格的物理学
|
||||
*/
|
||||
export class ArcadeRigidbody extends Component implements IUpdatable {
|
||||
/** 这个刚体的质量。质量为0,则是一个不可移动的物体 */
|
||||
public get mass() {
|
||||
return this._mass;
|
||||
}
|
||||
|
||||
public set mass(value: number) {
|
||||
this.setMass(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0-1范围,其中0为无反弹,1为完全反射。
|
||||
*/
|
||||
public get elasticity() {
|
||||
return this._elasticity;
|
||||
}
|
||||
|
||||
public set elasticiy(value: number) {
|
||||
this.setElasticity(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 - 1范围。0表示没有摩擦力,1表示物体会停止在原地
|
||||
*/
|
||||
public get friction() {
|
||||
return this._friction;
|
||||
}
|
||||
|
||||
public set friction(value: number) {
|
||||
this.setFriction(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0-9的范围。当发生碰撞时,沿碰撞面做直线运动时,如果其平方幅度小于glue摩擦力,则将碰撞设置为上限
|
||||
*/
|
||||
public get glue() {
|
||||
return this._glue;
|
||||
}
|
||||
|
||||
public set glue(value: number) {
|
||||
this.setGlue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果为真,则每一帧都会考虑到Physics.gravity
|
||||
*/
|
||||
public shouldUseGravity: boolean = true;
|
||||
|
||||
/**
|
||||
* 该刚体的速度
|
||||
*/
|
||||
public velocity: Vector2 = Vector2.zero;
|
||||
|
||||
/**
|
||||
* 质量为0的刚体被认为是不可移动的。改变速度和碰撞对它们没有影响
|
||||
*/
|
||||
public get isImmovable() {
|
||||
return this._mass < 0.0001;
|
||||
}
|
||||
|
||||
public _mass = 10;
|
||||
public _elasticity = 0.5;
|
||||
public _friction = 0.5;
|
||||
public _glue = 0.01;
|
||||
public _inverseMass: number = 0;
|
||||
public _collider: Collider;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._inverseMass = 1 / this._mass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个刚体的质量。质量为0,则是一个不可移动的物体
|
||||
* @param mass
|
||||
*/
|
||||
public setMass(mass: number): ArcadeRigidbody {
|
||||
this._mass = MathHelper.clamp(mass, 0, Number.MAX_VALUE);
|
||||
|
||||
if (this._mass > 0.0001)
|
||||
this._inverseMass = 1 / this._mass;
|
||||
else
|
||||
this._inverseMass = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 0-1范围,其中0为无反弹,1为完全反射。
|
||||
* @param value
|
||||
*/
|
||||
public setElasticity(value: number): ArcadeRigidbody {
|
||||
this._elasticity = MathHelper.clamp01(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 - 1范围。0表示没有摩擦力,1表示物体会停止在原地
|
||||
* @param value
|
||||
*/
|
||||
public setFriction(value: number): ArcadeRigidbody {
|
||||
this._friction = MathHelper.clamp01(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 0-9的范围。当发生碰撞时,沿碰撞面做直线运动时,如果其平方幅度小于glue摩擦力,则将碰撞设置为上限
|
||||
* @param value
|
||||
*/
|
||||
public setGlue(value: number): ArcadeRigidbody {
|
||||
this._glue = MathHelper.clamp(value, 0, 10);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setVelocity(velocity: Vector2): ArcadeRigidbody {
|
||||
this.velocity = velocity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用刚体的质量给刚体加上一个瞬间的力脉冲。力是一个加速度,单位是每秒像素每秒。将力乘以100000,使数值使用更合理
|
||||
* @param force
|
||||
*/
|
||||
public addImpulse(force: Vector2) {
|
||||
if (!this.isImmovable) {
|
||||
this.velocity.addEqual(force.scale(100000 * (this._inverseMass * (Time.deltaTime * Time.deltaTime))));
|
||||
}
|
||||
}
|
||||
|
||||
public onAddedToEntity() {
|
||||
this._collider = null;
|
||||
for (let i = 0; i < this.entity.components.buffer.length; i++) {
|
||||
let component = this.entity.components.buffer[i];
|
||||
if (component instanceof Collider) {
|
||||
this._collider = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Debug.warnIf(this._collider == null, "ArcadeRigidbody 没有 Collider。ArcadeRigidbody需要一个Collider!");
|
||||
}
|
||||
|
||||
public update() {
|
||||
if (this.isImmovable || this._collider == null) {
|
||||
this.velocity = Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldUseGravity)
|
||||
this.velocity.addEqual(Physics.gravity.scale(Time.deltaTime));
|
||||
this.entity.position = this.entity.position.add(this.velocity.scale(Time.deltaTime));
|
||||
|
||||
let collisionResult = new CollisionResult();
|
||||
|
||||
// 捞取我们在新的位置上可能会碰撞到的任何东西
|
||||
let neighbors = Physics.boxcastBroadphaseExcludingSelf(this._collider, this._collider.bounds, this._collider.collidesWithLayers.value);
|
||||
for (const neighbor of neighbors) {
|
||||
if (!neighbor)
|
||||
continue;
|
||||
|
||||
// 如果邻近的对撞机是同一个实体,则忽略它
|
||||
if (neighbor.entity.equals(this.entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._collider.collidesWithNonMotion(neighbor, collisionResult)) {
|
||||
// 如果附近有一个ArcadeRigidbody,我们就会处理完整的碰撞响应。如果没有,我们会根据附近是不可移动的来计算事情
|
||||
let neighborRigidbody = neighbor.entity.getComponent(ArcadeRigidbody);
|
||||
if (neighborRigidbody != null) {
|
||||
this.processOverlap(neighborRigidbody, collisionResult.minimumTranslationVector);
|
||||
this.processCollision(neighborRigidbody, collisionResult.minimumTranslationVector);
|
||||
} else {
|
||||
// 没有ArcadeRigidbody,所以我们假设它是不动的,只移动我们自己的
|
||||
this.entity.position = this.entity.position.sub(collisionResult.minimumTranslationVector);
|
||||
const relativeVelocity = this.calculateResponseVelocity(this.velocity, collisionResult.minimumTranslationVector);
|
||||
this.velocity.addEqual(relativeVelocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将两个重叠的刚体分开。也处理其中一个不可移动的情况
|
||||
* @param other
|
||||
* @param minimumTranslationVector
|
||||
*/
|
||||
public processOverlap(other: ArcadeRigidbody, minimumTranslationVector: Vector2) {
|
||||
if (this.isImmovable) {
|
||||
other.entity.position = other.entity.position.add(minimumTranslationVector);
|
||||
} else if (other.isImmovable) {
|
||||
this.entity.position = this.entity.position.sub(minimumTranslationVector);
|
||||
} else {
|
||||
this.entity.position = this.entity.position.sub(minimumTranslationVector.scale(0.5));
|
||||
other.entity.position = other.entity.position.add(minimumTranslationVector.scale(0.5));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理两个非重叠的刚体的碰撞。新的速度将根据情况分配给每个刚体
|
||||
* @param other
|
||||
* @param minimumTranslationVector
|
||||
*/
|
||||
public processCollision(other: ArcadeRigidbody, minimumTranslationVector: Vector2) {
|
||||
// 我们计算两个相撞物体的响应。
|
||||
// 计算的基础是沿碰撞表面法线反射的物体的相对速度。
|
||||
// 然后,响应的一部分会根据质量加到每个物体上
|
||||
let relativeVelocity = this.velocity.sub(other.velocity);
|
||||
|
||||
relativeVelocity = this.calculateResponseVelocity(relativeVelocity, minimumTranslationVector);
|
||||
|
||||
// 现在,我们使用质量来线性缩放两个刚体上的响应
|
||||
const totalinverseMass = this._inverseMass + other._inverseMass;
|
||||
const ourResponseFraction = this._inverseMass / totalinverseMass;
|
||||
const otherResponseFraction = other._inverseMass / totalinverseMass;
|
||||
|
||||
|
||||
this.velocity = this.velocity.add(relativeVelocity.scale(ourResponseFraction));
|
||||
other.velocity = other.velocity.sub(relativeVelocity.scale(otherResponseFraction));
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定两个物体和MTV之间的相对速度,本方法修改相对速度,使其成为碰撞响应
|
||||
* @param relativeVelocity
|
||||
* @param minimumTranslationVector
|
||||
* @param responseVelocity
|
||||
*/
|
||||
public calculateResponseVelocity(relativeVelocity: Vector2, minimumTranslationVector: Vector2) {
|
||||
// 首先,我们得到反方向的归一化MTV:表面法线
|
||||
let inverseMTV = minimumTranslationVector.scale(-1);
|
||||
let normal = inverseMTV.normalize();
|
||||
// 速度是沿碰撞法线和碰撞平面分解的。
|
||||
// 弹性将影响沿法线的响应(法线速度分量),摩擦力将影响速度的切向分量(切向速度分量)
|
||||
let n = relativeVelocity.dot(normal);
|
||||
|
||||
let normalVelocityComponent = normal.scale(n);
|
||||
let tangentialVelocityComponent = relativeVelocity.sub(normalVelocityComponent);
|
||||
|
||||
if (n > 0)
|
||||
normalVelocityComponent = Vector2.zero;
|
||||
|
||||
// 如果切向分量的平方幅度小于glue,那么我们就把摩擦力提升到最大
|
||||
let coefficientOfFriction = this._friction;
|
||||
if (tangentialVelocityComponent.lengthSquared() < this._glue)
|
||||
coefficientOfFriction = 1.01;
|
||||
|
||||
// 弹性影响速度的法向分量,摩擦力影响速度的切向分量
|
||||
return normalVelocityComponent
|
||||
.scale(1 + this._elasticity)
|
||||
.sub(tangentialVelocityComponent.scale(coefficientOfFriction))
|
||||
.scale(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
module es {
|
||||
class CharacterRaycastOrigins {
|
||||
public topLeft: Vector2;
|
||||
public bottomRight: Vector2;
|
||||
public bottomLeft: Vector2;
|
||||
|
||||
public constructor() {
|
||||
this.topLeft = Vector2.zero;
|
||||
this.bottomRight = Vector2.zero;
|
||||
this.bottomLeft = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
class CharacterCollisionState2D {
|
||||
public right: boolean = false;
|
||||
public left: boolean = false;
|
||||
public above: boolean = false;
|
||||
public below: boolean = false;
|
||||
public becameGroundedThisFrame: boolean = false;
|
||||
public wasGroundedLastFrame: boolean = false;
|
||||
public movingDownSlope: boolean = false;
|
||||
public slopeAngle: number = 0;
|
||||
|
||||
public hasCollision(): boolean {
|
||||
return this.below || this.right || this.left || this.above;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.right = this.left = false;
|
||||
this.above = this.below = false;
|
||||
this.becameGroundedThisFrame = this.movingDownSlope = false;
|
||||
this.slopeAngle = 0;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `[CharacterCollisionState2D] r: ${this.right}, l: ${this.left}, a: ${this.above}, b: ${this.below}, movingDownSlope: ${this.movingDownSlope}, angle: ${this.slopeAngle}, wasGroundedLastFrame: ${this.wasGroundedLastFrame}, becameGroundedThisFrame: ${this.becameGroundedThisFrame}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class CharacterController implements ITriggerListener {
|
||||
public onControllerCollidedEvent: ObservableT<RaycastHit>;
|
||||
public onTriggerEnterEvent: ObservableT<Collider>;
|
||||
public onTriggerExitEvent: ObservableT<Collider>;
|
||||
|
||||
|
||||
/**
|
||||
* 如果为 true,则在垂直移动单帧时将忽略平台的一种方式
|
||||
*/
|
||||
public ignoreOneWayPlatformsTime: number;
|
||||
public supportSlopedOneWayPlatforms: boolean;
|
||||
|
||||
public ignoredColliders: Set<Collider> = new Set();
|
||||
|
||||
|
||||
/**
|
||||
* 定义距离碰撞射线的边缘有多远。
|
||||
* 如果使用 0 范围进行投射,则通常会导致不需要的光线击中(例如,直接从表面水平投射的足部碰撞器可能会导致击中)
|
||||
*/
|
||||
public get skinWidth() {
|
||||
return this._skinWidth;
|
||||
}
|
||||
|
||||
public set skinWidth(value: number) {
|
||||
this._skinWidth = value;
|
||||
this.recalculateDistanceBetweenRays();
|
||||
}
|
||||
|
||||
/**
|
||||
* CC2D 可以爬升的最大坡度角
|
||||
*/
|
||||
public slopeLimit: number = 30;
|
||||
|
||||
/**
|
||||
* 构成跳跃的帧之间垂直运动变化的阈值
|
||||
*/
|
||||
public jumpingThreshold: number = -7;
|
||||
|
||||
/**
|
||||
* 基于斜率乘以速度的曲线(负 = 下坡和正 = 上坡)
|
||||
*/
|
||||
public slopeSpeedMultiplier: AnimCurve;
|
||||
|
||||
public totalHorizontalRays: number = 5;
|
||||
public totalVerticalRays: number = 3;
|
||||
|
||||
public collisionState: CharacterCollisionState2D = new CharacterCollisionState2D();
|
||||
public velocity: Vector2 = new Vector2(0, 0);
|
||||
|
||||
public get isGrounded(): boolean {
|
||||
return this.collisionState.below;
|
||||
}
|
||||
|
||||
public get raycastHitsThisFrame(): RaycastHit[] {
|
||||
return this._raycastHitsThisFrame;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
player: Entity,
|
||||
skinWidth?: number,
|
||||
platformMask: number = -1,
|
||||
onewayPlatformMask: number = -1,
|
||||
triggerMask: number = -1
|
||||
) {
|
||||
this.onTriggerEnterEvent = new ObservableT();
|
||||
this.onTriggerExitEvent = new ObservableT();
|
||||
this.onControllerCollidedEvent = new ObservableT();
|
||||
|
||||
this.platformMask = platformMask;
|
||||
this.oneWayPlatformMask = onewayPlatformMask;
|
||||
this.triggerMask = triggerMask;
|
||||
|
||||
// 将我们的单向平台添加到我们的普通平台掩码中,以便我们可以从上方降落
|
||||
this.platformMask |= this.oneWayPlatformMask;
|
||||
|
||||
this._player = player;
|
||||
let collider = null;
|
||||
for (let i = 0; i < this._player.components.buffer.length; i++) {
|
||||
let component = this._player.components.buffer[i];
|
||||
if (component instanceof Collider) {
|
||||
collider = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
collider.isTrigger = false;
|
||||
if (collider instanceof BoxCollider) {
|
||||
this._collider = collider as BoxCollider;
|
||||
} else {
|
||||
throw new Error('player collider must be box');
|
||||
}
|
||||
|
||||
// 在这里,我们触发了具有主体的 setter 的属性
|
||||
this.skinWidth = skinWidth || collider.width * 0.05;
|
||||
|
||||
this._slopeLimitTangent = Math.tan(75 * MathHelper.Deg2Rad);
|
||||
this._triggerHelper = new ColliderTriggerHelper(this._player);
|
||||
|
||||
// 我们想设置我们的 CC2D 忽略所有碰撞层,除了我们的 triggerMask
|
||||
for (let i = 0; i < 32; i++) {
|
||||
// 查看我们的 triggerMask 是否包含此层,如果不包含则忽略它
|
||||
if ((this.triggerMask & (1 << i)) === 0) {
|
||||
Flags.unsetFlag(this._collider.collidesWithLayers, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onTriggerEnter(other: Collider, local: Collider): void {
|
||||
this.onTriggerEnterEvent.notify(other);
|
||||
}
|
||||
|
||||
public onTriggerExit(other: Collider, local: Collider): void {
|
||||
this.onTriggerExitEvent.notify(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试将角色移动到位置 + deltaMovement。 任何挡路的碰撞器都会在遇到时导致运动停止
|
||||
* @param deltaMovement
|
||||
* @param deltaTime
|
||||
*/
|
||||
public move(deltaMovement: Vector2, deltaTime: number): void {
|
||||
this.collisionState.wasGroundedLastFrame = this.collisionState.below;
|
||||
|
||||
this.collisionState.reset();
|
||||
this._raycastHitsThisFrame = [];
|
||||
this._isGoingUpSlope = false;
|
||||
|
||||
this.primeRaycastOrigins();
|
||||
|
||||
if (deltaMovement.y > 0 && this.collisionState.wasGroundedLastFrame) {
|
||||
deltaMovement = this.handleVerticalSlope(deltaMovement);
|
||||
}
|
||||
|
||||
if (deltaMovement.x !== 0) {
|
||||
deltaMovement = this.moveHorizontally(deltaMovement);
|
||||
}
|
||||
|
||||
if (deltaMovement.y !== 0) {
|
||||
deltaMovement = this.moveVertically(deltaMovement);
|
||||
}
|
||||
|
||||
this._player.setPosition(
|
||||
this._player.position.x + deltaMovement.x,
|
||||
this._player.position.y + deltaMovement.y
|
||||
);
|
||||
|
||||
if (deltaTime > 0) {
|
||||
this.velocity.x = deltaMovement.x / deltaTime;
|
||||
this.velocity.y = deltaMovement.y / deltaTime;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.collisionState.wasGroundedLastFrame &&
|
||||
this.collisionState.below
|
||||
) {
|
||||
this.collisionState.becameGroundedThisFrame = true;
|
||||
}
|
||||
|
||||
if (this._isGoingUpSlope) {
|
||||
this.velocity.y = 0;
|
||||
}
|
||||
|
||||
if (!this._isWarpingToGround) {
|
||||
this._triggerHelper.update();
|
||||
}
|
||||
for (let i = 0; i < this._raycastHitsThisFrame.length; i++) {
|
||||
this.onControllerCollidedEvent.notify(this._raycastHitsThisFrame[i]);
|
||||
}
|
||||
|
||||
if (this.ignoreOneWayPlatformsTime > 0) {
|
||||
this.ignoreOneWayPlatformsTime -= deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 直接向下移动直到接地
|
||||
* @param maxDistance
|
||||
*/
|
||||
public warpToGrounded(maxDistance: number = 1000): void {
|
||||
this.ignoreOneWayPlatformsTime = 0;
|
||||
this._isWarpingToGround = true;
|
||||
let delta = 0;
|
||||
do {
|
||||
delta += 1;
|
||||
this.move(new Vector2(0, 1), 0.02);
|
||||
if (delta > maxDistance) {
|
||||
break;
|
||||
}
|
||||
} while (!this.isGrounded);
|
||||
this._isWarpingToGround = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这应该在您必须在运行时修改 BoxCollider2D 的任何时候调用。
|
||||
* 它将重新计算用于碰撞检测的光线之间的距离。
|
||||
* 它也用于 skinWidth setter,以防在运行时更改。
|
||||
*/
|
||||
public recalculateDistanceBetweenRays(): void {
|
||||
const colliderUsableHeight =
|
||||
this._collider.height * Math.abs(this._player.scale.y) -
|
||||
2 * this._skinWidth;
|
||||
this._verticalDistanceBetweenRays =
|
||||
colliderUsableHeight / (this.totalHorizontalRays - 1);
|
||||
|
||||
const colliderUsableWidth =
|
||||
this._collider.width * Math.abs(this._player.scale.x) -
|
||||
2 * this._skinWidth;
|
||||
this._horizontalDistanceBetweenRays =
|
||||
colliderUsableWidth / (this.totalVerticalRays - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 raycastOrigins 重置为由 skinWidth 插入的框碰撞器的当前范围。
|
||||
* 插入它是为了避免从直接接触另一个碰撞器的位置投射光线,从而导致不稳定的法线数据。
|
||||
*/
|
||||
private primeRaycastOrigins(): void {
|
||||
const rect = this._collider.bounds;
|
||||
this._raycastOrigins.topLeft = new Vector2(
|
||||
rect.x + this._skinWidth,
|
||||
rect.y + this._skinWidth
|
||||
);
|
||||
this._raycastOrigins.bottomRight = new Vector2(
|
||||
rect.right - this._skinWidth,
|
||||
rect.bottom - this._skinWidth
|
||||
);
|
||||
this._raycastOrigins.bottomLeft = new Vector2(
|
||||
rect.x + this._skinWidth,
|
||||
rect.bottom - this._skinWidth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我们必须在这方面使用一些技巧。
|
||||
* 光线必须从我们的碰撞器(skinWidth)内部的一小段距离投射,以避免零距离光线会得到错误的法线。
|
||||
* 由于这个小偏移,我们必须增加光线距离 skinWidth 然后记住在实际移动玩家之前从 deltaMovement 中删除 skinWidth
|
||||
* @param deltaMovement
|
||||
* @returns
|
||||
*/
|
||||
private moveHorizontally(deltaMovement: Vector2): Vector2 {
|
||||
const isGoingRight = deltaMovement.x > 0;
|
||||
let rayDistance =
|
||||
Math.abs(deltaMovement.x) +
|
||||
this._skinWidth * this.rayOriginSkinMutiplier;
|
||||
const rayDirection: Vector2 = isGoingRight ? Vector2.right : Vector2.left;
|
||||
const initialRayOriginY = this._raycastOrigins.bottomLeft.y;
|
||||
const initialRayOriginX = isGoingRight
|
||||
? this._raycastOrigins.bottomRight.x -
|
||||
this._skinWidth * (this.rayOriginSkinMutiplier - 1)
|
||||
: this._raycastOrigins.bottomLeft.x +
|
||||
this._skinWidth * (this.rayOriginSkinMutiplier - 1);
|
||||
|
||||
for (let i = 0; i < this.totalHorizontalRays; i++) {
|
||||
const ray = new Vector2(
|
||||
initialRayOriginX,
|
||||
initialRayOriginY - i * this._verticalDistanceBetweenRays
|
||||
);
|
||||
|
||||
// 如果我们接地,我们将只在第一条射线(底部)上包含 oneWayPlatforms。
|
||||
// 允许我们走上倾斜的 oneWayPlatforms
|
||||
if (
|
||||
i === 0 &&
|
||||
this.supportSlopedOneWayPlatforms &&
|
||||
this.collisionState.wasGroundedLastFrame
|
||||
) {
|
||||
this._raycastHit = Physics.linecast(
|
||||
ray,
|
||||
ray.add(rayDirection.scaleEqual(rayDistance)),
|
||||
this.platformMask,
|
||||
this.ignoredColliders
|
||||
);
|
||||
} else {
|
||||
this._raycastHit = Physics.linecast(
|
||||
ray,
|
||||
ray.add(rayDirection.scaleEqual(rayDistance)),
|
||||
this.platformMask & ~this.oneWayPlatformMask,
|
||||
this.ignoredColliders
|
||||
);
|
||||
}
|
||||
|
||||
if (this._raycastHit.collider) {
|
||||
if (
|
||||
i === 0 &&
|
||||
this.handleHorizontalSlope(
|
||||
deltaMovement,
|
||||
Vector2.unsignedAngle(this._raycastHit.normal, Vector2.up)
|
||||
)
|
||||
) {
|
||||
this._raycastHitsThisFrame.push(this._raycastHit);
|
||||
break;
|
||||
}
|
||||
|
||||
deltaMovement.x = this._raycastHit.point.x - ray.x;
|
||||
rayDistance = Math.abs(deltaMovement.x);
|
||||
|
||||
if (isGoingRight) {
|
||||
deltaMovement.x -= this._skinWidth * this.rayOriginSkinMutiplier;
|
||||
this.collisionState.right = true;
|
||||
} else {
|
||||
deltaMovement.x += this._skinWidth * this.rayOriginSkinMutiplier;
|
||||
this.collisionState.left = true;
|
||||
}
|
||||
|
||||
this._raycastHitsThisFrame.push(this._raycastHit);
|
||||
|
||||
if (
|
||||
rayDistance <
|
||||
this._skinWidth * this.rayOriginSkinMutiplier +
|
||||
this.kSkinWidthFloatFudgeFactor
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deltaMovement;
|
||||
}
|
||||
|
||||
private moveVertically(deltaMovement: Vector2): Vector2 {
|
||||
const isGoingUp = deltaMovement.y < 0;
|
||||
let rayDistance =
|
||||
Math.abs(deltaMovement.y) +
|
||||
this._skinWidth * this.rayOriginSkinMutiplier;
|
||||
const rayDirection = isGoingUp ? Vector2.up : Vector2.down;
|
||||
|
||||
let initialRayOriginX = this._raycastOrigins.topLeft.x;
|
||||
const initialRayOriginY = isGoingUp
|
||||
? this._raycastOrigins.topLeft.y +
|
||||
this._skinWidth * (this.rayOriginSkinMutiplier - 1)
|
||||
: this._raycastOrigins.bottomLeft.y -
|
||||
this._skinWidth * (this.rayOriginSkinMutiplier - 1);
|
||||
|
||||
initialRayOriginX += deltaMovement.x;
|
||||
|
||||
let mask = this.platformMask;
|
||||
if (isGoingUp || this.ignoreOneWayPlatformsTime > 0) {
|
||||
mask &= ~this.oneWayPlatformMask;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.totalVerticalRays; i++) {
|
||||
const rayStart = new Vector2(
|
||||
initialRayOriginX + i * this._horizontalDistanceBetweenRays,
|
||||
initialRayOriginY
|
||||
);
|
||||
this._raycastHit = Physics.linecast(
|
||||
rayStart,
|
||||
rayStart.add(rayDirection.scaleEqual(rayDistance)),
|
||||
mask,
|
||||
this.ignoredColliders
|
||||
);
|
||||
if (this._raycastHit.collider) {
|
||||
deltaMovement.y = this._raycastHit.point.y - rayStart.y;
|
||||
rayDistance = Math.abs(deltaMovement.y);
|
||||
|
||||
if (isGoingUp) {
|
||||
deltaMovement.y += this._skinWidth * this.rayOriginSkinMutiplier;
|
||||
this.collisionState.above = true;
|
||||
} else {
|
||||
deltaMovement.y -= this._skinWidth * this.rayOriginSkinMutiplier;
|
||||
this.collisionState.below = true;
|
||||
}
|
||||
|
||||
this._raycastHitsThisFrame.push(this._raycastHit);
|
||||
|
||||
if (!isGoingUp && deltaMovement.y < -0.00001) {
|
||||
this._isGoingUpSlope = true;
|
||||
}
|
||||
|
||||
if (
|
||||
rayDistance <
|
||||
this._skinWidth * this.rayOriginSkinMutiplier +
|
||||
this.kSkinWidthFloatFudgeFactor
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deltaMovement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 BoxCollider2D 下的中心点是否存在坡度。
|
||||
* 如果找到一个,则调整 deltaMovement 以便玩家保持接地,并考虑slopeSpeedModifier 以加快移动速度。
|
||||
* @param deltaMovement
|
||||
* @returns
|
||||
*/
|
||||
private handleVerticalSlope(deltaMovement: Vector2): Vector2 {
|
||||
const centerOfCollider =
|
||||
(this._raycastOrigins.bottomLeft.x +
|
||||
this._raycastOrigins.bottomRight.x) *
|
||||
0.5;
|
||||
const rayDirection = Vector2.down;
|
||||
|
||||
const slopeCheckRayDistance =
|
||||
this._slopeLimitTangent *
|
||||
(this._raycastOrigins.bottomRight.x - centerOfCollider);
|
||||
|
||||
const slopeRay = new Vector2(
|
||||
centerOfCollider,
|
||||
this._raycastOrigins.bottomLeft.y
|
||||
);
|
||||
|
||||
this._raycastHit = Physics.linecast(
|
||||
slopeRay,
|
||||
slopeRay.add(rayDirection.scaleEqual(slopeCheckRayDistance)),
|
||||
this.platformMask,
|
||||
this.ignoredColliders
|
||||
);
|
||||
if (this._raycastHit.collider) {
|
||||
const angle = Vector2.unsignedAngle(this._raycastHit.normal, Vector2.up);
|
||||
if (angle === 0) {
|
||||
return deltaMovement;
|
||||
}
|
||||
|
||||
const isMovingDownSlope =
|
||||
Math.sign(this._raycastHit.normal.x) === Math.sign(deltaMovement.x);
|
||||
if (isMovingDownSlope) {
|
||||
const slopeModifier = this.slopeSpeedMultiplier
|
||||
? this.slopeSpeedMultiplier.lerp(-angle)
|
||||
: 1;
|
||||
deltaMovement.y +=
|
||||
this._raycastHit.point.y - slopeRay.y - this.skinWidth;
|
||||
deltaMovement.x *= slopeModifier;
|
||||
this.collisionState.movingDownSlope = true;
|
||||
this.collisionState.slopeAngle = angle;
|
||||
}
|
||||
}
|
||||
|
||||
return deltaMovement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果我们要上坡,则处理调整 deltaMovement
|
||||
* @param deltaMovement
|
||||
* @param angle
|
||||
* @returns
|
||||
*/
|
||||
private handleHorizontalSlope(
|
||||
deltaMovement: Vector2,
|
||||
angle: number
|
||||
): boolean {
|
||||
if (Math.round(angle) === 90) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (angle < this.slopeLimit) {
|
||||
if (deltaMovement.y > this.jumpingThreshold) {
|
||||
const slopeModifier = this.slopeSpeedMultiplier
|
||||
? this.slopeSpeedMultiplier.lerp(angle)
|
||||
: 1;
|
||||
deltaMovement.x *= slopeModifier;
|
||||
|
||||
deltaMovement.y = Math.abs(
|
||||
Math.tan(angle * MathHelper.Deg2Rad) * deltaMovement.x
|
||||
);
|
||||
const isGoingRight = deltaMovement.x > 0;
|
||||
|
||||
const ray = isGoingRight
|
||||
? this._raycastOrigins.bottomRight
|
||||
: this._raycastOrigins.bottomLeft;
|
||||
let raycastHit = null;
|
||||
if (
|
||||
this.supportSlopedOneWayPlatforms &&
|
||||
this.collisionState.wasGroundedLastFrame
|
||||
) {
|
||||
raycastHit = Physics.linecast(
|
||||
ray,
|
||||
ray.add(deltaMovement),
|
||||
this.platformMask,
|
||||
this.ignoredColliders
|
||||
);
|
||||
} else {
|
||||
raycastHit = Physics.linecast(
|
||||
ray,
|
||||
ray.add(deltaMovement),
|
||||
this.platformMask & ~this.oneWayPlatformMask,
|
||||
this.ignoredColliders
|
||||
);
|
||||
}
|
||||
|
||||
if (raycastHit.collider) {
|
||||
deltaMovement.x = raycastHit.point.x - ray.x;
|
||||
deltaMovement.y = raycastHit.point.y - ray.y;
|
||||
if (isGoingRight) {
|
||||
deltaMovement.x -= this._skinWidth;
|
||||
} else {
|
||||
deltaMovement.x += this._skinWidth;
|
||||
}
|
||||
}
|
||||
|
||||
this._isGoingUpSlope = true;
|
||||
this.collisionState.below = true;
|
||||
}
|
||||
} else {
|
||||
deltaMovement.x = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _player: Entity;
|
||||
private _collider: BoxCollider;
|
||||
private _skinWidth: number = 0.02;
|
||||
private _triggerHelper: ColliderTriggerHelper;
|
||||
|
||||
/**
|
||||
* 这用于计算为检查坡度而投射的向下光线。
|
||||
* 我们使用有点随意的值 75 度来计算检查斜率的射线的长度。
|
||||
*/
|
||||
private _slopeLimitTangent: number;
|
||||
|
||||
private readonly kSkinWidthFloatFudgeFactor: number = 0.001;
|
||||
|
||||
/**
|
||||
* 我们的光线投射原点角的支架(TR、TL、BR、BL)
|
||||
*/
|
||||
private _raycastOrigins: CharacterRaycastOrigins = new CharacterRaycastOrigins();
|
||||
|
||||
/**
|
||||
* 存储我们在移动过程中命中的光线投射
|
||||
*/
|
||||
private _raycastHit: RaycastHit = new RaycastHit();
|
||||
|
||||
/**
|
||||
* 存储此帧发生的任何光线投射命中。
|
||||
* 我们必须存储它们,以防我们遇到水平和垂直移动的碰撞,以便我们可以在设置所有碰撞状态后发送事件
|
||||
*/
|
||||
private _raycastHitsThisFrame: RaycastHit[];
|
||||
|
||||
// 水平/垂直移动数据
|
||||
private _verticalDistanceBetweenRays: number;
|
||||
private _horizontalDistanceBetweenRays: number;
|
||||
|
||||
/**
|
||||
* 我们使用这个标志来标记我们正在爬坡的情况,我们修改了 delta.y 以允许爬升。
|
||||
* 原因是,如果我们到达斜坡的尽头,我们可以进行调整以保持接地
|
||||
*/
|
||||
private _isGoingUpSlope: boolean = false;
|
||||
|
||||
private _isWarpingToGround: boolean = true;
|
||||
|
||||
private platformMask: number = -1;
|
||||
private triggerMask: number = -1;
|
||||
private oneWayPlatformMask: number = -1;
|
||||
|
||||
private readonly rayOriginSkinMutiplier = 4;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
///<reference path="./Collider.ts" />
|
||||
module es {
|
||||
export class BoxCollider extends Collider {
|
||||
/**
|
||||
* 创建一个BoxCollider,并使用x/y组件作为局部Offset
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
constructor(x: number = 0, y: number = 0, width: number = 1, height: number = 1) {
|
||||
super();
|
||||
|
||||
if (width == 1 && height == 1) {
|
||||
this._colliderRequiresAutoSizing = true;
|
||||
} else {
|
||||
this._localOffset = new Vector2(x + width / 2, y + height / 2);
|
||||
}
|
||||
|
||||
this.shape = new Box(width, height);
|
||||
}
|
||||
|
||||
public get width() {
|
||||
return (this.shape as Box).width;
|
||||
}
|
||||
|
||||
public set width(value: number) {
|
||||
this.setWidth(value);
|
||||
}
|
||||
|
||||
public get height() {
|
||||
return (this.shape as Box).height;
|
||||
}
|
||||
|
||||
public set height(value: number) {
|
||||
this.setHeight(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置BoxCollider的大小
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
public setSize(width: number, height: number) {
|
||||
this._colliderRequiresAutoSizing = false;
|
||||
let box = this.shape as Box;
|
||||
if (width != box.width || height != box.height) {
|
||||
// 更新框,改变边界,如果我们需要更新物理系统中的边界
|
||||
box.updateBox(width, height);
|
||||
this._isPositionDirty = true;
|
||||
if (this.entity && this._isParentEntityAddedToScene)
|
||||
Physics.updateCollider(this);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置BoxCollider的宽度
|
||||
* @param width
|
||||
*/
|
||||
public setWidth(width: number): BoxCollider {
|
||||
this._colliderRequiresAutoSizing = false;
|
||||
let box = this.shape as Box;
|
||||
if (width != box.width) {
|
||||
// 更新框,改变边界,如果我们需要更新物理系统中的边界
|
||||
box.updateBox(width, box.height);
|
||||
this._isPositionDirty = true;
|
||||
if (this.entity != null && this._isParentEntityAddedToScene)
|
||||
Physics.updateCollider(this);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置BoxCollider的高度
|
||||
* @param height
|
||||
*/
|
||||
public setHeight(height: number) {
|
||||
this._colliderRequiresAutoSizing = false;
|
||||
let box = this.shape as Box;
|
||||
if (height != box.height) {
|
||||
// 更新框,改变边界,如果我们需要更新物理系统中的边界
|
||||
box.updateBox(box.width, height);
|
||||
this._isPositionDirty = true;
|
||||
if (this.entity && this._isParentEntityAddedToScene)
|
||||
Physics.updateCollider(this);
|
||||
}
|
||||
}
|
||||
|
||||
public debugRender(batcher: IBatcher) {
|
||||
let poly = this.shape as Polygon;
|
||||
batcher.drawHollowRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height, new Color(76, 76, 76, 76), 2);
|
||||
batcher.end();
|
||||
batcher.drawPolygon(this.shape.position, poly.points, new Color(139, 0, 0, 255), true, 2);
|
||||
batcher.end();
|
||||
batcher.drawPixel(this.entity.position, new Color(255, 255, 0), 4);
|
||||
batcher.end();
|
||||
batcher.drawPixel(es.Vector2.add(this.transform.position, this.shape.center), new Color(255, 0, 0), 2);
|
||||
batcher.end();
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `[BoxCollider: bounds: ${this.bounds}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
module es {
|
||||
export class CircleCollider extends Collider {
|
||||
/**
|
||||
* 创建一个具有半径的CircleCollider。
|
||||
* 请注意,当指定半径时,如果在实体上使用RenderableComponent,您将需要设置原点来对齐CircleCollider。
|
||||
* 例如,如果RenderableComponent有一个0,0的原点,并且创建了一个半径为1.5f * renderable.width的CircleCollider,你可以通过设置originNormalied为中心除以缩放尺寸来偏移原点
|
||||
*
|
||||
* @param radius
|
||||
*/
|
||||
constructor(radius: number = 1) {
|
||||
super();
|
||||
|
||||
this.shape = new Circle(radius);
|
||||
if (radius == 1) {
|
||||
this._colliderRequiresAutoSizing = true;
|
||||
}
|
||||
}
|
||||
|
||||
public get radius(): number {
|
||||
return (this.shape as Circle).radius;
|
||||
}
|
||||
|
||||
public set radius(value: number) {
|
||||
this.setRadius(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置圆的半径
|
||||
* @param radius
|
||||
*/
|
||||
public setRadius(radius: number): CircleCollider {
|
||||
this._colliderRequiresAutoSizing = false;
|
||||
let circle = this.shape as Circle;
|
||||
if (radius != circle.radius) {
|
||||
circle.radius = radius;
|
||||
circle._originalRadius = radius;
|
||||
this._isPositionDirty = true;
|
||||
|
||||
if (this.entity != null && this._isParentEntityAddedToScene)
|
||||
Physics.updateCollider(this);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public debugRender(batcher: IBatcher) {
|
||||
batcher.drawHollowRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height, new Color(76, 76, 76, 76), 2);
|
||||
batcher.end();
|
||||
batcher.drawCircle(this.shape.position, this.radius, new Color(139, 0, 0), 2);
|
||||
batcher.end();
|
||||
batcher.drawPixel(this.entity.transform.position, new Color(255, 255, 0), 4);
|
||||
batcher.end();
|
||||
batcher.drawPixel(this.shape.position, new Color(255, 0, 0), 2);
|
||||
batcher.end();
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `[CircleCollider: bounds: ${this.bounds}, radius: ${(this.shape as Circle).radius}]`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
module es {
|
||||
export abstract class Collider extends Component {
|
||||
public static readonly lateSortOrder = 999;
|
||||
public castSortOrder: number = 0;
|
||||
/**
|
||||
* 对撞机的基本形状
|
||||
*/
|
||||
public shape: Shape;
|
||||
/**
|
||||
* 如果这个碰撞器是一个触发器,它将不会引起碰撞,但它仍然会触发事件
|
||||
*/
|
||||
public isTrigger: boolean = false;
|
||||
/**
|
||||
* 在处理冲突时,physicsLayer可以用作过滤器。Flags类有帮助位掩码的方法
|
||||
*/
|
||||
public physicsLayer = new Ref(1 << 0);
|
||||
/**
|
||||
* 碰撞器在使用移动器移动时应该碰撞的层
|
||||
* 默认为所有层
|
||||
*/
|
||||
public collidesWithLayers: Ref<number> = new Ref(Physics.allLayers);
|
||||
/**
|
||||
* 如果为true,碰撞器将根据附加的变换缩放和旋转
|
||||
*/
|
||||
public shouldColliderScaleAndRotateWithTransform = true;
|
||||
/**
|
||||
* 这个对撞机在物理系统注册时的边界。
|
||||
* 存储这个允许我们始终能够安全地从物理系统中移除对撞机,即使它在试图移除它之前已经被移动了。
|
||||
*/
|
||||
public registeredPhysicsBounds: Rectangle = new Rectangle();
|
||||
|
||||
protected _colliderRequiresAutoSizing: boolean;
|
||||
|
||||
public _localOffsetLength: number;
|
||||
public _isPositionDirty: boolean = true;
|
||||
public _isRotationDirty: boolean = true;
|
||||
/**
|
||||
* 标记来跟踪我们的实体是否被添加到场景中
|
||||
*/
|
||||
protected _isParentEntityAddedToScene;
|
||||
/**
|
||||
* 标记来记录我们是否注册了物理系统
|
||||
*/
|
||||
protected _isColliderRegistered;
|
||||
|
||||
/**
|
||||
* 镖师碰撞器的绝对位置
|
||||
*/
|
||||
public get absolutePosition(): Vector2 {
|
||||
return Vector2.add(this.entity.transform.position, this._localOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装变换。如果碰撞器没和实体一起旋转 则返回transform.rotation
|
||||
*/
|
||||
public get rotation(): number {
|
||||
if (this.shouldColliderScaleAndRotateWithTransform && this.entity != null)
|
||||
return this.entity.transform.rotation;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public get bounds(): Rectangle {
|
||||
if (this._isPositionDirty || this._isRotationDirty) {
|
||||
this.shape.recalculateBounds(this);
|
||||
this._isPositionDirty = this._isRotationDirty = false;
|
||||
}
|
||||
|
||||
return this.shape.bounds;
|
||||
}
|
||||
|
||||
protected _localOffset: Vector2 = Vector2.zero;
|
||||
|
||||
/**
|
||||
* 将localOffset添加到实体。获取碰撞器几何图形的最终位置。
|
||||
* 允许向一个实体添加多个碰撞器并分别定位,还允许你设置缩放/旋转
|
||||
*/
|
||||
public get localOffset(): Vector2 {
|
||||
return this._localOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将localOffset添加到实体。获取碰撞器几何图形的最终位置。
|
||||
* 允许向一个实体添加多个碰撞器并分别定位,还允许你设置缩放/旋转
|
||||
* @param value
|
||||
*/
|
||||
public set localOffset(value: Vector2) {
|
||||
this.setLocalOffset(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将localOffset添加到实体。获取碰撞器的最终位置。
|
||||
* 这允许您向一个实体添加多个碰撞器并分别定位它们。
|
||||
* @param offset
|
||||
*/
|
||||
public setLocalOffset(offset: Vector2): Collider {
|
||||
if (!this._localOffset.equals(offset)) {
|
||||
this.unregisterColliderWithPhysicsSystem();
|
||||
this._localOffset.setTo(offset.x, offset.y);
|
||||
this._localOffsetLength = this._localOffset.magnitude();
|
||||
this._isPositionDirty = true;
|
||||
this.registerColliderWithPhysicsSystem();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果为true,碰撞器将根据附加的变换缩放和旋转
|
||||
* @param shouldColliderScaleAndRotationWithTransform
|
||||
*/
|
||||
public setShouldColliderScaleAndRotateWithTransform(shouldColliderScaleAndRotationWithTransform: boolean): Collider {
|
||||
this.shouldColliderScaleAndRotateWithTransform = shouldColliderScaleAndRotationWithTransform;
|
||||
this._isPositionDirty = this._isRotationDirty = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onAddedToEntity() {
|
||||
if (this._colliderRequiresAutoSizing) {
|
||||
let renderable = null;
|
||||
for (let i = 0; i < this.entity.components.buffer.length; i ++) {
|
||||
let component = this.entity.components.buffer[i];
|
||||
if (component instanceof RenderableComponent){
|
||||
renderable = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (renderable != null) {
|
||||
let renderableBounds = renderable.bounds.clone();
|
||||
|
||||
let width = renderableBounds.width / this.entity.transform.scale.x;
|
||||
let height = renderableBounds.height / this.entity.transform.scale.y;
|
||||
|
||||
if (this instanceof CircleCollider) {
|
||||
this.radius = Math.max(width, height) * 0.5;
|
||||
this.localOffset = renderableBounds.center.sub(this.entity.transform.position);
|
||||
} else if (this instanceof BoxCollider) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
this.localOffset = renderableBounds.center.sub(this.entity.transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._isParentEntityAddedToScene = true;
|
||||
this.registerColliderWithPhysicsSystem();
|
||||
}
|
||||
|
||||
public onRemovedFromEntity() {
|
||||
this.unregisterColliderWithPhysicsSystem();
|
||||
this._isParentEntityAddedToScene = false;
|
||||
}
|
||||
|
||||
public onEntityTransformChanged(comp: ComponentTransform) {
|
||||
switch (comp) {
|
||||
case ComponentTransform.position:
|
||||
this._isPositionDirty = true;
|
||||
break;
|
||||
case ComponentTransform.scale:
|
||||
this._isPositionDirty = true;
|
||||
break;
|
||||
case ComponentTransform.rotation:
|
||||
this._isRotationDirty = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this._isColliderRegistered)
|
||||
Physics.updateCollider(this);
|
||||
}
|
||||
|
||||
public onEnabled() {
|
||||
this.registerColliderWithPhysicsSystem();
|
||||
this._isPositionDirty = this._isRotationDirty = true;
|
||||
}
|
||||
|
||||
public onDisabled() {
|
||||
this.unregisterColliderWithPhysicsSystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* 父实体会在不同的时间调用它(当添加到场景,启用,等等)
|
||||
*/
|
||||
public registerColliderWithPhysicsSystem() {
|
||||
// 如果在将我们添加到实体之前更改了origin等属性,则实体可以为null
|
||||
if (this._isParentEntityAddedToScene && !this._isColliderRegistered) {
|
||||
Physics.addCollider(this);
|
||||
this._isColliderRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 父实体会在不同的时候调用它(从场景中移除,禁用,等等)
|
||||
*/
|
||||
public unregisterColliderWithPhysicsSystem() {
|
||||
if (this._isParentEntityAddedToScene && this._isColliderRegistered) {
|
||||
Physics.removeCollider(this);
|
||||
}
|
||||
this._isColliderRegistered = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查这个形状是否与物理系统中的其他对撞机重叠
|
||||
* @param other
|
||||
*/
|
||||
public overlaps(other: Collider): boolean {
|
||||
return this.shape.overlaps(other.shape);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查这个与运动应用的碰撞器(移动向量)是否与碰撞器碰撞。如果是这样,将返回true,并且结果将填充碰撞数据。
|
||||
* @param collider
|
||||
* @param motion
|
||||
* @param result
|
||||
*/
|
||||
public collidesWith(collider: Collider, motion: Vector2, result: CollisionResult = new CollisionResult()): boolean {
|
||||
// 改变形状的位置,使它在移动后的位置,这样我们可以检查重叠
|
||||
const oldPosition = this.entity.position;
|
||||
this.entity.position = this.entity.position.add(motion);
|
||||
|
||||
const didCollide = this.shape.collidesWithShape(collider.shape, result);
|
||||
if (didCollide)
|
||||
result.collider = collider;
|
||||
|
||||
// 将图形位置返回到检查前的位置
|
||||
this.entity.position = oldPosition;
|
||||
|
||||
return didCollide;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查这个对撞机是否与对撞机发生碰撞。如果碰撞,则返回true,结果将被填充
|
||||
* @param collider
|
||||
* @param result
|
||||
*/
|
||||
public collidesWithNonMotion(collider: Collider, result: CollisionResult = new CollisionResult()): boolean {
|
||||
if (this.shape.collidesWithShape(collider.shape, result)) {
|
||||
result.collider = collider;
|
||||
return true;
|
||||
}
|
||||
|
||||
result.collider = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查此碰撞器是否已应用运动(增量运动矢量)与任何碰撞器发生碰撞。
|
||||
* 如果是这样,则将返回true,并且将使用碰撞数据填充结果。 运动将设置为碰撞器在碰撞之前可以行进的最大距离。
|
||||
* @param motion
|
||||
* @param result
|
||||
*/
|
||||
public collidesWithAny(motion: Vector2, result: CollisionResult) {
|
||||
// 在我们的新位置上获取我们可能会碰到的任何东西
|
||||
let colliderBounds = this.bounds.clone();
|
||||
colliderBounds.x += motion.x;
|
||||
colliderBounds.y += motion.y;
|
||||
let neighbors = Physics.boxcastBroadphaseExcludingSelf(this, colliderBounds, this.collidesWithLayers.value);
|
||||
|
||||
// 更改形状位置,使其处于移动后的位置,以便我们检查是否有重叠
|
||||
let oldPosition = this.shape.position.clone();
|
||||
this.shape.position = Vector2.add(this.shape.position, motion);
|
||||
|
||||
let didCollide = false;
|
||||
for (let neighbor of neighbors) {
|
||||
if (neighbor.isTrigger)
|
||||
continue;
|
||||
|
||||
if (this.collidesWithNonMotion(neighbor, result)) {
|
||||
motion = motion.sub(result.minimumTranslationVector);
|
||||
this.shape.position = this.shape.position.sub(result.minimumTranslationVector);
|
||||
didCollide = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 将形状位置返回到检查之前的位置
|
||||
this.shape.position = oldPosition.clone();
|
||||
|
||||
return didCollide;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查此碰撞器是否与场景中的其他碰撞器碰撞。它相交的第一个碰撞器将在碰撞结果中返回碰撞数据。
|
||||
* @param result
|
||||
*/
|
||||
public collidesWithAnyNonMotion(result: CollisionResult = new CollisionResult()) {
|
||||
// 在我们的新位置上获取我们可能会碰到的任何东西
|
||||
let neighbors = Physics.boxcastBroadphaseExcludingSelfNonRect(this, this.collidesWithLayers.value);
|
||||
|
||||
for (let neighbor of neighbors) {
|
||||
if (neighbor.isTrigger)
|
||||
continue;
|
||||
|
||||
if (this.collidesWithNonMotion(neighbor, result))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 多边形应该以顺时针方式定义
|
||||
*/
|
||||
export class PolygonCollider extends Collider {
|
||||
/**
|
||||
* 如果这些点没有居中,它们将以localOffset的差异为居中。
|
||||
* @param points
|
||||
*/
|
||||
constructor(points: Vector2[]) {
|
||||
super();
|
||||
|
||||
// 第一点和最后一点决不能相同。我们想要一个开放的多边形
|
||||
let isPolygonClosed = points[0] == points[points.length - 1];
|
||||
|
||||
// 最后一个移除
|
||||
if (isPolygonClosed)
|
||||
points = points.slice(0, points.length - 1);
|
||||
|
||||
let center = Polygon.findPolygonCenter(points);
|
||||
this.setLocalOffset(center);
|
||||
Polygon.recenterPolygonVerts(points);
|
||||
this.shape = new Polygon(points);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 当添加到组件时,每当实体上的冲突器与另一个组件重叠/退出时,将调用这些方法。
|
||||
* ITriggerListener方法将在实现接口的触发器实体上的任何组件上调用。
|
||||
* 注意,这个接口只与Mover类一起工作
|
||||
*/
|
||||
export interface ITriggerListener {
|
||||
/**
|
||||
* 当碰撞器与触发碰撞器相交时调用。这是在触发碰撞器和触发碰撞器上调用的。
|
||||
* 移动必须由Mover/ProjectileMover方法处理,以使其自动工作。
|
||||
* @param other
|
||||
* @param local
|
||||
*/
|
||||
onTriggerEnter(other: Collider, local: Collider);
|
||||
|
||||
/**
|
||||
* 当另一个碰撞器离开触发碰撞器时调用
|
||||
* @param other
|
||||
* @param local
|
||||
*/
|
||||
onTriggerExit(other: Collider, local: Collider);
|
||||
}
|
||||
|
||||
export class TriggerListenerHelper {
|
||||
public static getITriggerListener(entity: Entity, components: ITriggerListener[]){
|
||||
for (let component of entity.components._components) {
|
||||
if (isITriggerListener(component)) {
|
||||
components.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i in entity.components._componentsToAdd) {
|
||||
let component = entity.components._componentsToAdd[i];
|
||||
if (isITriggerListener(component)) {
|
||||
components.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
}
|
||||
|
||||
export var isITriggerListener = (props: any): props is ITriggerListener => typeof (props as ITriggerListener)['onTriggerEnter'] !== 'undefined';
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 辅助类说明了一种处理移动的方法,它考虑了包括触发器在内的所有冲突。
|
||||
* ITriggerListener接口用于管理对移动过程中违反的任何触发器的回调。
|
||||
* 一个物体只能通过移动器移动。要正确报告触发器的move方法。
|
||||
*
|
||||
* 请注意,多个移动者相互交互将多次调用ITriggerListener。
|
||||
*/
|
||||
export class Mover extends Component {
|
||||
private _triggerHelper: ColliderTriggerHelper;
|
||||
|
||||
public onAddedToEntity() {
|
||||
this._triggerHelper = new ColliderTriggerHelper(this.entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算修改运动矢量的运动,以考虑移动时可能发生的碰撞
|
||||
* @param motion
|
||||
* @param collisionResult
|
||||
*/
|
||||
public calculateMovement(motion: Vector2, collisionResult: CollisionResult): boolean {
|
||||
let collider = null;
|
||||
for (let i = 0; i < this.entity.components.buffer.length; i++) {
|
||||
let component = this.entity.components.buffer[i];
|
||||
if (component instanceof Collider) {
|
||||
collider = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (collider == null || this._triggerHelper == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移动所有的非触发碰撞器并获得最近的碰撞
|
||||
let colliders: Collider[] = [];
|
||||
for (let i = 0; i < this.entity.components.buffer.length; i ++) {
|
||||
let component = this.entity.components.buffer[i];
|
||||
if (component instanceof Collider) {
|
||||
colliders.push(component);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < colliders.length; i++) {
|
||||
let collider = colliders[i];
|
||||
|
||||
// 不检测触发器 在我们移动后会重新访问它
|
||||
if (collider.isTrigger)
|
||||
continue;
|
||||
|
||||
// 获取我们在新位置可能发生碰撞的任何东西
|
||||
let bounds = collider.bounds.clone();
|
||||
bounds.x += motion.x;
|
||||
bounds.y += motion.y;
|
||||
let neighbors = Physics.boxcastBroadphaseExcludingSelf(collider, bounds, collider.collidesWithLayers.value);
|
||||
|
||||
for (let neighbor of neighbors) {
|
||||
// 不检测触发器
|
||||
if (neighbor.isTrigger)
|
||||
return;
|
||||
|
||||
let _internalcollisionResult: CollisionResult = new CollisionResult();
|
||||
if (collider.collidesWith(neighbor, motion, _internalcollisionResult)) {
|
||||
// 如果碰撞 则退回之前的移动量
|
||||
motion.sub(_internalcollisionResult.minimumTranslationVector);
|
||||
|
||||
// 如果我们碰到多个对象,为了简单起见,只取第一个。
|
||||
if (_internalcollisionResult.collider != null) {
|
||||
collisionResult.collider = _internalcollisionResult.collider;
|
||||
collisionResult.minimumTranslationVector = _internalcollisionResult.minimumTranslationVector;
|
||||
collisionResult.normal = _internalcollisionResult.normal;
|
||||
collisionResult.point = _internalcollisionResult.point;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListPool.free(colliders);
|
||||
|
||||
return collisionResult.collider != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将calculatemomovement应用到实体并更新triggerHelper
|
||||
* @param motion
|
||||
*/
|
||||
public applyMovement(motion: Vector2) {
|
||||
// 移动实体到它的新位置,如果我们有一个碰撞,否则移动全部数量。当碰撞发生时,运动被更新
|
||||
this.entity.position = Vector2.add(this.entity.position, motion);
|
||||
|
||||
// 对所有是触发器的碰撞器与所有宽相位碰撞器进行重叠检查。任何重叠都会导致触发事件。
|
||||
if (this._triggerHelper)
|
||||
this._triggerHelper.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过调用calculateMovement和applyMovement来移动考虑碰撞的实体;
|
||||
* @param motion
|
||||
* @param collisionResult
|
||||
*/
|
||||
public move(motion: Vector2, collisionResult: CollisionResult) {
|
||||
this.calculateMovement(motion, collisionResult);
|
||||
this.applyMovement(motion);
|
||||
return collisionResult.collider != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 移动时考虑到碰撞,只用于向任何ITriggerListeners报告。
|
||||
* 物体总是会全量移动,所以如果需要的话,由调用者在撞击时销毁它。
|
||||
*/
|
||||
export class ProjectileMover extends Component {
|
||||
private _tempTriggerList: ITriggerListener[] = [];
|
||||
private _collider: Collider;
|
||||
|
||||
public onAddedToEntity() {
|
||||
let collider = null;
|
||||
for (let i = 0; i < this.entity.components.buffer.length; i++) {
|
||||
let component = this.entity.components.buffer[i];
|
||||
if (component instanceof Collider) {
|
||||
collider = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._collider = collider;
|
||||
Debug.warnIf(this._collider == null, "ProjectileMover没有Collider。ProjectilMover需要一个Collider!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 在考虑到碰撞的情况下移动实体
|
||||
* @param motion
|
||||
*/
|
||||
public move(motion: Vector2): boolean {
|
||||
if (this._collider == null)
|
||||
return false;
|
||||
|
||||
let didCollide = false;
|
||||
|
||||
// 获取我们在新的位置上可能会碰撞到的任何东西
|
||||
this.entity.position = Vector2.add(this.entity.position, motion);
|
||||
|
||||
// 获取任何可能在新位置发生碰撞的东西
|
||||
let neighbors = Physics.boxcastBroadphase(this._collider.bounds, this._collider.collidesWithLayers.value);
|
||||
for (let neighbor of neighbors){
|
||||
if (this._collider.overlaps(neighbor) && neighbor.enabled){
|
||||
didCollide = true;
|
||||
this.notifyTriggerListeners(this._collider, neighbor);
|
||||
}
|
||||
}
|
||||
|
||||
return didCollide;
|
||||
}
|
||||
|
||||
private notifyTriggerListeners(self: Collider, other: Collider) {
|
||||
// 通知我们重叠的碰撞器实体上的任何侦听器
|
||||
TriggerListenerHelper.getITriggerListener(other.entity, this._tempTriggerList);
|
||||
for (let i = 0; i < this._tempTriggerList.length; i++) {
|
||||
this._tempTriggerList[i].onTriggerEnter(self, other);
|
||||
}
|
||||
this._tempTriggerList.length = 0;
|
||||
|
||||
// 通知此实体上的任何侦听器
|
||||
TriggerListenerHelper.getITriggerListener(this.entity, this._tempTriggerList);
|
||||
for (let i = 0; i < this._tempTriggerList.length; i++) {
|
||||
this._tempTriggerList[i].onTriggerEnter(other, self);
|
||||
}
|
||||
this._tempTriggerList.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
module es {
|
||||
export interface IRenderable {
|
||||
enabled: boolean;
|
||||
renderLayer: number;
|
||||
isVisibleFromCamera(camera: ICamera): boolean;
|
||||
render(batcher: IBatcher, camera: ICamera): void;
|
||||
debugRender(batcher: IBatcher): void;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
module es {
|
||||
export abstract class RenderableComponent extends es.Component implements IRenderable {
|
||||
public getwidth() {
|
||||
return this.bounds.width;
|
||||
}
|
||||
|
||||
public getheight() {
|
||||
return this.bounds.height;
|
||||
}
|
||||
|
||||
protected _bounds: es.Rectangle = new es.Rectangle();
|
||||
public getbounds(): es.Rectangle {
|
||||
if (this._areBoundsDirty) {
|
||||
this._bounds.calculateBounds(this.entity.transform.position, this._localOffset, new es.Vector2(this.getwidth() / 2, this.getheight() / 2),
|
||||
this.entity.transform.scale, this.entity.transform.rotation, this.getwidth(), this.getheight());
|
||||
this._areBoundsDirty = false;
|
||||
}
|
||||
return this._bounds;
|
||||
}
|
||||
public get bounds() {
|
||||
return this.getbounds();
|
||||
}
|
||||
protected _areBoundsDirty: boolean = true;
|
||||
public color: Color = Color.White;
|
||||
|
||||
public get renderLayer() {
|
||||
return this._renderLayer;
|
||||
}
|
||||
public set renderLayer(value: number) {
|
||||
this.setRenderLayer(value);
|
||||
}
|
||||
|
||||
protected _renderLayer: number = 0;
|
||||
|
||||
public onEntityTransformChanged(comp: ComponentTransform) {
|
||||
this._areBoundsDirty = true;
|
||||
}
|
||||
|
||||
public get localOffset() {
|
||||
return this._localOffset;
|
||||
}
|
||||
public set localOffset(value: es.Vector2) {
|
||||
this.setLocalOffset(value);
|
||||
}
|
||||
|
||||
public setLocalOffset(offset: es.Vector2) {
|
||||
if (!this._localOffset.equals(offset)) {
|
||||
this._localOffset = offset;
|
||||
this._areBoundsDirty = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public get isVisible() {
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
public set isVisible(value: boolean) {
|
||||
if (this._isVisible != value) {
|
||||
this._isVisible = value;
|
||||
|
||||
if (this._isVisible) {
|
||||
this.onBecameVisible();
|
||||
} else {
|
||||
this.onBecameInvisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public debugRenderEnabled: boolean = true;
|
||||
|
||||
protected _isVisible: boolean = false;
|
||||
protected _localOffset: es.Vector2 = new es.Vector2();
|
||||
|
||||
public abstract render(batcher: IBatcher, camera: ICamera): void;
|
||||
|
||||
protected onBecameVisible() {
|
||||
|
||||
}
|
||||
|
||||
protected onBecameInvisible() {
|
||||
|
||||
}
|
||||
|
||||
public setRenderLayer(renderLayer: number): RenderableComponent {
|
||||
if (renderLayer != this._renderLayer) {
|
||||
let oldRenderLayer = this._renderLayer;
|
||||
this._renderLayer = renderLayer;
|
||||
|
||||
if (this.entity != null && this.entity.scene != null)
|
||||
es.Core.scene.renderableComponents.updateRenderableRenderLayer(this, oldRenderLayer, this._renderLayer);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public isVisibleFromCamera(cam: ICamera): boolean {
|
||||
this.isVisible = cam.bounds.intersects(this.bounds);
|
||||
|
||||
return this.isVisible;
|
||||
}
|
||||
|
||||
public debugRender(batcher: IBatcher) {
|
||||
if (!this.debugRenderEnabled)
|
||||
return;
|
||||
|
||||
let collider = null;
|
||||
for (let i = 0; i < this.entity.components.buffer.length; i++) {
|
||||
let component = this.entity.components.buffer[i];
|
||||
if (component instanceof Collider) {
|
||||
collider = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (collider == null) {
|
||||
batcher.drawHollowRect(this.bounds.x, this.bounds.y, this.bounds.width, this.bounds.height, new Color(255, 255, 0));
|
||||
batcher.end();
|
||||
}
|
||||
|
||||
batcher.drawPixel(es.Vector2.add(this.entity.transform.position, this._localOffset), new Color(153, 50, 204), 4);
|
||||
batcher.end();
|
||||
}
|
||||
|
||||
public tweenColorTo(to: Color, duration: number) {
|
||||
const tween = Pool.obtain(RenderableColorTween);
|
||||
tween.setTarget(this);
|
||||
tween.initialize(tween, to, duration);
|
||||
return tween;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
module es {
|
||||
export class SceneComponent implements IComparer<SceneComponent> {
|
||||
/**
|
||||
* 这个场景组件被附加到的场景
|
||||
*/
|
||||
public scene: Scene;
|
||||
|
||||
/**
|
||||
* 如果启用了SceneComponent,则为true。状态的改变会导致调用onEnabled/onDisable。
|
||||
*/
|
||||
public get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果启用了SceneComponent,则为true。状态的改变会导致调用onEnabled/onDisable。
|
||||
* @param value
|
||||
*/
|
||||
public set enabled(value: boolean) {
|
||||
this.setEnabled(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新此场景中SceneComponents的顺序
|
||||
*/
|
||||
public updateOrder: number = 0;
|
||||
|
||||
public _enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 在启用此SceneComponent时调用
|
||||
*/
|
||||
public onEnabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当禁用此SceneComponent时调用
|
||||
*/
|
||||
public onDisabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当该SceneComponent从场景中移除时调用
|
||||
*/
|
||||
public onRemovedFromScene() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在实体更新之前每一帧调用
|
||||
*/
|
||||
public update() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用这个SceneComponent
|
||||
* @param isEnabled
|
||||
*/
|
||||
public setEnabled(isEnabled: boolean): SceneComponent {
|
||||
if (this._enabled != isEnabled) {
|
||||
this._enabled = isEnabled;
|
||||
|
||||
if (this._enabled) {
|
||||
this.onEnabled();
|
||||
} else {
|
||||
this.onDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置SceneComponent的updateOrder并触发某种SceneComponent
|
||||
* @param updateOrder
|
||||
*/
|
||||
public setUpdateOrder(updateOrder: number) {
|
||||
if (this.updateOrder != updateOrder) {
|
||||
this.updateOrder = updateOrder;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public compare(other: SceneComponent): number {
|
||||
return this.updateOrder - other.updateOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
module es {
|
||||
export enum CoreEvents {
|
||||
/**
|
||||
* 当场景发生变化时触发
|
||||
*/
|
||||
sceneChanged,
|
||||
/**
|
||||
* 每帧更新事件
|
||||
*/
|
||||
frameUpdated,
|
||||
/**
|
||||
* 当渲染发生时触发
|
||||
*/
|
||||
renderChanged,
|
||||
}
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
module es {
|
||||
export class EntityComparer implements IComparer<Entity> {
|
||||
public compare(self: Entity, other: Entity): number {
|
||||
let compare = self.updateOrder - other.updateOrder;
|
||||
if (compare == 0)
|
||||
compare = self.id - other.id;
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
|
||||
export class Entity implements IEqualityComparable {
|
||||
public static entityComparer: IComparer<Entity> = new EntityComparer();
|
||||
/**
|
||||
* 当前实体所属的场景
|
||||
*/
|
||||
public scene: Scene;
|
||||
/**
|
||||
* 实体名称。用于在场景范围内搜索实体
|
||||
*/
|
||||
public name: string;
|
||||
/**
|
||||
* 此实体的唯一标识
|
||||
*/
|
||||
public readonly id: number;
|
||||
/**
|
||||
* 封装实体的位置/旋转/缩放,并允许设置一个高层结构
|
||||
*/
|
||||
public readonly transform: Transform;
|
||||
/**
|
||||
* 当前附加到此实体的所有组件的列表
|
||||
*/
|
||||
public readonly components: ComponentList;
|
||||
/**
|
||||
* 指定应该调用这个entity update方法的频率。1表示每一帧,2表示每一帧,以此类推
|
||||
*/
|
||||
public updateInterval: number = 1;
|
||||
public componentBits: Bits;
|
||||
|
||||
constructor(name: string, id: number) {
|
||||
this.components = new ComponentList(this);
|
||||
this.transform = new Transform(this);
|
||||
this.componentBits = new Bits();
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public _isDestroyed: boolean;
|
||||
|
||||
/**
|
||||
* 如果调用了destroy,那么在下一次处理实体之前这将一直为true
|
||||
*/
|
||||
public get isDestroyed() {
|
||||
return this._isDestroyed;
|
||||
}
|
||||
|
||||
private _tag: number = 0;
|
||||
|
||||
/**
|
||||
* 你可以随意使用。稍后可以使用它来查询场景中具有特定标记的所有实体
|
||||
*/
|
||||
public get tag(): number {
|
||||
return this._tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 你可以随意使用。稍后可以使用它来查询场景中具有特定标记的所有实体
|
||||
* @param value
|
||||
*/
|
||||
public set tag(value: number) {
|
||||
this.setTag(value);
|
||||
}
|
||||
|
||||
private _enabled: boolean = true;
|
||||
|
||||
/**
|
||||
* 启用/禁用实体。当禁用碰撞器从物理系统和组件中移除时,方法将不会被调用
|
||||
*/
|
||||
public get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用实体。当禁用碰撞器从物理系统和组件中移除时,方法将不会被调用
|
||||
* @param value
|
||||
*/
|
||||
public set enabled(value: boolean) {
|
||||
this.setEnabled(value);
|
||||
}
|
||||
|
||||
private _updateOrder: number = 0;
|
||||
|
||||
/**
|
||||
* 更新此实体的顺序。updateOrder还用于对scene.entities上的标签列表进行排序
|
||||
*/
|
||||
public get updateOrder() {
|
||||
return this._updateOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新此实体的顺序。updateOrder还用于对scene.entities上的标签列表进行排序
|
||||
* @param value
|
||||
*/
|
||||
public set updateOrder(value: number) {
|
||||
this.setUpdateOrder(value);
|
||||
}
|
||||
|
||||
public get parent(): Transform {
|
||||
return this.transform.parent;
|
||||
}
|
||||
|
||||
public set parent(value: Transform) {
|
||||
this.transform.setParent(value);
|
||||
}
|
||||
|
||||
public get childCount() {
|
||||
return this.transform.childCount;
|
||||
}
|
||||
|
||||
public get position(): Vector2 {
|
||||
return this.transform.position;
|
||||
}
|
||||
|
||||
public set position(value: Vector2) {
|
||||
this.transform.setPosition(value.x, value.y);
|
||||
}
|
||||
|
||||
public get localPosition(): Vector2 {
|
||||
return this.transform.localPosition;
|
||||
}
|
||||
|
||||
public set localPosition(value: Vector2) {
|
||||
this.transform.setLocalPosition(value);
|
||||
}
|
||||
|
||||
public get rotation(): number {
|
||||
return this.transform.rotation;
|
||||
}
|
||||
|
||||
public set rotation(value: number) {
|
||||
this.transform.setRotation(value);
|
||||
}
|
||||
|
||||
public get rotationDegrees(): number {
|
||||
return this.transform.rotationDegrees;
|
||||
}
|
||||
|
||||
public set rotationDegrees(value: number) {
|
||||
this.transform.setRotationDegrees(value);
|
||||
}
|
||||
|
||||
public get localRotation(): number {
|
||||
return this.transform.localRotation;
|
||||
}
|
||||
|
||||
public set localRotation(value: number) {
|
||||
this.transform.setLocalRotation(value);
|
||||
}
|
||||
|
||||
public get localRotationDegrees(): number {
|
||||
return this.transform.localRotationDegrees;
|
||||
}
|
||||
|
||||
public set localRotationDegrees(value: number) {
|
||||
this.transform.setLocalRotationDegrees(value);
|
||||
}
|
||||
|
||||
public get scale(): Vector2 {
|
||||
return this.transform.scale;
|
||||
}
|
||||
|
||||
public set scale(value: Vector2) {
|
||||
this.transform.setScale(value);
|
||||
}
|
||||
|
||||
public get localScale(): Vector2 {
|
||||
return this.transform.localScale;
|
||||
}
|
||||
|
||||
public set localScale(value: Vector2) {
|
||||
this.transform.setLocalScale(value);
|
||||
}
|
||||
|
||||
public get worldInverseTransform(): Matrix2D {
|
||||
return this.transform.worldInverseTransform;
|
||||
}
|
||||
|
||||
public get localToWorldTransform(): Matrix2D {
|
||||
return this.transform.localToWorldTransform;
|
||||
}
|
||||
|
||||
public get worldToLocalTransform(): Matrix2D {
|
||||
return this.transform.worldToLocalTransform;
|
||||
}
|
||||
|
||||
public onTransformChanged(comp: ComponentTransform) {
|
||||
// 通知我们的子项改变了位置
|
||||
this.components.onEntityTransformChanged(comp);
|
||||
}
|
||||
|
||||
public setParent(parent: Entity);
|
||||
public setParent(parent: Transform);
|
||||
public setParent(parent: Transform | Entity) {
|
||||
if (parent instanceof Transform) {
|
||||
this.transform.setParent(parent);
|
||||
} else if (parent instanceof Entity) {
|
||||
this.transform.setParent(parent.transform);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setPosition(x: number, y: number) {
|
||||
this.transform.setPosition(x, y);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setLocalPosition(localPosition: Vector2) {
|
||||
this.transform.setLocalPosition(localPosition);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setRotation(radians: number) {
|
||||
this.transform.setRotation(radians);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setRotationDegrees(degrees: number) {
|
||||
this.transform.setRotationDegrees(degrees);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setLocalRotation(radians: number) {
|
||||
this.transform.setLocalRotation(radians);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setLocalRotationDegrees(degrees: number) {
|
||||
this.transform.setLocalRotationDegrees(degrees);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setScale(scale: number);
|
||||
public setScale(scale: Vector2);
|
||||
public setScale(scale: Vector2 | number) {
|
||||
if (scale instanceof Vector2) {
|
||||
this.transform.setScale(scale);
|
||||
} else {
|
||||
this.transform.setScale(new Vector2(scale, scale));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setLocalScale(scale: number);
|
||||
public setLocalScale(scale: Vector2);
|
||||
public setLocalScale(scale: Vector2 | number) {
|
||||
if (scale instanceof Vector2) {
|
||||
this.transform.setLocalScale(scale);
|
||||
} else {
|
||||
this.transform.setLocalScale(new Vector2(scale, scale));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实体的标记
|
||||
* @param tag
|
||||
*/
|
||||
public setTag(tag: number): Entity {
|
||||
if (this._tag != tag) {
|
||||
// 我们只有在已经有场景的情况下才会调用entityTagList。如果我们还没有场景,我们会被添加到entityTagList
|
||||
if (this.scene)
|
||||
this.scene.entities.removeFromTagList(this);
|
||||
this._tag = tag;
|
||||
if (this.scene)
|
||||
this.scene.entities.addToTagList(this);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置实体的启用状态。当禁用碰撞器从物理系统和组件中移除时,方法将不会被调用
|
||||
* @param isEnabled
|
||||
*/
|
||||
public setEnabled(isEnabled: boolean) {
|
||||
if (this._enabled != isEnabled) {
|
||||
this._enabled = isEnabled;
|
||||
|
||||
if (this._enabled)
|
||||
this.components.onEntityEnabled();
|
||||
else
|
||||
this.components.onEntityDisabled();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置此实体的更新顺序。updateOrder还用于对scene.entities上的标签列表进行排序
|
||||
* @param updateOrder
|
||||
*/
|
||||
public setUpdateOrder(updateOrder: number) {
|
||||
if (this._updateOrder != updateOrder) {
|
||||
this._updateOrder = updateOrder;
|
||||
if (this.scene) {
|
||||
this.scene.entities.markEntityListUnsorted();
|
||||
this.scene.entities.markTagUnsorted(this.tag);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景中删除实体并销毁所有子元素
|
||||
*/
|
||||
public destroy() {
|
||||
this._isDestroyed = true;
|
||||
this.scene.identifierPool.checkIn(this.id);
|
||||
this.scene.entities.remove(this);
|
||||
this.transform.parent = null;
|
||||
|
||||
// 销毁所有子项
|
||||
for (let i = this.transform.childCount - 1; i >= 0; i--) {
|
||||
let child = this.transform.getChild(i);
|
||||
child.entity.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体从场景中分离。下面的生命周期方法将被调用在组件上:OnRemovedFromEntity
|
||||
*/
|
||||
public detachFromScene() {
|
||||
this.scene.entities.remove(this);
|
||||
this.components.deregisterAllComponents();
|
||||
|
||||
for (let i = 0; i < this.transform.childCount; i++)
|
||||
this.transform.getChild(i).entity.detachFromScene();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一个先前分离的实体附加到一个新的场景
|
||||
* @param newScene
|
||||
*/
|
||||
public attachToScene(newScene: Scene) {
|
||||
this.scene = newScene;
|
||||
newScene.entities.add(this);
|
||||
this.components.registerAllComponents();
|
||||
|
||||
for (let i = 0; i < this.transform.childCount; i++) {
|
||||
this.transform.getChild(i).entity.attachToScene(newScene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在提交了所有挂起的实体更改后,将此实体添加到场景时调用
|
||||
*/
|
||||
public onAddedToScene() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当此实体从场景中删除时调用
|
||||
*/
|
||||
public onRemovedFromScene() {
|
||||
// 如果已经被销毁了,移走我们的组件。如果我们只是分离,我们需要保持我们的组件在实体上。
|
||||
if (this._isDestroyed)
|
||||
this.components.removeAllComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧进行调用进行更新组件
|
||||
*/
|
||||
public update() {
|
||||
this.components.update();
|
||||
}
|
||||
|
||||
public debugRender(batcher: IBatcher) {
|
||||
if (!batcher) return;
|
||||
this.components.debugRender(batcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组件的新实例。返回实例组件
|
||||
* @param componentType
|
||||
*/
|
||||
public createComponent<T extends Component>(componentType: new (...args) => T): T {
|
||||
let component = new componentType();
|
||||
this.addComponent(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将组件添加到组件列表中。返回组件。
|
||||
* @param component
|
||||
*/
|
||||
public addComponent<T extends Component>(component: T): T {
|
||||
component.entity = this;
|
||||
this.components.add(component);
|
||||
component.initialize();
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型T的第一个组件并返回它。如果没有找到组件,则返回null。
|
||||
* @param type
|
||||
*/
|
||||
public getComponent<T extends Component>(type: new (...args) => T): T {
|
||||
return this.components.getComponent(type, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型T的第一个并已加入场景的组件并返回它。如果没有找到组件,则返回null。
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
public getComponentInScene<T extends Component>(type: new (...args) => T): T {
|
||||
return this.components.getComponent(type, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取T类型的组件。如果未找到任何组件,则返回false
|
||||
* @param type
|
||||
* @param outComponent
|
||||
* @returns
|
||||
*/
|
||||
public tryGetComponent<T extends Component>(type: new (...args) => T, outComponent: Ref<T>): boolean {
|
||||
outComponent.value = this.components.getComponent<T>(type, false);
|
||||
return outComponent.value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否具有该组件
|
||||
* @param type
|
||||
*/
|
||||
public hasComponent<T extends Component>(type: new (...args) => T) {
|
||||
return this.components.getComponent<T>(type, false) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型T的第一个组件并返回它。如果没有找到组件,将创建组件。
|
||||
* @param type
|
||||
*/
|
||||
public getOrCreateComponent<T extends Component>(type: new (...args) => T) {
|
||||
let comp = this.components.getComponent<T>(type, true);
|
||||
if (!comp) {
|
||||
comp = this.addComponent<T>(new type());
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取typeName类型的所有组件,但不使用列表分配
|
||||
* @param typeName
|
||||
* @param componentList
|
||||
*/
|
||||
public getComponents(typeName: any, componentList?) {
|
||||
return this.components.getComponents(typeName, componentList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从组件列表中删除组件
|
||||
* @param component
|
||||
*/
|
||||
public removeComponent(component: Component) {
|
||||
this.components.remove(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从组件列表中删除类型为T的第一个组件
|
||||
* @param type
|
||||
*/
|
||||
public removeComponentForType<T extends Component>(type) {
|
||||
let comp = this.getComponent<T>(type);
|
||||
if (comp) {
|
||||
this.removeComponent(comp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从实体中删除所有组件
|
||||
*/
|
||||
public removeAllComponents() {
|
||||
for (let i = 0; i < this.components.count; i++) {
|
||||
this.removeComponent(this.components.buffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public tweenPositionTo(to: Vector2, duration: number = 0.3): ITween<Vector2> {
|
||||
const tween = Pool.obtain(TransformVector2Tween);
|
||||
tween.setTargetAndType(this.transform, TransformTargetType.position);
|
||||
tween.initialize(tween, to, duration);
|
||||
|
||||
return tween;
|
||||
}
|
||||
|
||||
public tweenLocalPositionTo(to: Vector2, duration = 0.3): ITween<Vector2> {
|
||||
const tween = Pool.obtain(TransformVector2Tween);
|
||||
tween.setTargetAndType(this.transform, TransformTargetType.localPosition);
|
||||
tween.initialize(tween, to, duration);
|
||||
|
||||
return tween;
|
||||
}
|
||||
|
||||
public tweenScaleTo(to: Vector2, duration?: number);
|
||||
public tweenScaleTo(to: number, duration?: number);
|
||||
public tweenScaleTo(to: Vector2 | number, duration: number = 0.3) {
|
||||
if (typeof (to) == 'number') {
|
||||
return this.tweenScaleTo(new Vector2(to, to), duration);
|
||||
}
|
||||
|
||||
const tween = Pool.obtain(TransformVector2Tween);
|
||||
tween.setTargetAndType(this.transform, TransformTargetType.scale);
|
||||
tween.initialize(tween, to, duration);
|
||||
|
||||
return tween;
|
||||
}
|
||||
|
||||
public tweenLocalScaleTo(to: Vector2, duration?);
|
||||
public tweenLocalScaleTo(to: number, duration?);
|
||||
public tweenLocalScaleTo(to: Vector2 | number, duration = 0.3) {
|
||||
if (typeof (to) == 'number') {
|
||||
return this.tweenLocalScaleTo(new Vector2(to, to), duration);
|
||||
}
|
||||
|
||||
const tween = Pool.obtain(TransformVector2Tween);
|
||||
tween.setTargetAndType(this.transform, TransformTargetType.localScale);
|
||||
tween.initialize(tween, to, duration);
|
||||
|
||||
return tween;
|
||||
}
|
||||
|
||||
public tweenRotationDegreesTo(to: number, duration = 0.3) {
|
||||
const tween = Pool.obtain(TransformVector2Tween);
|
||||
tween.setTargetAndType(this.transform, TransformTargetType.rotationDegrees);
|
||||
tween.initialize(tween, new Vector2(to, to), duration);
|
||||
|
||||
return tween;
|
||||
}
|
||||
|
||||
public tweenLocalRotationDegreesTo(to: number, duration = 0.3) {
|
||||
const tween = Pool.obtain(TransformVector2Tween);
|
||||
tween.setTargetAndType(this.transform, TransformTargetType.localRotationDegrees);
|
||||
tween.initialize(tween, new Vector2(to, to), duration);
|
||||
|
||||
return tween;
|
||||
}
|
||||
|
||||
public compareTo(other: Entity): number {
|
||||
let compare = this._updateOrder - other._updateOrder;
|
||||
if (compare == 0)
|
||||
compare = this.id - other.id;
|
||||
return compare;
|
||||
}
|
||||
|
||||
public equals(other: Entity): boolean {
|
||||
return this.compareTo(other) == 0;
|
||||
}
|
||||
|
||||
public getHashCode(): number {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `[Entity: name: ${this.name}, tag: ${this.tag}, enabled: ${this.enabled}, depth: ${this.updateOrder}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
///<reference path="../Math/Vector2.ts" />
|
||||
module es {
|
||||
/** 场景 */
|
||||
export class Scene {
|
||||
public camera: ICamera;
|
||||
/** 这个场景中的实体列表 */
|
||||
public readonly entities: EntityList;
|
||||
public readonly renderableComponents: RenderableComponentList;
|
||||
/** 管理所有实体处理器 */
|
||||
public readonly entityProcessors: EntityProcessorList;
|
||||
|
||||
public readonly _sceneComponents: SceneComponent[] = [];
|
||||
public _renderers: Renderer[] = [];
|
||||
public readonly identifierPool: IdentifierPool;
|
||||
private _didSceneBegin: boolean;
|
||||
|
||||
constructor() {
|
||||
this.entities = new EntityList(this);
|
||||
this.renderableComponents = new RenderableComponentList();
|
||||
|
||||
this.entityProcessors = new EntityProcessorList();
|
||||
this.identifierPool = new IdentifierPool();
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场景子类中重写这个,然后在这里进行加载。
|
||||
* 在场景设置好之后,但在调用begin之前,从contructor中调用这个函数
|
||||
*/
|
||||
public initialize() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当Core将这个场景设置为活动场景时,这个将被调用
|
||||
*/
|
||||
public onStart() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场景子类中重写这个,并在这里做任何必要的卸载。
|
||||
* 当Core把这个场景从活动槽中移除时,这个被调用。
|
||||
*/
|
||||
public unload() {
|
||||
}
|
||||
|
||||
public begin() {
|
||||
if (this._renderers.length == 0) {
|
||||
this.addRenderer(new DefaultRenderer());
|
||||
}
|
||||
|
||||
Physics.reset();
|
||||
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.begin();
|
||||
|
||||
this._didSceneBegin = true;
|
||||
this.onStart();
|
||||
|
||||
}
|
||||
|
||||
public end() {
|
||||
this._didSceneBegin = false;
|
||||
|
||||
for (let i = 0; i < this._renderers.length; i ++)
|
||||
this._renderers[i].unload();
|
||||
|
||||
this.entities.removeAllEntities();
|
||||
|
||||
for (let i = 0; i < this._sceneComponents.length; i++) {
|
||||
this._sceneComponents[i].onRemovedFromScene();
|
||||
}
|
||||
this._sceneComponents.length = 0;
|
||||
|
||||
this.camera = null;
|
||||
Physics.clear();
|
||||
|
||||
if (this.entityProcessors)
|
||||
this.entityProcessors.end();
|
||||
|
||||
this.unload();
|
||||
}
|
||||
|
||||
public update() {
|
||||
// 更新我们的列表,以防它们有任何变化
|
||||
this.entities.updateLists();
|
||||
|
||||
for (let i = this._sceneComponents.length - 1; i >= 0; i--) {
|
||||
if (this._sceneComponents[i].enabled)
|
||||
this._sceneComponents[i].update();
|
||||
}
|
||||
|
||||
// 更新我们的实体解析器
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.update();
|
||||
|
||||
// 更新我们的实体组
|
||||
this.entities.update();
|
||||
|
||||
if (this.entityProcessors != null)
|
||||
this.entityProcessors.lateUpdate();
|
||||
|
||||
this.renderableComponents.updateLists();
|
||||
this.render();
|
||||
}
|
||||
|
||||
public render() {
|
||||
for (let i = 0; i < this._renderers.length; i ++) {
|
||||
this._renderers[i].render(this);
|
||||
}
|
||||
}
|
||||
|
||||
public addRenderer<T extends Renderer>(renderer: T): T {
|
||||
this._renderers.push(renderer);
|
||||
this._renderers.sort((self, other) => self.renderOrder - other.renderOrder);
|
||||
|
||||
renderer.onAddedToScene(this);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public getRenderer<T extends Renderer>(type: new (...args: any[]) => T): T {
|
||||
for (let i = 0; i < this._renderers.length; i ++) {
|
||||
if (this._renderers[i] instanceof type)
|
||||
return this._renderers[i] as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public removeRenderer(renderer: Renderer) {
|
||||
new List(this._renderers).remove(renderer);
|
||||
renderer.unload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向组件列表添加并返回SceneComponent
|
||||
* @param component
|
||||
*/
|
||||
public addSceneComponent<T extends SceneComponent>(component: T): T {
|
||||
component.scene = this;
|
||||
component.onEnabled();
|
||||
this._sceneComponents.push(component);
|
||||
this._sceneComponents.sort(component.compare);
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型为T的第一个SceneComponent并返回它。如果没有找到组件,则返回null。
|
||||
* @param type
|
||||
*/
|
||||
public getSceneComponent<T extends SceneComponent>(type) {
|
||||
for (let i = 0; i < this._sceneComponents.length; i++) {
|
||||
let component = this._sceneComponents[i];
|
||||
if (component instanceof type)
|
||||
return component as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型为T的第一个SceneComponent并返回它。如果没有找到SceneComponent,则将创建SceneComponent。
|
||||
* @param type
|
||||
*/
|
||||
public getOrCreateSceneComponent<T extends SceneComponent>(type) {
|
||||
let comp = this.getSceneComponent<T>(type);
|
||||
if (comp == null)
|
||||
comp = this.addSceneComponent<T>(new type());
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SceneComponents列表中删除一个SceneComponent
|
||||
* @param component
|
||||
*/
|
||||
public removeSceneComponent(component: SceneComponent) {
|
||||
const sceneComponentList = new es.List(this._sceneComponents);
|
||||
Insist.isTrue(sceneComponentList.contains(component), `SceneComponent${component}不在SceneComponents列表中!`);
|
||||
sceneComponentList.remove(component);
|
||||
component.onRemovedFromScene();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体添加到此场景,并返回它
|
||||
* @param name
|
||||
*/
|
||||
public createEntity(name: string) {
|
||||
let entity = new Entity(name, this.identifierPool.checkOut());
|
||||
return this.addEntity(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场景的实体列表中添加一个实体
|
||||
* @param entity
|
||||
*/
|
||||
public addEntity(entity: Entity) {
|
||||
Insist.isFalse(new es.List(this.entities.buffer).contains(entity), `您试图将同一实体添加到场景两次: ${entity}`);
|
||||
this.entities.add(entity);
|
||||
entity.scene = this;
|
||||
|
||||
for (let i = 0; i < entity.transform.childCount; i++)
|
||||
this.addEntity(entity.transform.getChild(i).entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景中删除所有实体
|
||||
*/
|
||||
public destroyAllEntities() {
|
||||
for (let i = 0; i < this.entities.count; i++) {
|
||||
this.entities.buffer[i].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索并返回第一个具有名称的实体
|
||||
* @param name
|
||||
*/
|
||||
public findEntity(name: string): Entity {
|
||||
return this.entities.findEntity(name);
|
||||
}
|
||||
|
||||
public findEntityById(id: number): Entity {
|
||||
return this.entities.findEntityById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回具有给定标记的所有实体
|
||||
* @param tag
|
||||
*/
|
||||
public findEntitiesWithTag(tag: number): Entity[] {
|
||||
return this.entities.entitiesWithTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回提一个具有该标记的实体
|
||||
* @param tag
|
||||
* @returns
|
||||
*/
|
||||
public findEntityWithTag(tag: number): Entity {
|
||||
return this.entities.entityWithTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个启用加载的类型为T的组件
|
||||
* @param type
|
||||
*/
|
||||
public findComponentOfType<T extends Component>(type: new (...args) => T): T {
|
||||
return this.entities.findComponentOfType<T>(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回类型为T的所有已启用已加载组件的列表
|
||||
* @param type
|
||||
*/
|
||||
public findComponentsOfType<T extends Component>(type: new (...args) => T): T[] {
|
||||
return this.entities.findComponentsOfType<T>(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回场景中包含特定组件的实体列表
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
public findEntitiesOfComponent(...types): Entity[] {
|
||||
return this.entities.findEntitesOfComponent(...types);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在场景中添加一个EntitySystem处理器
|
||||
* @param processor 处理器
|
||||
*/
|
||||
public addEntityProcessor(processor: EntitySystem) {
|
||||
processor.scene = this;
|
||||
this.entityProcessors.add(processor);
|
||||
return processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景中删除EntitySystem处理器
|
||||
* @param processor
|
||||
*/
|
||||
public removeEntityProcessor(processor: EntitySystem) {
|
||||
this.entityProcessors.remove(processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取EntitySystem处理器
|
||||
*/
|
||||
public getEntityProcessor<T extends EntitySystem>(type: new (...args: any[]) => T): T {
|
||||
return this.entityProcessors.getProcessor<T>(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
///<reference path="./EntitySystem.ts"/>
|
||||
module es {
|
||||
/**
|
||||
* 追踪每个实体的冷却时间,当实体的计时器耗尽时进行处理
|
||||
*
|
||||
* 一个示例系统将是ExpirationSystem,该系统将在特定生存期后删除实体。
|
||||
* 你不必运行会为每个实体递减timeLeft值的系统
|
||||
* 而只需使用此系统在寿命最短的实体时在将来执行
|
||||
* 然后重置系统在未来的某一个最短命实体的时间运行
|
||||
*
|
||||
* 另一个例子是一个动画系统
|
||||
* 你知道什么时候你必须对某个实体进行动画制作,比如300毫秒内。
|
||||
* 所以你可以设置系统以300毫秒为单位运行来执行动画
|
||||
*
|
||||
* 这将在某些情况下节省CPU周期
|
||||
*/
|
||||
export abstract class DelayedIteratingSystem extends EntitySystem {
|
||||
/**
|
||||
* 一个实体应被处理的时间
|
||||
*/
|
||||
private delay: number = 0;
|
||||
/**
|
||||
* 如果系统正在运行,并倒计时延迟
|
||||
*/
|
||||
private running: boolean = false;
|
||||
/**
|
||||
* 倒计时
|
||||
*/
|
||||
private acc: number = 0;
|
||||
|
||||
constructor(matcher: Matcher) {
|
||||
super(matcher);
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
let processed = entities.length;
|
||||
if (processed == 0) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.delay = Number.MAX_VALUE;
|
||||
for (let i = 0; processed > i; i++) {
|
||||
let e = entities[i];
|
||||
this.processDelta(e, this.acc);
|
||||
let remaining = this.getRemainingDelay(e);
|
||||
if (remaining <= 0) {
|
||||
this.processExpired(e);
|
||||
} else {
|
||||
this.offerDelay(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
this.acc = 0;
|
||||
}
|
||||
|
||||
protected checkProcessing() {
|
||||
if (this.running) {
|
||||
this.acc += Time.deltaTime;
|
||||
return this.acc >= this.delay;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只有当提供的延迟比系统当前计划执行的时间短时,才会重新启动系统。
|
||||
* 如果系统已经停止(不运行),那么提供的延迟将被用来重新启动系统,无论其值如何
|
||||
* 如果系统已经在倒计时,并且提供的延迟大于剩余时间,系统将忽略它。
|
||||
* 如果提供的延迟时间短于剩余时间,系统将重新启动,以提供的延迟时间运行。
|
||||
* @param offeredDelay
|
||||
*/
|
||||
public offerDelay(offeredDelay: number) {
|
||||
if (!this.running) {
|
||||
this.running = true;
|
||||
this.delay = offeredDelay;
|
||||
} else {
|
||||
this.delay = Math.min(this.delay, offeredDelay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理本系统感兴趣的实体
|
||||
* 从实体定义的延迟中抽象出accumulativeDelta
|
||||
* @param entity
|
||||
* @param accumulatedDelta 本系统最后一次执行后的delta时间
|
||||
*/
|
||||
protected abstract processDelta(entity: Entity, accumulatedDelta: number);
|
||||
|
||||
protected abstract processExpired(entity: Entity);
|
||||
|
||||
/**
|
||||
* 返回该实体处理前的延迟时间
|
||||
* @param entity
|
||||
*/
|
||||
protected abstract getRemainingDelay(entity: Entity): number;
|
||||
|
||||
/**
|
||||
* 获取系统被命令处理实体后的初始延迟
|
||||
*/
|
||||
public getInitialTimeDelay() {
|
||||
return this.delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统计划运行前的时间
|
||||
* 如果系统没有运行,则返回零
|
||||
*/
|
||||
public getRemainingTimeUntilProcessing(): number {
|
||||
if (this.running) {
|
||||
return this.delay - this.acc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查系统是否正在倒计时处理
|
||||
*/
|
||||
public isRunning(): boolean {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止系统运行,中止当前倒计时
|
||||
*/
|
||||
public stop() {
|
||||
this.running = false;
|
||||
this.acc = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
///<reference path="./EntitySystem.ts" />
|
||||
module es {
|
||||
/**
|
||||
* 基本实体处理系统。将其用作处理具有特定组件的许多实体的基础
|
||||
*
|
||||
* 按实体引用遍历实体订阅成员实体的系统
|
||||
* 当你需要处理与Matcher相匹配的实体,并且你更喜欢使用Entity的时候,可以使用这个功能。
|
||||
*/
|
||||
export abstract class EntityProcessingSystem extends EntitySystem {
|
||||
constructor(matcher: Matcher) {
|
||||
super(matcher);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理特定的实体
|
||||
* @param entity
|
||||
*/
|
||||
public abstract processEntity(entity: Entity);
|
||||
|
||||
public lateProcessEntity(entity: Entity) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历这个系统的所有实体并逐个处理它们
|
||||
* @param entities
|
||||
*/
|
||||
protected process(entities: Entity[]) {
|
||||
if (entities.length == 0)
|
||||
return;
|
||||
|
||||
for (let i = 0, s = entities.length; i < s; ++ i) {
|
||||
let entity = entities[i];
|
||||
this.processEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: Entity[]) {
|
||||
if (entities.length == 0)
|
||||
return;
|
||||
|
||||
for (let i = 0, s = entities.length; i < s; ++ i) {
|
||||
let entity = entities[i];
|
||||
this.lateProcessEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
///<reference path="../../Utils/Collections/HashMap.ts"/>
|
||||
module es {
|
||||
/**
|
||||
* 追踪实体的子集,但不实现任何排序或迭代。
|
||||
*/
|
||||
export abstract class EntitySystem {
|
||||
private _entities: Entity[] = [];
|
||||
|
||||
constructor(matcher?: Matcher) {
|
||||
this._matcher = matcher ? matcher : Matcher.empty();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private _scene: Scene;
|
||||
|
||||
/**
|
||||
* 这个系统所属的场景
|
||||
*/
|
||||
public get scene() {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
public set scene(value: Scene) {
|
||||
this._scene = value;
|
||||
this._entities = [];
|
||||
}
|
||||
|
||||
private _matcher: Matcher;
|
||||
|
||||
public get matcher() {
|
||||
return this._matcher;
|
||||
}
|
||||
|
||||
private _startTime = 0;
|
||||
private _endTime = 0;
|
||||
private _useTime = 0;
|
||||
/** 获取系统在当前帧所消耗的时间 仅在debug模式下生效 */
|
||||
public get useTime() {
|
||||
return this._useTime;
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
|
||||
}
|
||||
|
||||
public onChanged(entity: Entity) {
|
||||
let contains = !!this._entities.find(e => e.id == entity.id);
|
||||
let interest = this._matcher.isInterestedEntity(entity);
|
||||
|
||||
if (interest && !contains)
|
||||
this.add(entity);
|
||||
else if (!interest && contains)
|
||||
this.remove(entity);
|
||||
}
|
||||
|
||||
public add(entity: Entity) {
|
||||
if (!this._entities.find(e => e.id == entity.id))
|
||||
this._entities.push(entity);
|
||||
this.onAdded(entity);
|
||||
}
|
||||
|
||||
public onAdded(entity: Entity) { }
|
||||
|
||||
public remove(entity: Entity) {
|
||||
new es.List(this._entities).remove(entity);
|
||||
this.onRemoved(entity);
|
||||
}
|
||||
|
||||
public onRemoved(entity: Entity) { }
|
||||
|
||||
public update() {
|
||||
if (this.checkProcessing()) {
|
||||
this.begin();
|
||||
this.process(this._entities);
|
||||
}
|
||||
}
|
||||
|
||||
public lateUpdate() {
|
||||
if (this.checkProcessing()) {
|
||||
this.lateProcess(this._entities);
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在系统处理开始前调用
|
||||
* 在下一个系统开始处理或新的处理回合开始之前(以先到者为准),使用此方法创建的任何实体都不会激活
|
||||
*/
|
||||
protected begin() {
|
||||
if (!Core.Instance.debug)
|
||||
return;
|
||||
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]) { }
|
||||
|
||||
protected lateProcess(entities: Entity[]) { }
|
||||
|
||||
/**
|
||||
* 系统处理完毕后调用
|
||||
*/
|
||||
protected end() {
|
||||
if (!Core.Instance.debug)
|
||||
return;
|
||||
|
||||
this._endTime = Date.now();
|
||||
this._useTime = this._endTime - this._startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统是否需要处理
|
||||
*
|
||||
* 在启用系统时有用,但仅偶尔需要处理
|
||||
* 这只影响处理,不影响事件或订阅列表
|
||||
* @returns 如果系统应该处理,则为true,如果不处理则为false。
|
||||
*/
|
||||
protected checkProcessing() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
///<reference path="./IntervalSystem.ts"/>
|
||||
module es {
|
||||
/**
|
||||
* 每x个ticks处理一个实体的子集
|
||||
*
|
||||
* 典型的用法是每隔一定的时间间隔重新生成弹药或生命值
|
||||
* 而无需在每个游戏循环中都进行
|
||||
* 而是每100毫秒一次或每秒
|
||||
*/
|
||||
export abstract class IntervalIteratingSystem extends IntervalSystem {
|
||||
constructor(matcher: Matcher, interval: number) {
|
||||
super(matcher, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理本系统感兴趣的实体
|
||||
* @param entity
|
||||
*/
|
||||
public abstract processEntity(entity: Entity);
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
entities.forEach(entity => this.processEntity(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 实体系统以一定的时间间隔进行处理
|
||||
*/
|
||||
export abstract class IntervalSystem extends EntitySystem {
|
||||
/**
|
||||
* 累积增量以跟踪间隔
|
||||
*/
|
||||
protected acc: number = 0;
|
||||
/**
|
||||
* 更新之间需要等待多长时间
|
||||
*/
|
||||
private readonly interval: number = 0;
|
||||
private intervalDelta: number = 0;
|
||||
|
||||
constructor(matcher: Matcher, interval: number) {
|
||||
super(matcher);
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
protected checkProcessing() {
|
||||
this.acc += Time.deltaTime;
|
||||
if (this.acc >= this.interval) {
|
||||
this.acc -= this.interval;
|
||||
this.intervalDelta = (this.acc - this.intervalDelta);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本系统上次处理后的实际delta值
|
||||
*/
|
||||
protected getIntervalDelta() {
|
||||
return this.interval + this.intervalDelta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* JobSystem使用实体的子集调用Execute(entities),并在指定数量的线程上分配工作负载。
|
||||
*/
|
||||
export abstract class JobSystem extends EntitySystem {
|
||||
public readonly _threads: number;
|
||||
public readonly _jobs: Job[];
|
||||
public readonly _executeStr: string;
|
||||
|
||||
constructor(matcher: Matcher, threads: number) {
|
||||
super(matcher);
|
||||
|
||||
this._threads = threads;
|
||||
this._jobs = new Array(threads);
|
||||
for (let i = 0; i < this._jobs.length; i++) {
|
||||
this._jobs[i] = new Job();
|
||||
}
|
||||
this._executeStr = JSON.stringify(this.execute, function (key, val) {
|
||||
if (typeof val === 'function') {
|
||||
return val + '';
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
let remainder = entities.length & this._threads;
|
||||
let slice = entities.length / this._threads + (remainder == 0 ? 0 : 1);
|
||||
for (let t = 0; t < this._threads; t++) {
|
||||
let from = t * slice;
|
||||
let to = from + slice;
|
||||
if (to > entities.length) {
|
||||
to = entities.length;
|
||||
}
|
||||
|
||||
let job = this._jobs[t];
|
||||
job.set(entities, from, to, this._executeStr, this);
|
||||
if (from != to) {
|
||||
const worker = WorkerUtils.makeWorker(this.queueOnThread);
|
||||
const workerDo = WorkerUtils.workerMessage(worker);
|
||||
workerDo(job).then((message) => {
|
||||
let job = message as Job;
|
||||
this.resetJob(job);
|
||||
worker.terminate();
|
||||
}).catch((err) => {
|
||||
job.err = err;
|
||||
worker.terminate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private queueOnThread() {
|
||||
onmessage = ({ data: { jobId, message } }) => {
|
||||
let job = message[0] as Job;
|
||||
let executeFunc: Function = JSON.parse(job.execute, function (k, v) {
|
||||
if (v.indexOf && v.indexOf('function') > -1) {
|
||||
return eval("(function(){return " + v + " })()")
|
||||
}
|
||||
|
||||
return v;
|
||||
});
|
||||
for (let i = job.from; i < job.to; i++) {
|
||||
executeFunc.call(job.context, job.entities[i]);
|
||||
}
|
||||
|
||||
postMessage({ jobId, result: message }, null);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 当操作完成时,改变的值需要用户进行手动传递
|
||||
* 由于worker数据无法共享,所以这块需要特殊处理
|
||||
* @example this.test = job[0].context.test;
|
||||
* @param job
|
||||
*/
|
||||
protected abstract resetJob(job: Job);
|
||||
/**
|
||||
* 对指定实体进行多线程操作
|
||||
* @param entity
|
||||
*/
|
||||
protected abstract execute(entity: Entity);
|
||||
}
|
||||
|
||||
class Job {
|
||||
public entities: Entity[];
|
||||
public from: number;
|
||||
public to: number;
|
||||
public worker: Worker;
|
||||
public execute: string;
|
||||
public err: string;
|
||||
public context;
|
||||
|
||||
public set(entities: Entity[], from: number, to: number, execute: string, context: any) {
|
||||
this.entities = entities;
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.execute = execute;
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
module es {
|
||||
export abstract class PassiveSystem extends EntitySystem {
|
||||
public onChanged(entity: Entity) {
|
||||
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
// 我们用我们自己的不考虑实体的基本实体系统来代替
|
||||
this.begin();
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/** 用于协调其他系统的通用系统基类 */
|
||||
module es {
|
||||
export abstract class ProcessingSystem extends EntitySystem {
|
||||
public onChanged(entity: Entity) {
|
||||
|
||||
}
|
||||
|
||||
/** 处理我们的系统 每帧调用 */
|
||||
public abstract processSystem();
|
||||
|
||||
protected process(entities: Entity[]) {
|
||||
this.begin();
|
||||
this.processSystem();
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
module es {
|
||||
export enum ComponentTransform {
|
||||
position,
|
||||
scale,
|
||||
rotation,
|
||||
}
|
||||
|
||||
export enum DirtyType {
|
||||
clean = 0,
|
||||
positionDirty = 1,
|
||||
scaleDirty = 2,
|
||||
rotationDirty = 4,
|
||||
}
|
||||
|
||||
export class Transform {
|
||||
/** 与此转换关联的实体 */
|
||||
public readonly entity: Entity;
|
||||
public hierarchyDirty: DirtyType;
|
||||
public _localDirty: boolean;
|
||||
public _localPositionDirty: boolean;
|
||||
public _localScaleDirty: boolean;
|
||||
public _localRotationDirty: boolean;
|
||||
public _positionDirty: boolean;
|
||||
public _worldToLocalDirty: boolean;
|
||||
public _worldInverseDirty: boolean;
|
||||
/**
|
||||
* 值会根据位置、旋转和比例自动重新计算
|
||||
*/
|
||||
public _localTransform: Matrix2D = Matrix2D.identity;
|
||||
/**
|
||||
* 值将自动从本地和父矩阵重新计算。
|
||||
*/
|
||||
public _worldTransform = Matrix2D.identity;
|
||||
public _rotationMatrix: Matrix2D = Matrix2D.identity;
|
||||
public _translationMatrix: Matrix2D = Matrix2D.identity;
|
||||
public _scaleMatrix: Matrix2D = Matrix2D.identity;
|
||||
public _children: Transform[] = [];
|
||||
|
||||
constructor(entity: Entity) {
|
||||
this.entity = entity;
|
||||
this.scale = this._localScale = Vector2.one;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个转换的所有子元素
|
||||
*/
|
||||
public get childCount() {
|
||||
return this._children.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换在世界空间的旋转度
|
||||
*/
|
||||
public get rotationDegrees(): number {
|
||||
return MathHelper.toDegrees(this._rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换在世界空间的旋转度
|
||||
* @param value
|
||||
*/
|
||||
public set rotationDegrees(value: number) {
|
||||
this.setRotation(MathHelper.toRadians(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转相对于父变换旋转的角度
|
||||
*/
|
||||
public get localRotationDegrees(): number {
|
||||
return MathHelper.toDegrees(this._localRotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转相对于父变换旋转的角度
|
||||
* @param value
|
||||
*/
|
||||
public set localRotationDegrees(value: number) {
|
||||
this.localRotation = MathHelper.toRadians(value);
|
||||
}
|
||||
|
||||
public get localToWorldTransform(): Matrix2D {
|
||||
this.updateTransform();
|
||||
return this._worldTransform;
|
||||
}
|
||||
|
||||
public _parent: Transform;
|
||||
|
||||
/**
|
||||
* 获取此转换的父转换
|
||||
*/
|
||||
public get parent() {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置此转换的父转换
|
||||
* @param value
|
||||
*/
|
||||
public set parent(value: Transform) {
|
||||
this.setParent(value);
|
||||
}
|
||||
|
||||
public _worldToLocalTransform = Matrix2D.identity;
|
||||
|
||||
public get worldToLocalTransform(): Matrix2D {
|
||||
if (this._worldToLocalDirty) {
|
||||
if (this.parent == null) {
|
||||
this._worldToLocalTransform = Matrix2D.identity;
|
||||
} else {
|
||||
this.parent.updateTransform();
|
||||
this._worldToLocalTransform = Matrix2D.invert(this.parent._worldTransform);
|
||||
}
|
||||
|
||||
this._worldToLocalDirty = false;
|
||||
}
|
||||
|
||||
return this._worldToLocalTransform;
|
||||
}
|
||||
|
||||
public _worldInverseTransform = Matrix2D.identity;
|
||||
|
||||
public get worldInverseTransform(): Matrix2D {
|
||||
this.updateTransform();
|
||||
if (this._worldInverseDirty) {
|
||||
this._worldInverseTransform = Matrix2D.invert(this._worldTransform);
|
||||
this._worldInverseDirty = false;
|
||||
}
|
||||
|
||||
return this._worldInverseTransform;
|
||||
}
|
||||
|
||||
public _position: Vector2 = Vector2.zero;
|
||||
|
||||
/**
|
||||
* 变换在世界空间中的位置
|
||||
*/
|
||||
public get position(): Vector2 {
|
||||
this.updateTransform();
|
||||
if (this._positionDirty) {
|
||||
if (this.parent == null) {
|
||||
this._position = this._localPosition;
|
||||
} else {
|
||||
this.parent.updateTransform();
|
||||
Vector2Ext.transformR(this._localPosition, this.parent._worldTransform, this._position);
|
||||
}
|
||||
|
||||
this._positionDirty = false;
|
||||
}
|
||||
|
||||
return this._position;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换在世界空间中的位置
|
||||
* @param value
|
||||
*/
|
||||
public set position(value: Vector2) {
|
||||
this.setPosition(value.x, value.y);
|
||||
}
|
||||
|
||||
public _scale: Vector2 = Vector2.one;
|
||||
|
||||
/**
|
||||
* 变换在世界空间的缩放
|
||||
*/
|
||||
public get scale(): Vector2 {
|
||||
this.updateTransform();
|
||||
return this._scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换在世界空间的缩放
|
||||
* @param value
|
||||
*/
|
||||
public set scale(value: Vector2) {
|
||||
this.setScale(value);
|
||||
}
|
||||
|
||||
public _rotation: number = 0;
|
||||
|
||||
/**
|
||||
* 在世界空间中以弧度旋转的变换
|
||||
*/
|
||||
public get rotation(): number {
|
||||
this.updateTransform();
|
||||
return this._rotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换在世界空间的旋转度
|
||||
* @param value
|
||||
*/
|
||||
public set rotation(value: number) {
|
||||
this.setRotation(value);
|
||||
}
|
||||
|
||||
public _localPosition: Vector2 = Vector2.zero;
|
||||
|
||||
/**
|
||||
* 转换相对于父转换的位置。如果转换没有父元素,则与transform.position相同
|
||||
*/
|
||||
public get localPosition(): Vector2 {
|
||||
this.updateTransform();
|
||||
return this._localPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换相对于父转换的位置。如果转换没有父元素,则与transform.position相同
|
||||
* @param value
|
||||
*/
|
||||
public set localPosition(value: Vector2) {
|
||||
this.setLocalPosition(value);
|
||||
}
|
||||
|
||||
public _localScale: Vector2 = Vector2.one;
|
||||
|
||||
/**
|
||||
* 转换相对于父元素的比例。如果转换没有父元素,则与transform.scale相同
|
||||
*/
|
||||
public get localScale(): Vector2 {
|
||||
this.updateTransform();
|
||||
return this._localScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换相对于父元素的比例。如果转换没有父元素,则与transform.scale相同
|
||||
* @param value
|
||||
*/
|
||||
public set localScale(value: Vector2) {
|
||||
this.setLocalScale(value);
|
||||
}
|
||||
|
||||
public _localRotation: number = 0;
|
||||
|
||||
/**
|
||||
* 相对于父变换的旋转,变换的旋转。如果转换没有父元素,则与transform.rotation相同
|
||||
*/
|
||||
public get localRotation(): number {
|
||||
this.updateTransform();
|
||||
return this._localRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对于父变换的旋转,变换的旋转。如果转换没有父元素,则与transform.rotation相同
|
||||
* @param value
|
||||
*/
|
||||
public set localRotation(value: number) {
|
||||
this.setLocalRotation(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回在索引处的转换子元素
|
||||
* @param index
|
||||
*/
|
||||
public getChild(index: number): Transform {
|
||||
return this._children[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置此转换的父转换
|
||||
* @param parent
|
||||
*/
|
||||
public setParent(parent: Transform): Transform {
|
||||
if (this._parent == parent)
|
||||
return this;
|
||||
|
||||
if (this._parent != null) {
|
||||
const index = this._parent._children.findIndex(t => t == this);
|
||||
if (index != -1)
|
||||
this._parent._children.splice(index, 1);
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
parent._children.push(this);
|
||||
}
|
||||
|
||||
this._parent = parent;
|
||||
this.setDirty(DirtyType.positionDirty);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置转换在世界空间中的位置
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
public setPosition(x: number, y: number): Transform {
|
||||
let position = new Vector2(x, y);
|
||||
if (position.equals(this._position))
|
||||
return this;
|
||||
|
||||
this._position = position;
|
||||
if (this.parent != null) {
|
||||
this.localPosition = Vector2.transform(this._position, this.worldToLocalTransform);
|
||||
} else {
|
||||
this.localPosition = position;
|
||||
}
|
||||
this._positionDirty = false;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置转换相对于父转换的位置。如果转换没有父元素,则与transform.position相同
|
||||
* @param localPosition
|
||||
*/
|
||||
public setLocalPosition(localPosition: Vector2): Transform {
|
||||
if (localPosition.equals(this._localPosition))
|
||||
return this;
|
||||
|
||||
this._localPosition = localPosition;
|
||||
this._localDirty = this._positionDirty = this._localPositionDirty = this._localRotationDirty = this._localScaleDirty = true;
|
||||
this.setDirty(DirtyType.positionDirty);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置变换在世界空间的旋转度
|
||||
* @param radians
|
||||
*/
|
||||
public setRotation(radians: number): Transform {
|
||||
this._rotation = radians;
|
||||
if (this.parent != null) {
|
||||
this.localRotation = this.parent.rotation + radians;
|
||||
} else {
|
||||
this.localRotation = radians;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换在世界空间的旋转度
|
||||
* @param degrees
|
||||
*/
|
||||
public setRotationDegrees(degrees: number): Transform {
|
||||
return this.setRotation(MathHelper.toRadians(degrees));
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转精灵的顶部,使其朝向位置
|
||||
* @param pos
|
||||
*/
|
||||
public lookAt(pos: Vector2) {
|
||||
const sign = this.position.x > pos.x ? -1 : 1;
|
||||
const vectorToAlignTo = this.position.sub(pos).normalize();
|
||||
this.rotation = sign * Math.acos(vectorToAlignTo.dot(Vector2.unitY));
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对于父变换的旋转设置变换的旋转。如果转换没有父元素,则与transform.rotation相同
|
||||
* @param radians
|
||||
*/
|
||||
public setLocalRotation(radians: number) {
|
||||
this._localRotation = radians;
|
||||
this._localDirty = this._positionDirty = this._localPositionDirty = this._localRotationDirty = this._localScaleDirty = true;
|
||||
this.setDirty(DirtyType.rotationDirty);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对于父变换的旋转设置变换的旋转。如果转换没有父元素,则与transform.rotation相同
|
||||
* @param degrees
|
||||
*/
|
||||
public setLocalRotationDegrees(degrees: number): Transform {
|
||||
return this.setLocalRotation(MathHelper.toRadians(degrees));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换在世界空间中的缩放
|
||||
* @param scale
|
||||
*/
|
||||
public setScale(scale: Vector2): Transform {
|
||||
this._scale = scale;
|
||||
if (this.parent != null) {
|
||||
this.localScale = Vector2.divide(scale, this.parent._scale);
|
||||
} else {
|
||||
this.localScale = scale;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置转换相对于父对象的比例。如果转换没有父元素,则与transform.scale相同
|
||||
* @param scale
|
||||
*/
|
||||
public setLocalScale(scale: Vector2): Transform {
|
||||
this._localScale = scale;
|
||||
this._localDirty = this._positionDirty = this._localScaleDirty = true;
|
||||
this.setDirty(DirtyType.scaleDirty);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对精灵坐标进行四舍五入
|
||||
*/
|
||||
public roundPosition() {
|
||||
this.position = Vector2Ext.round(this._position);
|
||||
}
|
||||
|
||||
public updateTransform() {
|
||||
if (this.hierarchyDirty != DirtyType.clean) {
|
||||
if (this.parent != null)
|
||||
this.parent.updateTransform();
|
||||
|
||||
if (this._localDirty) {
|
||||
if (this._localPositionDirty) {
|
||||
Matrix2D.createTranslation(this._localPosition.x, this._localPosition.y, this._translationMatrix);
|
||||
this._localPositionDirty = false;
|
||||
}
|
||||
|
||||
if (this._localRotationDirty) {
|
||||
Matrix2D.createRotation(this._localRotation, this._rotationMatrix);
|
||||
this._localRotationDirty = false;
|
||||
}
|
||||
|
||||
if (this._localScaleDirty) {
|
||||
Matrix2D.createScale(this._localScale.x, this._localScale.y, this._scaleMatrix);
|
||||
this._localScaleDirty = false;
|
||||
}
|
||||
|
||||
Matrix2D.multiply(this._scaleMatrix, this._rotationMatrix, this._localTransform);
|
||||
Matrix2D.multiply(this._localTransform, this._translationMatrix, this._localTransform);
|
||||
|
||||
if (this.parent == null) {
|
||||
this._worldTransform = this._localTransform;
|
||||
this._rotation = this._localRotation;
|
||||
this._scale = this._localScale;
|
||||
this._worldInverseDirty = true;
|
||||
}
|
||||
|
||||
this._localDirty = false;
|
||||
}
|
||||
|
||||
if (this.parent != null) {
|
||||
Matrix2D.multiply(this._localTransform, this.parent._worldTransform, this._worldTransform);
|
||||
this._rotation = this._localRotation + this.parent._rotation;
|
||||
this._scale = this.parent._scale.multiply(this._localScale);;
|
||||
this._worldInverseDirty = true;
|
||||
}
|
||||
|
||||
this._worldToLocalDirty = true;
|
||||
this._positionDirty = true;
|
||||
this.hierarchyDirty = DirtyType.clean;
|
||||
}
|
||||
}
|
||||
|
||||
public setDirty(dirtyFlagType: DirtyType) {
|
||||
if ((this.hierarchyDirty & dirtyFlagType) == 0) {
|
||||
this.hierarchyDirty |= dirtyFlagType;
|
||||
|
||||
switch (dirtyFlagType) {
|
||||
case DirtyType.positionDirty:
|
||||
this.entity.onTransformChanged(ComponentTransform.position);
|
||||
break;
|
||||
case DirtyType.rotationDirty:
|
||||
this.entity.onTransformChanged(ComponentTransform.rotation);
|
||||
break;
|
||||
case DirtyType.scaleDirty:
|
||||
this.entity.onTransformChanged(ComponentTransform.scale);
|
||||
break;
|
||||
}
|
||||
|
||||
// 告诉子项发生了变换
|
||||
for (let i = 0; i < this._children.length; i++)
|
||||
this._children[i].setDirty(dirtyFlagType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从另一个transform属性进行拷贝
|
||||
* @param transform
|
||||
*/
|
||||
public copyFrom(transform: Transform) {
|
||||
this._position = transform.position.clone();
|
||||
this._localPosition = transform._localPosition.clone();
|
||||
this._rotation = transform._rotation;
|
||||
this._localRotation = transform._localRotation;
|
||||
this._scale = transform._scale;
|
||||
this._localScale = transform._localScale;
|
||||
|
||||
this.setDirty(DirtyType.positionDirty);
|
||||
this.setDirty(DirtyType.rotationDirty);
|
||||
this.setDirty(DirtyType.scaleDirty);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `[Transform: parent: ${this.parent}, position: ${this.position}, rotation: ${this.rotation},
|
||||
scale: ${this.scale}, localPosition: ${this._localPosition}, localRotation: ${this._localRotation},
|
||||
localScale: ${this._localScale}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
module es {
|
||||
export class Bits {
|
||||
private _bit: {[index: number]: number} = {};
|
||||
|
||||
public set(index: number, value: number) {
|
||||
this._bit[index] = value;
|
||||
}
|
||||
|
||||
public get(index: number): number {
|
||||
let v = this._bit[index];
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
///<reference path="../Components/IUpdatable.ts" />
|
||||
module es {
|
||||
export class ComponentList {
|
||||
/**
|
||||
* 组件列表的全局updateOrder排序
|
||||
*/
|
||||
public static compareUpdatableOrder: IUpdatableComparer = new IUpdatableComparer();
|
||||
public _entity: Entity;
|
||||
|
||||
/**
|
||||
* 添加到实体的组件列表
|
||||
*/
|
||||
public _components: Component[] = [];
|
||||
/**
|
||||
* 所有需要更新的组件列表
|
||||
*/
|
||||
public _updatableComponents: IUpdatable[] = [];
|
||||
/**
|
||||
* 添加到此框架的组件列表。用来对组件进行分组,这样我们就可以同时进行加工
|
||||
*/
|
||||
public _componentsToAdd: { [index: number]: Component } = {};
|
||||
/**
|
||||
* 标记要删除此框架的组件列表。用来对组件进行分组,这样我们就可以同时进行加工
|
||||
*/
|
||||
public _componentsToRemove: { [index: number]: Component } = {};
|
||||
public _componentsToAddList: Component[] = [];
|
||||
public _componentsToRemoveList: Component[] = [];
|
||||
public _tempBufferList: Component[] = [];
|
||||
/**
|
||||
* 用于确定是否需要对该框架中的组件进行排序的标志
|
||||
*/
|
||||
public _isComponentListUnsorted: boolean;
|
||||
private componentsByType = new Map<new (...args: any[]) => Component, es.Component[]>();
|
||||
private componentsToAddByType = new Map<new (...args: any[]) => Component, es.Component[]>();
|
||||
|
||||
constructor(entity: Entity) {
|
||||
this._entity = entity;
|
||||
}
|
||||
|
||||
public get count() {
|
||||
return this._components.length;
|
||||
}
|
||||
|
||||
public get buffer() {
|
||||
return this._components;
|
||||
}
|
||||
|
||||
public markEntityListUnsorted() {
|
||||
this._isComponentListUnsorted = true;
|
||||
}
|
||||
|
||||
public add(component: Component) {
|
||||
this._componentsToAdd[component.id] = component;
|
||||
this._componentsToAddList.push(component);
|
||||
this.addComponentsToAddByType(component);
|
||||
}
|
||||
|
||||
public remove(component: Component) {
|
||||
if (this._componentsToAdd[component.id]) {
|
||||
let index = this._componentsToAddList.findIndex(c => c.id == component.id);
|
||||
if (index != -1)
|
||||
this._componentsToAddList.splice(index, 1);
|
||||
delete this._componentsToAdd[component.id];
|
||||
this.removeComponentsToAddByType(component);
|
||||
return;
|
||||
}
|
||||
|
||||
this._componentsToRemove[component.id] = component;
|
||||
this._componentsToRemoveList.push(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即从组件列表中删除所有组件
|
||||
*/
|
||||
public removeAllComponents() {
|
||||
if (this._components.length > 0) {
|
||||
for (let i = 0, s = this._components.length; i < s; ++ i) {
|
||||
this.handleRemove(this._components[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.componentsByType.clear();
|
||||
this.componentsToAddByType.clear();
|
||||
this._components.length = 0;
|
||||
this._updatableComponents.length = 0;
|
||||
this._componentsToAdd = {};
|
||||
this._componentsToRemove = {};
|
||||
this._componentsToAddList.length = 0;
|
||||
this._componentsToRemoveList.length = 0;
|
||||
}
|
||||
|
||||
public deregisterAllComponents() {
|
||||
if (this._components.length > 0) {
|
||||
for (let i = 0, s = this._components.length; i < s; ++ i) {
|
||||
let component = this._components[i];
|
||||
if (!component) continue;
|
||||
|
||||
if (component instanceof RenderableComponent)
|
||||
this._entity.scene.renderableComponents.remove(component);
|
||||
|
||||
// 处理IUpdatable
|
||||
if (isIUpdatable(component))
|
||||
new es.List(this._updatableComponents).remove(component);
|
||||
|
||||
this.decreaseBits(component);
|
||||
this._entity.scene.entityProcessors.onComponentRemoved(this._entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public registerAllComponents() {
|
||||
if (this._components.length > 0) {
|
||||
for (let i = 0, s = this._components.length; i < s; ++ i) {
|
||||
let component = this._components[i];
|
||||
if (component instanceof RenderableComponent)
|
||||
this._entity.scene.renderableComponents.remove(component);
|
||||
|
||||
if (isIUpdatable(component))
|
||||
this._updatableComponents.push(component);
|
||||
|
||||
this.addBits(component);
|
||||
this._entity.scene.entityProcessors.onComponentAdded(this._entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private decreaseBits(component: Component) {
|
||||
let bits = this._entity.componentBits;
|
||||
let typeIndex = ComponentTypeManager.getIndexFor(TypeUtils.getType(component));
|
||||
bits.set(typeIndex, bits.get(typeIndex) - 1);
|
||||
}
|
||||
|
||||
private addBits(component: Component) {
|
||||
let bits = this._entity.componentBits;
|
||||
let typeIndex = ComponentTypeManager.getIndexFor(TypeUtils.getType(component));
|
||||
bits.set(typeIndex, bits.get(typeIndex) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理任何需要删除或添加的组件
|
||||
*/
|
||||
public updateLists() {
|
||||
if (this._componentsToRemoveList.length > 0) {
|
||||
for (let i = 0, l = this._componentsToRemoveList.length; i < l; ++ i) {
|
||||
let component = this._componentsToRemoveList[i];
|
||||
this.handleRemove(component);
|
||||
let index = this._components.findIndex(c => c.id == component.id);
|
||||
if (index != -1)
|
||||
this._components.splice(index, 1);
|
||||
this.removeComponentsByType(component);
|
||||
}
|
||||
|
||||
this._componentsToRemove = {};
|
||||
this._componentsToRemoveList.length = 0;
|
||||
}
|
||||
|
||||
if (this._componentsToAddList.length > 0) {
|
||||
for (let i = 0, l = this._componentsToAddList.length; i < l; ++ i) {
|
||||
let component = this._componentsToAddList[i];
|
||||
if (component instanceof RenderableComponent)
|
||||
this._entity.scene.renderableComponents.add(component);
|
||||
|
||||
if (isIUpdatable(component))
|
||||
this._updatableComponents.push(component);
|
||||
|
||||
this.addBits(component);
|
||||
this._entity.scene.entityProcessors.onComponentAdded(this._entity);
|
||||
|
||||
this.addComponentsByType(component);
|
||||
this._components.push(component);
|
||||
this._tempBufferList.push(component);
|
||||
}
|
||||
|
||||
// 在调用onAddedToEntity之前清除,以防添加更多组件
|
||||
this._componentsToAdd = {};
|
||||
this._componentsToAddList.length = 0;
|
||||
this.componentsToAddByType.clear();
|
||||
this._isComponentListUnsorted = true;
|
||||
}
|
||||
|
||||
if (this._tempBufferList.length > 0) {
|
||||
// 现在所有的组件都添加到了场景中,我们再次循环并调用onAddedToEntity/onEnabled
|
||||
for (let i = 0, l = this._tempBufferList.length; i < l; ++ i) {
|
||||
let component = this._tempBufferList[i];
|
||||
component.onAddedToEntity();
|
||||
|
||||
// enabled检查实体和组件
|
||||
if (component.enabled) {
|
||||
component.onEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
this._tempBufferList.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public handleRemove(component: Component) {
|
||||
if (component instanceof RenderableComponent)
|
||||
this._entity.scene.renderableComponents.remove(component);
|
||||
|
||||
if (isIUpdatable(component) && this._updatableComponents.length > 0) {
|
||||
let index = this._updatableComponents.findIndex((c) => (<any>c as Component).id == component.id);
|
||||
if (index != -1)
|
||||
this._updatableComponents.splice(index, 1);
|
||||
}
|
||||
|
||||
this.decreaseBits(component);
|
||||
this._entity.scene.entityProcessors.onComponentRemoved(this._entity);
|
||||
|
||||
component.onRemovedFromEntity();
|
||||
component.entity = null;
|
||||
}
|
||||
|
||||
private removeComponentsByType(component: Component) {
|
||||
let fastList = this.componentsByType.get(TypeUtils.getType(component));
|
||||
let fastIndex = fastList.findIndex(c => c.id == component.id);
|
||||
if (fastIndex != -1) {
|
||||
fastList.splice(fastIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private addComponentsByType(component: Component) {
|
||||
let fastList = this.componentsByType.get(TypeUtils.getType(component));
|
||||
if (!fastList) fastList = [];
|
||||
fastList.push(component);
|
||||
this.componentsByType.set(TypeUtils.getType(component), fastList);
|
||||
}
|
||||
|
||||
private removeComponentsToAddByType(component: Component) {
|
||||
let fastList = this.componentsToAddByType.get(TypeUtils.getType(component));
|
||||
let fastIndex = fastList.findIndex(c => c.id == component.id);
|
||||
if (fastIndex != -1) {
|
||||
fastList.splice(fastIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private addComponentsToAddByType(component: Component) {
|
||||
let fastList = this.componentsToAddByType.get(TypeUtils.getType(component));
|
||||
if (!fastList) fastList = [];
|
||||
fastList.push(component);
|
||||
this.componentsToAddByType.set(TypeUtils.getType(component), fastList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型T的第一个组件并返回它
|
||||
* 可以选择跳过检查未初始化的组件(尚未调用onAddedToEntity方法的组件)
|
||||
* 如果没有找到组件,则返回null。
|
||||
* @param type
|
||||
* @param onlyReturnInitializedComponents
|
||||
*/
|
||||
public getComponent<T extends Component>(type, onlyReturnInitializedComponents: boolean): T {
|
||||
let fastList = this.componentsByType.get(type);
|
||||
if (fastList && fastList.length > 0)
|
||||
return fastList[0] as T;
|
||||
|
||||
// 我们可以选择检查挂起的组件,以防addComponent和getComponent在同一个框架中被调用
|
||||
if (!onlyReturnInitializedComponents) {
|
||||
let fastToAddList = this.componentsToAddByType.get(type);
|
||||
if (fastToAddList && fastToAddList.length > 0)
|
||||
return fastToAddList[0] as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取T类型的所有组件,但不使用列表分配
|
||||
* @param typeName
|
||||
* @param components
|
||||
*/
|
||||
public getComponents(typeName: any, components?: any[]) {
|
||||
if (!components)
|
||||
components = [];
|
||||
|
||||
let fastList = this.componentsByType.get(typeName);
|
||||
if (fastList)
|
||||
components = components.concat(fastList);
|
||||
|
||||
let fastToAddList = this.componentsToAddByType.get(typeName);
|
||||
if (fastToAddList)
|
||||
components = components.concat(fastToAddList);
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.updateLists();
|
||||
if (this._updatableComponents.length > 0) {
|
||||
for (let i = 0, s = this._updatableComponents.length; i < s; ++ i) {
|
||||
let updateComponent = this._updatableComponents[i];
|
||||
if (updateComponent.enabled)
|
||||
updateComponent.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onEntityTransformChanged(comp: ComponentTransform) {
|
||||
if (this._components.length > 0 ){
|
||||
for (let i = 0, s = this._components.length; i < s; ++ i) {
|
||||
let component = this._components[i];
|
||||
if (component.enabled)
|
||||
component.onEntityTransformChanged(comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._componentsToAddList.length > 0) {
|
||||
for (let i = 0, s = this._componentsToAddList.length; i < s; ++ i) {
|
||||
let component = this._componentsToAddList[i];
|
||||
if (component.enabled)
|
||||
component.onEntityTransformChanged(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onEntityEnabled() {
|
||||
if (this._components.length > 0) {
|
||||
for (let i = 0, s = this._components.length; i < s; i ++)
|
||||
this._components[i].onEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
public onEntityDisabled() {
|
||||
if (this._components.length > 0) {
|
||||
for (let i = 0, s = this._components.length; i < s; i ++)
|
||||
this._components[i].onDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
public debugRender(batcher: IBatcher) {
|
||||
if (!batcher) return;
|
||||
for (let i = 0; i < this._components.length; i ++) {
|
||||
if (this._components[i].enabled) {
|
||||
this._components[i].debugRender(batcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
module es {
|
||||
interface IdentityHashMap {
|
||||
[key: string]: ComponentType;
|
||||
}
|
||||
|
||||
export class ComponentTypeFactory {
|
||||
private componentTypes_: IdentityHashMap;
|
||||
|
||||
private componentTypeCount_ = 0;
|
||||
|
||||
public types: Bag<ComponentType>;
|
||||
|
||||
constructor() {
|
||||
this.componentTypes_ = {};
|
||||
this.types = new Bag<ComponentType>();
|
||||
}
|
||||
|
||||
public getTypeFor(c): ComponentType {
|
||||
if ("number" === typeof c) {
|
||||
return this.types.get(c);
|
||||
}
|
||||
|
||||
let type: ComponentType = this.componentTypes_[getClassName(c)];
|
||||
|
||||
if (type == null) {
|
||||
const index: number = this.componentTypeCount_++;
|
||||
type = new ComponentType(c, index);
|
||||
this.componentTypes_[getClassName(c)] = type;
|
||||
this.types.set(index, type);
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
public getIndexFor(c): number {
|
||||
return this.getTypeFor(c).getIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
module es {
|
||||
export class ComponentTypeManager {
|
||||
private static _componentTypesMask: Map<any, number> = new Map<any, number>();
|
||||
|
||||
public static add(type) {
|
||||
if (!this._componentTypesMask.has(type))
|
||||
this._componentTypesMask.set(type, this._componentTypesMask.size);
|
||||
}
|
||||
|
||||
public static getIndexFor(type) {
|
||||
let v = -1;
|
||||
if (!this._componentTypesMask.has(type)) {
|
||||
this.add(type);
|
||||
v = this._componentTypesMask.get(type);
|
||||
} else {
|
||||
v = this._componentTypesMask.get(type);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
module es {
|
||||
export class EntityList {
|
||||
public scene: Scene;
|
||||
/**
|
||||
* 场景中添加的实体列表
|
||||
*/
|
||||
public _entities: Entity[] = [];
|
||||
/**
|
||||
* 本帧添加的实体列表。用于对实体进行分组,以便我们可以同时处理它们
|
||||
*/
|
||||
public _entitiesToAdded: {[index: number]: Entity} = {};
|
||||
/**
|
||||
* 本帧被标记为删除的实体列表。用于对实体进行分组,以便我们可以同时处理它们
|
||||
*/
|
||||
public _entitiesToRemove: {[index: number]: Entity} = {};
|
||||
public _entitiesToAddedList: Entity[] = [];
|
||||
public _entitiesToRemoveList: Entity[] = [];
|
||||
/**
|
||||
* 标志,用于确定我们是否需要在这一帧中对实体进行排序
|
||||
*/
|
||||
public _isEntityListUnsorted: boolean;
|
||||
/**
|
||||
* 通过标签跟踪实体,便于检索
|
||||
*/
|
||||
public _entityDict: Map<number, Set<Entity>> = new Map<number, Set<Entity>>();
|
||||
public _unsortedTags: Set<number> = new Set<number>();
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
public get count() {
|
||||
return this._entities.length;
|
||||
}
|
||||
|
||||
public get buffer() {
|
||||
return this._entities;
|
||||
}
|
||||
|
||||
public markEntityListUnsorted() {
|
||||
this._isEntityListUnsorted = true;
|
||||
}
|
||||
|
||||
public markTagUnsorted(tag: number) {
|
||||
this._unsortedTags.add(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一个实体添加到列表中。所有的生命周期方法将在下一帧中被调用
|
||||
* @param entity
|
||||
*/
|
||||
public add(entity: Entity) {
|
||||
this._entitiesToAdded[entity.id] = entity;
|
||||
this._entitiesToAddedList.push(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从列表中删除一个实体。所有的生命周期方法将在下一帧中被调用
|
||||
* @param entity
|
||||
*/
|
||||
public remove(entity: Entity) {
|
||||
// 防止在同一帧中添加或删除实体
|
||||
if (this._entitiesToAdded[entity.id]) {
|
||||
let index = this._entitiesToAddedList.findIndex(e => e.id == entity.id);
|
||||
if (index != -1)
|
||||
this._entitiesToAddedList.splice(index, 1);
|
||||
delete this._entitiesToAdded[entity.id];
|
||||
return;
|
||||
}
|
||||
|
||||
this._entitiesToRemoveList.push(entity);
|
||||
if (!this._entitiesToRemove[entity.id])
|
||||
this._entitiesToRemove[entity.id] = entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从实体列表中删除所有实体
|
||||
*/
|
||||
public removeAllEntities() {
|
||||
this._unsortedTags.clear();
|
||||
this._entitiesToAdded = {};
|
||||
this._entitiesToAddedList.length = 0;
|
||||
this._isEntityListUnsorted = false;
|
||||
|
||||
// 为什么我们要在这里更新列表?主要是为了处理在场景切换前被分离的实体。
|
||||
// 它们仍然会在_entitiesToRemove列表中,这将由updateLists处理。
|
||||
this.updateLists();
|
||||
|
||||
for (let i = 0; i < this._entities.length; i++) {
|
||||
this._entities[i]._isDestroyed = true;
|
||||
this._entities[i].onRemovedFromScene();
|
||||
this._entities[i].scene = null;
|
||||
}
|
||||
|
||||
this._entities.length = 0;
|
||||
this._entityDict.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体目前是否由这个EntityList管理
|
||||
* @param entity
|
||||
*/
|
||||
public contains(entity: Entity): boolean {
|
||||
return !!this._entitiesToAdded[entity.id];
|
||||
}
|
||||
|
||||
public getTagList(tag: number) {
|
||||
let list = this._entityDict.get(tag);
|
||||
if (!list) {
|
||||
list = new Set();
|
||||
this._entityDict.set(tag, list);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public addToTagList(entity: Entity) {
|
||||
this.getTagList(entity.tag).add(entity);
|
||||
this._unsortedTags.add(entity.tag);
|
||||
}
|
||||
|
||||
public removeFromTagList(entity: Entity) {
|
||||
let list = this._entityDict.get(entity.tag);
|
||||
if (list)
|
||||
list.delete(entity);
|
||||
}
|
||||
|
||||
public update() {
|
||||
for (let i = 0, s = this._entities.length; i < s; ++ i) {
|
||||
let entity = this._entities[i];
|
||||
if (entity.enabled && (entity.updateInterval == 1 || Time.frameCount % entity.updateInterval == 0))
|
||||
entity.update();
|
||||
}
|
||||
}
|
||||
|
||||
public updateLists() {
|
||||
if (this._entitiesToRemoveList.length > 0) {
|
||||
for (let i = 0, s = this._entitiesToRemoveList.length; i < s; ++ i) {
|
||||
let entity = this._entitiesToRemoveList[i];
|
||||
this.removeFromTagList(entity);
|
||||
|
||||
// 处理常规实体列表
|
||||
let index = this._entities.findIndex(e => e.id == entity.id);
|
||||
if (index != -1)
|
||||
this._entities.splice(index, 1);
|
||||
entity.onRemovedFromScene();
|
||||
entity.scene = null;
|
||||
|
||||
this.scene.entityProcessors.onEntityRemoved(entity);
|
||||
}
|
||||
|
||||
this._entitiesToRemove = {};
|
||||
this._entitiesToRemoveList.length = 0;
|
||||
}
|
||||
|
||||
if (this._entitiesToAddedList.length > 0) {
|
||||
for (let i = 0, s = this._entitiesToAddedList.length; i < s; ++ i) {
|
||||
let entity = this._entitiesToAddedList[i];
|
||||
this._entities.push(entity);
|
||||
entity.scene = this.scene;
|
||||
|
||||
this.addToTagList(entity);
|
||||
|
||||
this.scene.entityProcessors.onEntityAdded(entity);
|
||||
}
|
||||
|
||||
for (let i = 0, s = this._entitiesToAddedList.length; i < s; ++ i) {
|
||||
let entity = this._entitiesToAddedList[i];
|
||||
entity.onAddedToScene();
|
||||
}
|
||||
|
||||
this._entitiesToAdded = {};
|
||||
this._entitiesToAddedList.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个找到的名字为name的实体。如果没有找到则返回null
|
||||
* @param name
|
||||
*/
|
||||
public findEntity(name: string) {
|
||||
if (this._entities.length > 0) {
|
||||
for (let i = 0, s = this._entities.length; i < s; ++ i) {
|
||||
let entity = this._entities[i];
|
||||
if (entity.name == name)
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._entitiesToAddedList.length > 0) {
|
||||
for (let i = 0, s = this._entitiesToAddedList.length; i < s; ++ i) {
|
||||
let entity = this._entitiesToAddedList[i];
|
||||
if (entity.name == name)
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
public findEntityById(id: number) {
|
||||
if (this._entities.length > 0) {
|
||||
for (let i = 0, s = this._entities.length; i < s; ++ i) {
|
||||
let entity = this._entities[i];
|
||||
if (entity.id == id)
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return this._entitiesToAdded[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回带有标签的所有实体的列表。如果没有实体有标签,则返回一个空列表。
|
||||
* 返回的List可以通过ListPool.free放回池中
|
||||
* @param tag
|
||||
*/
|
||||
public entitiesWithTag(tag: number) {
|
||||
let list = this.getTagList(tag);
|
||||
|
||||
let returnList = ListPool.obtain<Entity>();
|
||||
if (list.size > 0) {
|
||||
for (let entity of list) {
|
||||
returnList.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第一个找到该tag的实体
|
||||
* @param tag
|
||||
* @returns
|
||||
*/
|
||||
public entityWithTag(tag: number) {
|
||||
let list = this.getTagList(tag);
|
||||
|
||||
if (list.size > 0) {
|
||||
for (let entity of list) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回在场景中找到的第一个T类型的组件。
|
||||
* @param type
|
||||
*/
|
||||
public findComponentOfType<T extends Component>(type): T {
|
||||
if (this._entities.length > 0 ){
|
||||
for (let i = 0, s = this._entities.length; i < s; i++) {
|
||||
let entity = this._entities[i];
|
||||
if (entity.enabled) {
|
||||
let comp = entity.getComponent<T>(type);
|
||||
if (comp)
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._entitiesToAddedList.length > 0) {
|
||||
for (let i = 0; i < this._entitiesToAddedList.length; i++) {
|
||||
let entity = this._entitiesToAddedList[i];
|
||||
if (entity.enabled) {
|
||||
let comp = entity.getComponent<T>(type);
|
||||
if (comp)
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回在场景中找到的所有T类型的组件。
|
||||
* 返回的List可以通过ListPool.free放回池中。
|
||||
* @param type
|
||||
*/
|
||||
public findComponentsOfType<T extends Component>(type): T[] {
|
||||
let comps = ListPool.obtain<T>();
|
||||
if (this._entities.length > 0) {
|
||||
for (let i = 0, s = this._entities.length; i < s; i++) {
|
||||
let entity = this._entities[i];
|
||||
if (entity.enabled)
|
||||
entity.getComponents(type, comps);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._entitiesToAddedList.length > 0) {
|
||||
for (let i = 0, s = this._entitiesToAddedList.length; i < s; i ++) {
|
||||
let entity = this._entitiesToAddedList[i];
|
||||
if (entity.enabled)
|
||||
entity.getComponents(type, comps);
|
||||
}
|
||||
}
|
||||
|
||||
return comps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回场景中包含特定组件的实体列表
|
||||
* @param types
|
||||
* @returns
|
||||
*/
|
||||
public findEntitesOfComponent(...types: any[]): Entity[] {
|
||||
let entities = [];
|
||||
if (this._entities.length > 0) {
|
||||
for (let i = 0, s = this._entities.length; i < s; i++) {
|
||||
if (this._entities[i].enabled) {
|
||||
let meet = true;
|
||||
if (types.length > 0)
|
||||
for (let t = 0, ts = types.length; t < ts; t ++) {
|
||||
let type = types[t];
|
||||
let hasComp = this._entities[i].hasComponent(type);
|
||||
if (!hasComp) {
|
||||
meet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (meet) {
|
||||
entities.push(this._entities[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._entitiesToAddedList.length > 0) {
|
||||
for (let i = 0, s = this._entitiesToAddedList.length; i < s; i ++) {
|
||||
let entity = this._entitiesToAddedList[i];
|
||||
if (entity.enabled) {
|
||||
let meet = true;
|
||||
if (types.length > 0)
|
||||
for (let t = 0, ts = types.length; t < ts; t ++) {
|
||||
let type = types[t];
|
||||
let hasComp = entity.hasComponent(type);
|
||||
if (!hasComp) {
|
||||
meet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (meet) {
|
||||
entities.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
module es {
|
||||
export class EntityProcessorList {
|
||||
public _processors: EntitySystem[] = [];
|
||||
|
||||
public add(processor: EntitySystem) {
|
||||
this._processors.push(processor);
|
||||
}
|
||||
|
||||
public remove(processor: EntitySystem) {
|
||||
new es.List(this._processors).remove(processor);
|
||||
}
|
||||
|
||||
public onComponentAdded(entity: Entity) {
|
||||
this.notifyEntityChanged(entity);
|
||||
}
|
||||
|
||||
public onComponentRemoved(entity: Entity) {
|
||||
this.notifyEntityChanged(entity);
|
||||
}
|
||||
|
||||
public onEntityAdded(entity: Entity) {
|
||||
this.notifyEntityChanged(entity);
|
||||
}
|
||||
|
||||
public onEntityRemoved(entity: Entity) {
|
||||
this.removeFromProcessors(entity);
|
||||
}
|
||||
|
||||
public begin() {
|
||||
|
||||
}
|
||||
|
||||
public update() {
|
||||
if (this._processors.length == 0)
|
||||
return;
|
||||
|
||||
for (let i = 0, s = this._processors.length; i < s; ++ i) {
|
||||
this._processors[i].update();
|
||||
}
|
||||
}
|
||||
|
||||
public lateUpdate() {
|
||||
if (this._processors.length == 0)
|
||||
return;
|
||||
|
||||
for (let i = 0, s = this._processors.length; i < s; ++ i) {
|
||||
this._processors[i].lateUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public end() {
|
||||
|
||||
}
|
||||
|
||||
public getProcessor<T extends EntitySystem>(type: new (...args: any[]) => T): T {
|
||||
if (this._processors.length == 0)
|
||||
return null;
|
||||
|
||||
for (let i = 0, s = this._processors.length; i < s; ++ i) {
|
||||
let processor = this._processors[i];
|
||||
if (processor instanceof type)
|
||||
return processor as T;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected notifyEntityChanged(entity: Entity) {
|
||||
if (this._processors.length == 0)
|
||||
return;
|
||||
|
||||
for (let i = 0, s = this._processors.length; i < s; ++ i) {
|
||||
this._processors[i].onChanged(entity);
|
||||
}
|
||||
}
|
||||
|
||||
protected removeFromProcessors(entity: Entity) {
|
||||
if (this._processors.length == 0)
|
||||
return;
|
||||
|
||||
for (let i = 0, s = this._processors.length; i < s; ++ i) {
|
||||
this._processors[i].remove(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
module es {
|
||||
export class HashHelpers {
|
||||
public static readonly hashCollisionThreshold: number = 100;
|
||||
public static readonly hashPrime: number = 101;
|
||||
|
||||
/**
|
||||
* 用来作为哈希表大小的质数表。
|
||||
* 一个典型的调整大小的算法会在这个数组中选取比之前容量大两倍的最小质数。
|
||||
* 假设我们的Hashtable当前的容量为x,并且添加了足够多的元素,因此需要进行大小调整。
|
||||
* 调整大小首先计算2x,然后在表中找到第一个大于2x的质数,即如果质数的顺序是p_1,p_2,...,p_i,...,则找到p_n,使p_n-1 < 2x < p_n。
|
||||
* 双倍对于保持哈希特操作的渐近复杂度是很重要的,比如添加。
|
||||
* 拥有一个质数可以保证双倍哈希不会导致无限循环。 IE,你的哈希函数将是h1(key)+i*h2(key),0 <= i < size.h2和size必须是相对质数。
|
||||
*/
|
||||
public static readonly primes = [3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
|
||||
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
|
||||
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
|
||||
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
|
||||
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369];
|
||||
|
||||
/**
|
||||
* 这是比Array.MaxArrayLength小的最大质数
|
||||
*/
|
||||
public static readonly maxPrimeArrayLength = 0x7FEFFFFD;
|
||||
|
||||
public static isPrime(candidate: number): boolean {
|
||||
if ((candidate & 1) != 0){
|
||||
let limit = Math.sqrt(candidate);
|
||||
for (let divisor = 3; divisor <= limit; divisor += 2){
|
||||
if ((candidate & divisor) == 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return (candidate == 2);
|
||||
}
|
||||
|
||||
public static getPrime(min: number): number{
|
||||
if (min < 0)
|
||||
throw new Error("参数错误 min不能小于0");
|
||||
|
||||
for (let i = 0; i < this.primes.length; i ++){
|
||||
let prime = this.primes[i];
|
||||
if (prime >= min) return prime;
|
||||
}
|
||||
|
||||
// 在我们预定义的表之外,计算的方式稍复杂。
|
||||
for (let i = (min | 1); i < Number.MAX_VALUE; i += 2){
|
||||
if (this.isPrime(i) && ((i - 1) % this.hashPrime != 0))
|
||||
return i;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param oldSize
|
||||
* @returns 返回要增长的哈希特表的大小
|
||||
*/
|
||||
public static expandPrime(oldSize: number): number {
|
||||
let newSize = 2 * oldSize;
|
||||
|
||||
// 在遇到容量溢出之前,允许哈希特表增长到最大可能的大小
|
||||
// 请注意,即使当_items.Length溢出时,这项检查也会起作用
|
||||
if (newSize > this.maxPrimeArrayLength && this.maxPrimeArrayLength > oldSize){
|
||||
return this.maxPrimeArrayLength;
|
||||
}
|
||||
|
||||
return this.getPrime(newSize);
|
||||
}
|
||||
|
||||
public static getHashCode(str){
|
||||
let s;
|
||||
if (typeof str == 'object'){
|
||||
s = JSON.stringify(str);
|
||||
} else {
|
||||
s = str.toString();
|
||||
}
|
||||
|
||||
let hash = 0;
|
||||
if (s.length == 0) return hash;
|
||||
for (let i = 0; i < s.length; i ++){
|
||||
let char = s.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
module es {
|
||||
export class IdentifierPool {
|
||||
private ids: Bag<number>;
|
||||
private nextAvailableId_ = 0;
|
||||
|
||||
constructor() {
|
||||
this.ids = new Bag<number>();
|
||||
}
|
||||
|
||||
public checkOut(): number {
|
||||
if (this.ids.size() > 0) {
|
||||
return this.ids.removeLast();
|
||||
}
|
||||
|
||||
return this.nextAvailableId_++;
|
||||
}
|
||||
|
||||
public checkIn(id: number): void {
|
||||
this.ids.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
module es {
|
||||
export class Matcher {
|
||||
protected allSet: (new (...args: any[]) => Component)[] = [];
|
||||
protected exclusionSet: (new (...args: any[]) => Component)[] = [];
|
||||
protected oneSet: (new (...args: any[]) => Component)[] = [];
|
||||
|
||||
public static empty() {
|
||||
return new Matcher();
|
||||
}
|
||||
|
||||
public getAllSet() {
|
||||
return this.allSet;
|
||||
}
|
||||
|
||||
public getExclusionSet() {
|
||||
return this.exclusionSet;
|
||||
}
|
||||
|
||||
public getOneSet() {
|
||||
return this.oneSet;
|
||||
}
|
||||
|
||||
public isInterestedEntity(e: Entity) {
|
||||
return this.isInterested(e.componentBits);
|
||||
}
|
||||
|
||||
public isInterested(components: Bits) {
|
||||
if (this.allSet.length != 0) {
|
||||
for (let i = 0, s = this.allSet.length; i < s; ++ i) {
|
||||
let type = this.allSet[i];
|
||||
if (!components.get(ComponentTypeManager.getIndexFor(type)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.exclusionSet.length != 0) {
|
||||
for (let i = 0, s = this.exclusionSet.length; i < s; ++ i) {
|
||||
let type = this.exclusionSet[i];
|
||||
if (components.get(ComponentTypeManager.getIndexFor(type)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.oneSet.length != 0) {
|
||||
for (let i = 0, s = this.oneSet.length; i < s; ++ i) {
|
||||
let type = this.oneSet[i];
|
||||
if (components.get(ComponentTypeManager.getIndexFor(type)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public all(...types: any[]): Matcher {
|
||||
let t;
|
||||
for (let i = 0, s = types.length; i < s; ++ i) {
|
||||
t = types[i];
|
||||
this.allSet.push(t);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public exclude(...types: any[]) {
|
||||
let t;
|
||||
for (let i = 0, s = types.length; i < s; ++ i) {
|
||||
t = types[i];
|
||||
this.exclusionSet.push(t);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public one(...types: any[]) {
|
||||
for (let i = 0, s = types.length; i < s; ++ i) {
|
||||
const t = types[i];
|
||||
this.oneSet.push(t);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
module es {
|
||||
export class RenderableComponentList {
|
||||
private _components: IRenderable[] = [];
|
||||
private _componentsByRenderLayer: Map<number, IRenderable[]> = new Map();
|
||||
private _unsortedRenderLayers: number[] = [];
|
||||
private _componentsNeedSort = true;
|
||||
|
||||
public get count() {
|
||||
return this._components.length;
|
||||
}
|
||||
|
||||
public get(index: number) {
|
||||
return this._components[index];
|
||||
}
|
||||
|
||||
public add(component: IRenderable) {
|
||||
this._components.push(component);
|
||||
this.addToRenderLayerList(component, component.renderLayer);
|
||||
}
|
||||
|
||||
public remove(component: IRenderable) {
|
||||
new List(this._components).remove(component);
|
||||
new List(this._componentsByRenderLayer.get(component.renderLayer)).remove(component);
|
||||
}
|
||||
|
||||
public updateRenderableRenderLayer(component: IRenderable, oldRenderLayer: number, newRenderLayer: number) {
|
||||
if (this._componentsByRenderLayer.has(oldRenderLayer) && new List(this._componentsByRenderLayer.get(oldRenderLayer)).contains(component)) {
|
||||
new List(this._componentsByRenderLayer.get(oldRenderLayer)).remove(component);
|
||||
this.addToRenderLayerList(component, newRenderLayer);
|
||||
}
|
||||
}
|
||||
|
||||
public setRenderLayerNeedsComponentSort(renderLayer: number) {
|
||||
const unsortedRenderLayersList = new List(this._unsortedRenderLayers);
|
||||
if (!unsortedRenderLayersList.contains(renderLayer))
|
||||
unsortedRenderLayersList.add(renderLayer);
|
||||
this._componentsNeedSort = true;
|
||||
}
|
||||
|
||||
public setNeedsComponentSort() {
|
||||
this._componentsNeedSort = true;
|
||||
}
|
||||
|
||||
private addToRenderLayerList(component: IRenderable, renderLayer: number) {
|
||||
let list = this.componentsWithRenderLayer(renderLayer);
|
||||
es.Insist.isFalse(!!list.find(c => c == component), "组件renderLayer列表已包含此组件");
|
||||
|
||||
list.push(component);
|
||||
const unsortedRenderLayersList = new List(this._unsortedRenderLayers);
|
||||
if (!unsortedRenderLayersList.contains(renderLayer))
|
||||
unsortedRenderLayersList.add(renderLayer);
|
||||
this._componentsNeedSort = true;
|
||||
}
|
||||
|
||||
public componentsWithRenderLayer(renderLayer: number) {
|
||||
if (!this._componentsByRenderLayer.get(renderLayer)) {
|
||||
this._componentsByRenderLayer.set(renderLayer, []);
|
||||
}
|
||||
|
||||
return this._componentsByRenderLayer.get(renderLayer);
|
||||
}
|
||||
|
||||
public updateLists() {
|
||||
if (this._componentsNeedSort) {
|
||||
this._components.sort((self, other) => other.renderLayer - self.renderLayer);
|
||||
this._componentsNeedSort = false;
|
||||
}
|
||||
|
||||
if (this._unsortedRenderLayers.length > 0) {
|
||||
for (let i = 0, count = this._unsortedRenderLayers.length; i < count; i ++) {
|
||||
const renderLayerComponents = this._componentsByRenderLayer.get(this._unsortedRenderLayers[i]);
|
||||
if (renderLayerComponents) {
|
||||
renderLayerComponents.sort((self, other) => other.renderLayer - self.renderLayer);
|
||||
}
|
||||
|
||||
this._unsortedRenderLayers.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
class StringUtils {
|
||||
/**
|
||||
* 特殊符号字符串
|
||||
*/
|
||||
private static specialSigns: string[] = [
|
||||
'&', '&',
|
||||
'<', '<',
|
||||
'>', '>',
|
||||
'"', '"',
|
||||
"'", ''',
|
||||
'®', '®',
|
||||
'©', '©',
|
||||
'™', '™',
|
||||
];
|
||||
|
||||
/**
|
||||
* 匹配中文字符
|
||||
* @param str 需要匹配的字符串
|
||||
* @return
|
||||
*/
|
||||
public static matchChineseWord(str: string): string[] {
|
||||
//中文字符的unicode值[\u4E00-\u9FA5]
|
||||
let patternA: RegExp = /[\u4E00-\u9FA5]+/gim;
|
||||
return str.match(patternA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除字符串左端的空白字符
|
||||
* @param target 目标字符串
|
||||
* @return
|
||||
*/
|
||||
public static lTrim(target: string): string {
|
||||
let startIndex: number = 0;
|
||||
while (this.isWhiteSpace(target.charAt(startIndex))) {
|
||||
startIndex++;
|
||||
}
|
||||
return target.slice(startIndex, target.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除字符串右端的空白字符
|
||||
* @param target 目标字符串
|
||||
* @return
|
||||
*/
|
||||
public static rTrim(target: string): string {
|
||||
let endIndex: number = target.length - 1;
|
||||
while (this.isWhiteSpace(target.charAt(endIndex))) {
|
||||
endIndex--;
|
||||
}
|
||||
return target.slice(0, endIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个去除2段空白字符的字符串
|
||||
* @param target
|
||||
* @return 返回一个去除2段空白字符的字符串
|
||||
*/
|
||||
public static trim(target: string): string {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
return this.rTrim(this.lTrim(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回该字符是否为空白字符
|
||||
* @param str
|
||||
* @return 返回该字符是否为空白字符
|
||||
*/
|
||||
public static isWhiteSpace(str: string): boolean {
|
||||
if (str == " " || str == "\t" || str == "\r" || str == "\n")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回执行替换后的字符串
|
||||
* @param mainStr 待查找字符串
|
||||
* @param targetStr 目标字符串
|
||||
* @param replaceStr 替换字符串
|
||||
* @param caseMark 是否忽略大小写
|
||||
* @return 返回执行替换后的字符串
|
||||
*/
|
||||
public static replaceMatch(mainStr: string, targetStr: string,
|
||||
replaceStr: string, caseMark: boolean = false): string {
|
||||
let len: number = mainStr.length;
|
||||
let tempStr: string = "";
|
||||
let isMatch: boolean = false;
|
||||
let tempTarget: string = caseMark == true ? targetStr.toLowerCase() : targetStr;
|
||||
for (let i: number = 0; i < len; i++) {
|
||||
isMatch = false;
|
||||
if (mainStr.charAt(i) == tempTarget.charAt(0)) {
|
||||
if (mainStr.substr(i, tempTarget.length) == tempTarget) {
|
||||
isMatch = true;
|
||||
}
|
||||
}
|
||||
if (isMatch) {
|
||||
tempStr += replaceStr;
|
||||
i = i + tempTarget.length - 1;
|
||||
} else {
|
||||
tempStr += mainStr.charAt(i);
|
||||
}
|
||||
}
|
||||
return tempStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用html实体换掉字符窜中的特殊字符
|
||||
* @param str 需要替换的字符串
|
||||
* @param reversion 是否翻转替换:将转义符号替换为正常的符号
|
||||
* @return 换掉特殊字符后的字符串
|
||||
*/
|
||||
public static htmlSpecialChars(str: string, reversion: boolean = false): string {
|
||||
let len: number = this.specialSigns.length;
|
||||
for (let i: number = 0; i < len; i += 2) {
|
||||
let from: string;
|
||||
let to: string;
|
||||
from = this.specialSigns[i];
|
||||
to = this.specialSigns[i + 1];
|
||||
if (reversion) {
|
||||
let temp: string = from;
|
||||
from = to;
|
||||
to = temp;
|
||||
}
|
||||
str = this.replaceMatch(str, from, to);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 给数字字符前面添 "0"
|
||||
*
|
||||
* @param str 要进行处理的字符串
|
||||
* @param width 处理后字符串的长度,
|
||||
* 如果str.length >= width,将不做任何处理直接返回原始的str。
|
||||
* @return
|
||||
*
|
||||
*/
|
||||
public static zfill(str: string, width: number = 2): string {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
width = Math.floor(width);
|
||||
let slen: number = str.length;
|
||||
if (slen >= width) {
|
||||
return str;
|
||||
}
|
||||
|
||||
let negative: boolean = false;
|
||||
if (str.substr(0, 1) == '-') {
|
||||
negative = true;
|
||||
str = str.substr(1);
|
||||
}
|
||||
|
||||
let len: number = width - slen;
|
||||
for (let i: number = 0; i < len; i++) {
|
||||
str = '0' + str;
|
||||
}
|
||||
|
||||
if (negative) {
|
||||
str = '-' + str;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 翻转字符串
|
||||
* @param str 字符串
|
||||
* @return 翻转后的字符串
|
||||
*/
|
||||
public static reverse(str: string): string {
|
||||
if (str.length > 1)
|
||||
return this.reverse(str.substring(1)) + str.substring(0, 1);
|
||||
else
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截断某段字符串
|
||||
* @param str 目标字符串
|
||||
* @param start 需要截断的起始索引
|
||||
* @param en 截断长度
|
||||
* @param order 顺序,true从字符串头部开始计算,false从字符串尾巴开始结算。
|
||||
* @return 截断后的字符串
|
||||
*/
|
||||
public static cutOff(str: string, start: number,
|
||||
len: number, order: boolean = true): string {
|
||||
start = Math.floor(start);
|
||||
len = Math.floor(len);
|
||||
let length: number = str.length;
|
||||
if (start > length) start = length;
|
||||
let s: number = start;
|
||||
let e: number = start + len;
|
||||
let newStr: string;
|
||||
if (order) {
|
||||
newStr = str.substring(0, s) + str.substr(e, length);
|
||||
} else {
|
||||
s = length - 1 - start - len;
|
||||
e = s + len;
|
||||
newStr = str.substring(0, s + 1) + str.substr(e + 1, length);
|
||||
}
|
||||
return newStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* {0} 字符替换
|
||||
*/
|
||||
public static strReplace(str: string, rStr: string[]): string {
|
||||
let i: number = 0, len: number = rStr.length;
|
||||
for (; i < len; i++) {
|
||||
if (rStr[i] == null || rStr[i] == "") {
|
||||
rStr[i] = "无";
|
||||
}
|
||||
str = str.replace("{" + i + "}", rStr[i]);
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
public static format(str: string, ...args: any[]) {
|
||||
for (let i = 0; i < args.length - 1; i++) {
|
||||
let reg = new RegExp("\\{" + i + "\\}", "gm");
|
||||
str = str.replace(reg, args[i + 1]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
module es {
|
||||
/** 提供帧定时信息 */
|
||||
export class Time {
|
||||
/** 游戏运行的总时间 */
|
||||
public static totalTime: number = 0;
|
||||
/** deltaTime的未缩放版本。不受时间尺度的影响 */
|
||||
public static unscaledDeltaTime: number = 0;
|
||||
/** 前一帧到当前帧的时间增量,按时间刻度进行缩放 */
|
||||
public static deltaTime: number = 0;
|
||||
/** 时间刻度缩放 */
|
||||
public static timeScale = 1;
|
||||
/** DeltaTime可以为的最大值 */
|
||||
public static maxDeltaTime = Number.MAX_VALUE;
|
||||
/** 已传递的帧总数 */
|
||||
public static frameCount = 0;
|
||||
/** 自场景加载以来的总时间 */
|
||||
public static timeSinceSceneLoad: number = 0;
|
||||
private static _lastTime = -1;
|
||||
|
||||
public static update(currentTime: number) {
|
||||
if (currentTime == -1) {
|
||||
currentTime = Date.now();
|
||||
}
|
||||
if (this._lastTime == -1)
|
||||
this._lastTime = currentTime;
|
||||
|
||||
let dt = 0;
|
||||
if (currentTime == -1) {
|
||||
dt = (currentTime - this._lastTime) / 1000;
|
||||
} else {
|
||||
dt = currentTime;
|
||||
}
|
||||
|
||||
if (dt > this.maxDeltaTime)
|
||||
dt = this.maxDeltaTime;
|
||||
this.totalTime += dt;
|
||||
this.deltaTime = dt * this.timeScale;
|
||||
this.unscaledDeltaTime = dt;
|
||||
this.timeSinceSceneLoad += dt;
|
||||
this.frameCount++;
|
||||
|
||||
this._lastTime = currentTime;
|
||||
}
|
||||
|
||||
public static sceneChanged() {
|
||||
this.timeSinceSceneLoad = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 允许在间隔检查。只应该使用高于delta的间隔值,否则它将始终返回true。
|
||||
* @param interval
|
||||
*/
|
||||
public static checkEvery(interval: number) {
|
||||
// 我们减去了delta,因为timeSinceSceneLoad已经包含了这个update ticks delta
|
||||
return this.timeSinceSceneLoad / interval > (this.timeSinceSceneLoad - this.deltaTime) / interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
class TimeUtils {
|
||||
/**
|
||||
* 计算月份ID
|
||||
* @param d 指定计算日期
|
||||
* @returns 月ID
|
||||
*/
|
||||
public static monthId(d: Date = null): number {
|
||||
d = d ? d : new Date();
|
||||
let y = d.getFullYear();
|
||||
let m = d.getMonth() + 1;
|
||||
let g = m < 10 ? "0" : "";
|
||||
return parseInt(y + g + m);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算日期ID
|
||||
* @param d 指定计算日期
|
||||
* @returns 日期ID
|
||||
*/
|
||||
public static dateId(t: Date = null): number {
|
||||
t = t ? t : new Date();
|
||||
let m: number = t.getMonth() + 1;
|
||||
let a = m < 10 ? "0" : "";
|
||||
let d: number = t.getDate();
|
||||
let b = d < 10 ? "0" : "";
|
||||
return parseInt(t.getFullYear() + a + m + b + d);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算周ID
|
||||
* @param d 指定计算日期
|
||||
* @returns 周ID
|
||||
*/
|
||||
public static weekId(d: Date = null, first: boolean = true): number {
|
||||
d = d ? d : new Date();
|
||||
let c: Date = new Date();
|
||||
c.setTime(d.getTime());
|
||||
c.setDate(1);
|
||||
c.setMonth(0);//当年第一天
|
||||
|
||||
let year: number = c.getFullYear();
|
||||
let firstDay: number = c.getDay();
|
||||
if (firstDay == 0) {
|
||||
firstDay = 7;
|
||||
}
|
||||
let max: boolean = false;
|
||||
if (firstDay <= 4) {
|
||||
max = firstDay > 1;
|
||||
c.setDate(c.getDate() - (firstDay - 1));
|
||||
} else {
|
||||
c.setDate(c.getDate() + 7 - firstDay + 1);
|
||||
}
|
||||
let num: number = this.diffDay(d, c, false);
|
||||
if (num < 0) {
|
||||
c.setDate(1);
|
||||
c.setMonth(0);//当年第一天
|
||||
c.setDate(c.getDate() - 1);
|
||||
return this.weekId(c, false);
|
||||
}
|
||||
let week: number = num / 7;
|
||||
let weekIdx: number = Math.floor(week) + 1;
|
||||
if (weekIdx == 53) {
|
||||
c.setTime(d.getTime());
|
||||
c.setDate(c.getDate() - 1);
|
||||
let endDay: number = c.getDay();
|
||||
if (endDay == 0) {
|
||||
endDay = 7;
|
||||
}
|
||||
if (first && (!max || endDay < 4)) {
|
||||
c.setFullYear(c.getFullYear() + 1);
|
||||
c.setDate(1);
|
||||
c.setMonth(0);//当年第一天
|
||||
return this.weekId(c, false);
|
||||
}
|
||||
}
|
||||
let g: string = weekIdx > 9 ? "" : "0";
|
||||
let s: string = year + "00" + g + weekIdx;//加上00防止和月份ID冲突
|
||||
return parseInt(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算俩日期时间差,如果a比b小,返回负数
|
||||
*/
|
||||
public static diffDay(a: Date, b: Date, fixOne: boolean = false): number {
|
||||
let x = (a.getTime() - b.getTime()) / 86400000;
|
||||
return fixOne ? Math.ceil(x) : Math.floor(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本周一 凌晨时间
|
||||
*/
|
||||
public static getFirstDayOfWeek(d?: Date): Date {
|
||||
d = d ? d : new Date();
|
||||
let day = d.getDay() || 7;
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1 - day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当日凌晨时间
|
||||
*/
|
||||
public static getFirstOfDay(d?: Date): Date {
|
||||
d = d ? d : new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取次日凌晨时间
|
||||
*/
|
||||
public static getNextFirstOfDay(d?: Date): Date {
|
||||
return new Date(this.getFirstOfDay(d).getTime() + 86400000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns 2018-12-12
|
||||
*/
|
||||
public static formatDate(date: Date): string {
|
||||
let y = date.getFullYear();
|
||||
let m: any = date.getMonth() + 1;
|
||||
m = m < 10 ? '0' + m : m;
|
||||
let d: any = date.getDate();
|
||||
d = d < 10 ? ('0' + d) : d;
|
||||
return y + '-' + m + '-' + d;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns 2018-12-12 12:12:12
|
||||
*/
|
||||
public static formatDateTime(date: Date): string {
|
||||
let y = date.getFullYear();
|
||||
let m: any = date.getMonth() + 1;
|
||||
m = m < 10 ? ('0' + m) : m;
|
||||
let d: any = date.getDate();
|
||||
d = d < 10 ? ('0' + d) : d;
|
||||
let h = date.getHours();
|
||||
let i: any = date.getMinutes();
|
||||
i = i < 10 ? ('0' + i) : i;
|
||||
let s: any = date.getSeconds();
|
||||
s = s < 10 ? ('0' + s) : s;
|
||||
return y + '-' + m + '-' + d + ' ' + h + ':' + i + ":" + s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns s 2018-12-12 或者 2018-12-12 12:12:12
|
||||
*/
|
||||
public static parseDate(s: string): Date {
|
||||
let t = Date.parse(s);
|
||||
if (!isNaN(t)) {
|
||||
return new Date(Date.parse(s.replace(/-/g, "/")));
|
||||
} else {
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒数转换为时间形式。
|
||||
* @param time 秒数
|
||||
* @param partition 分隔符
|
||||
* @param showHour 是否显示小时
|
||||
* @return 返回一个以分隔符分割的时, 分, 秒
|
||||
*
|
||||
* 比如: time = 4351; secondToTime(time)返回字符串01:12:31;
|
||||
*/
|
||||
public static secondToTime(time: number = 0, partition: string = ":", showHour: boolean = true): string {
|
||||
let hours: number = Math.floor(time / 3600);
|
||||
let minutes: number = Math.floor(time % 3600 / 60);
|
||||
let seconds: number = Math.floor(time % 3600 % 60);
|
||||
|
||||
let h: string = hours.toString();
|
||||
let m: string = minutes.toString();
|
||||
let s: string = seconds.toString();
|
||||
|
||||
if (hours < 10) h = "0" + h;
|
||||
if (minutes < 10) m = "0" + m;
|
||||
if (seconds < 10) s = "0" + s;
|
||||
|
||||
let timeStr: string;
|
||||
if (showHour)
|
||||
timeStr = h + partition + m + partition + s;
|
||||
else
|
||||
timeStr = m + partition + s;
|
||||
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 时间形式转换为毫秒数。
|
||||
* @param time 以指定分隔符分割的时间字符串
|
||||
* @param partition 分隔符
|
||||
* @return 毫秒数显示的字符串
|
||||
* @throws Error Exception
|
||||
*
|
||||
* 用法1 trace(MillisecondTransform.timeToMillisecond("00:60:00"))
|
||||
* 输出 3600000
|
||||
*
|
||||
*
|
||||
* 用法2 trace(MillisecondTransform.timeToMillisecond("00.60.00","."))
|
||||
* 输出 3600000
|
||||
*/
|
||||
public static timeToMillisecond(time: string, partition: string = ":"): string {
|
||||
let _ary: any[] = time.split(partition);
|
||||
let timeNum: number = 0;
|
||||
let len: number = _ary.length;
|
||||
for (let i: number = 0; i < len; i++) {
|
||||
let n: number = <number>_ary[i];
|
||||
timeNum += n * Math.pow(60, (len - 1 - i));
|
||||
}
|
||||
timeNum *= 1000;
|
||||
return timeNum.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
module es {
|
||||
/**
|
||||
* 开辟一个新线程
|
||||
* 注意:它无法获得主线程中的上下文
|
||||
*/
|
||||
export class WorkerUtils {
|
||||
/** 正在执行的队列 */
|
||||
private static readonly pendingJobs = {};
|
||||
private static jobIdGen = 0;
|
||||
|
||||
/**
|
||||
* 创建一个worker
|
||||
* @param doFunc worker所能做的事情
|
||||
*/
|
||||
public static makeWorker(doFunc: Function) {
|
||||
const worker = new Worker(URL.createObjectURL(new Blob([`(${doFunc.toString()})()`])));
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
public static workerMessage(worker: Worker) {
|
||||
worker.onmessage = ({ data: { result, jobId } }) => {
|
||||
if (typeof this.pendingJobs[jobId] == 'function')
|
||||
this.pendingJobs[jobId](result);
|
||||
|
||||
delete this.pendingJobs[jobId];
|
||||
};
|
||||
|
||||
return (...message: any[]) => {
|
||||
return new Promise(resolve => {
|
||||
const jobId = this.jobIdGen++;
|
||||
this.pendingJobs[jobId] = resolve;
|
||||
worker.postMessage({ jobId, message });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,475 +0,0 @@
|
||||
module es {
|
||||
export class Color {
|
||||
/**
|
||||
* 红色通道
|
||||
*/
|
||||
public r: number;
|
||||
/**
|
||||
* 绿色通道
|
||||
*/
|
||||
public g: number;
|
||||
/**
|
||||
* 蓝色通道
|
||||
*/
|
||||
public b: number;
|
||||
/**
|
||||
* 透明度通道 (仅0-1之间)
|
||||
*/
|
||||
public a: number;
|
||||
|
||||
/**
|
||||
* 色调
|
||||
*/
|
||||
public h: number;
|
||||
/**
|
||||
* 饱和
|
||||
*/
|
||||
public s: number;
|
||||
/**
|
||||
* 亮度
|
||||
*/
|
||||
public l: number;
|
||||
|
||||
/**
|
||||
* 从 r, g, b, a 创建一个新的 Color 实例
|
||||
*
|
||||
* @param r 颜色的红色分量 (0-255)
|
||||
* @param g 颜色的绿色成分 (0-255)
|
||||
* @param b 颜色的蓝色分量 (0-255)
|
||||
* @param a 颜色的 alpha 分量 (0-1.0)
|
||||
*/
|
||||
constructor(r: number, g: number, b: number, a?: number) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.a = a != null ? a : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 r, g, b, a 创建一个新的 Color 实例
|
||||
*
|
||||
* @param r 颜色的红色分量 (0-255)
|
||||
* @param g 颜色的绿色成分 (0-255)
|
||||
* @param b 颜色的蓝色分量 (0-255)
|
||||
* @param a 颜色的 alpha 分量 (0-1.0)
|
||||
*/
|
||||
public static fromRGB(r: number, g: number, b: number, a?: number): Color {
|
||||
return new Color(r, g, b, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从十六进制字符串创建一个新的 Color 实例
|
||||
*
|
||||
* @param hex #ffffff 形式的 CSS 颜色字符串,alpha 组件是可选的
|
||||
*/
|
||||
public static createFromHex(hex: string): Color {
|
||||
const color = new Color(1, 1, 1);
|
||||
color.fromHex(hex);
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 hsl 值创建一个新的 Color 实例
|
||||
*
|
||||
* @param h 色调表示 [0-1]
|
||||
* @param s 饱和度表示为 [0-1]
|
||||
* @param l 亮度表示 [0-1]
|
||||
* @param a 透明度表示 [0-1]
|
||||
*/
|
||||
public static fromHSL(
|
||||
h: number,
|
||||
s: number,
|
||||
l: number,
|
||||
a: number = 1.0
|
||||
): Color {
|
||||
const temp = new HSLColor(h, s, l, a);
|
||||
return temp.toRGBA();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色调亮指定的量
|
||||
*
|
||||
* @param factor
|
||||
*/
|
||||
public lighten(factor: number = 0.1): Color {
|
||||
const temp = HSLColor.fromRGBA(this.r, this.g, this.b, this.a);
|
||||
temp.l += temp.l * factor;
|
||||
return temp.toRGBA();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色变暗指定的量
|
||||
*
|
||||
* @param factor
|
||||
*/
|
||||
public darken(factor: number = 0.1): Color {
|
||||
const temp = HSLColor.fromRGBA(this.r, this.g, this.b, this.a);
|
||||
temp.l -= temp.l * factor;
|
||||
return temp.toRGBA();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使当前颜色饱和指定的量
|
||||
*
|
||||
* @param factor
|
||||
*/
|
||||
public saturate(factor: number = 0.1): Color {
|
||||
const temp = HSLColor.fromRGBA(this.r, this.g, this.b, this.a);
|
||||
temp.s += temp.s * factor;
|
||||
return temp.toRGBA();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定量降低当前颜色的饱和度
|
||||
*
|
||||
* @param factor
|
||||
*/
|
||||
public desaturate(factor: number = 0.1): Color {
|
||||
const temp = HSLColor.fromRGBA(this.r, this.g, this.b, this.a);
|
||||
temp.s -= temp.s * factor;
|
||||
return temp.toRGBA();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一种颜色乘以另一种颜色,得到更深的颜色
|
||||
*
|
||||
* @param color
|
||||
*/
|
||||
public mulitiply(color: Color): Color {
|
||||
const newR = (((color.r / 255) * this.r) / 255) * 255;
|
||||
const newG = (((color.g / 255) * this.g) / 255) * 255;
|
||||
const newB = (((color.b / 255) * this.b) / 255) * 255;
|
||||
const newA = color.a * this.a;
|
||||
return new Color(newR, newG, newB, newA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选另一种颜色,导致颜色较浅
|
||||
*
|
||||
* @param color
|
||||
*/
|
||||
public screen(color: Color): Color {
|
||||
const color1 = color.invert();
|
||||
const color2 = color.invert();
|
||||
return color1.mulitiply(color2).invert();
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转当前颜色
|
||||
*/
|
||||
public invert(): Color {
|
||||
return new Color(255 - this.r, 255 - this.g, 255 - this.b, 1.0 - this.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色与另一个颜色平均
|
||||
*
|
||||
* @param color
|
||||
*/
|
||||
public average(color: Color): Color {
|
||||
const newR = (color.r + this.r) / 2;
|
||||
const newG = (color.g + this.g) / 2;
|
||||
const newB = (color.b + this.b) / 2;
|
||||
const newA = (color.a + this.a) / 2;
|
||||
return new Color(newR, newG, newB, newA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回颜色的 CSS 字符串表示形式。
|
||||
*
|
||||
* @param format
|
||||
*/
|
||||
public toString(format: 'rgb' | 'hsl' | 'hex' = 'rgb') {
|
||||
switch (format) {
|
||||
case 'rgb':
|
||||
return this.toRGBA();
|
||||
case 'hsl':
|
||||
return this.toHSLA();
|
||||
case 'hex':
|
||||
return this.toHex();
|
||||
default:
|
||||
throw new Error('Invalid Color format');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回颜色分量的十六进制值
|
||||
* @param c
|
||||
* @see https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
|
||||
*/
|
||||
private _componentToHex(c: number) {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
}
|
||||
|
||||
/**
|
||||
*返回颜色的十六进制表示
|
||||
*/
|
||||
public toHex(): string {
|
||||
return (
|
||||
'#' +
|
||||
this._componentToHex(this.r) +
|
||||
this._componentToHex(this.g) +
|
||||
this._componentToHex(this.b) +
|
||||
this._componentToHex(this.a)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从十六进制字符串设置颜色
|
||||
*
|
||||
* @param hex #ffffff 形式的 CSS 颜色字符串,alpha 组件是可选的
|
||||
*/
|
||||
public fromHex(hex: string) {
|
||||
const hexRegEx: RegExp = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i;
|
||||
const match = hex.match(hexRegEx);
|
||||
if (match) {
|
||||
const r = parseInt(match[1], 16);
|
||||
const g = parseInt(match[2], 16);
|
||||
const b = parseInt(match[3], 16);
|
||||
let a = 1;
|
||||
if (match[4]) {
|
||||
a = parseInt(match[4], 16) / 255;
|
||||
}
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.a = a;
|
||||
} else {
|
||||
throw new Error('Invalid hex string: ' + hex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回颜色的 RGBA 表示
|
||||
*/
|
||||
public toRGBA() {
|
||||
const result =
|
||||
String(this.r.toFixed(0)) +
|
||||
', ' +
|
||||
String(this.g.toFixed(0)) +
|
||||
', ' +
|
||||
String(this.b.toFixed(0));
|
||||
if (this.a !== undefined || this.a != null) {
|
||||
return 'rgba(' + result + ', ' + String(this.a) + ')';
|
||||
}
|
||||
return 'rgb(' + result + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回颜色的 HSLA 表示
|
||||
*/
|
||||
public toHSLA() {
|
||||
return HSLColor.fromRGBA(this.r, this.g, this.b, this.a).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回颜色的 CSS 字符串表示形式
|
||||
*/
|
||||
public fillStyle() {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前颜色的克隆
|
||||
*/
|
||||
public clone(): Color {
|
||||
return new Color(this.r, this.g, this.b, this.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Black (#000000)
|
||||
*/
|
||||
public static Black: Color = Color.createFromHex('#000000');
|
||||
|
||||
/**
|
||||
* White (#FFFFFF)
|
||||
*/
|
||||
public static White: Color = Color.createFromHex('#FFFFFF');
|
||||
|
||||
/**
|
||||
* Gray (#808080)
|
||||
*/
|
||||
public static Gray: Color = Color.createFromHex('#808080');
|
||||
|
||||
/**
|
||||
* Light gray (#D3D3D3)
|
||||
*/
|
||||
public static LightGray: Color = Color.createFromHex('#D3D3D3');
|
||||
|
||||
/**
|
||||
* Dark gray (#A9A9A9)
|
||||
*/
|
||||
public static DarkGray: Color = Color.createFromHex('#A9A9A9');
|
||||
|
||||
/**
|
||||
* Yellow (#FFFF00)
|
||||
*/
|
||||
public static Yellow: Color = Color.createFromHex('#FFFF00');
|
||||
|
||||
/**
|
||||
* Orange (#FFA500)
|
||||
*/
|
||||
public static Orange: Color = Color.createFromHex('#FFA500');
|
||||
|
||||
/**
|
||||
* Red (#FF0000)
|
||||
*/
|
||||
public static Red: Color = Color.createFromHex('#FF0000');
|
||||
|
||||
/**
|
||||
* Vermillion (#FF5B31)
|
||||
*/
|
||||
public static Vermillion: Color = Color.createFromHex('#FF5B31');
|
||||
|
||||
/**
|
||||
* Rose (#FF007F)
|
||||
*/
|
||||
public static Rose: Color = Color.createFromHex('#FF007F');
|
||||
|
||||
/**
|
||||
* Magenta (#FF00FF)
|
||||
*/
|
||||
public static Magenta: Color = Color.createFromHex('#FF00FF');
|
||||
|
||||
/**
|
||||
* Violet (#7F00FF)
|
||||
*/
|
||||
public static Violet: Color = Color.createFromHex('#7F00FF');
|
||||
|
||||
/**
|
||||
* Blue (#0000FF)
|
||||
*/
|
||||
public static Blue: Color = Color.createFromHex('#0000FF');
|
||||
|
||||
/**
|
||||
* Azure (#007FFF)
|
||||
*/
|
||||
public static Azure: Color = Color.createFromHex('#007FFF');
|
||||
|
||||
/**
|
||||
* Cyan (#00FFFF)
|
||||
*/
|
||||
public static Cyan: Color = Color.createFromHex('#00FFFF');
|
||||
|
||||
/**
|
||||
* Viridian (#59978F)
|
||||
*/
|
||||
public static Viridian: Color = Color.createFromHex('#59978F');
|
||||
|
||||
/**
|
||||
* Green (#00FF00)
|
||||
*/
|
||||
public static Green: Color = Color.createFromHex('#00FF00');
|
||||
|
||||
/**
|
||||
* Chartreuse (#7FFF00)
|
||||
*/
|
||||
public static Chartreuse: Color = Color.createFromHex('#7FFF00');
|
||||
|
||||
/**
|
||||
* Transparent (#FFFFFF00)
|
||||
*/
|
||||
public static Transparent: Color = Color.createFromHex('#FFFFFF00');
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部 HSL 颜色表示
|
||||
*
|
||||
* http://en.wikipedia.org/wiki/HSL_and_HSV
|
||||
* http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
|
||||
*/
|
||||
class HSLColor {
|
||||
constructor(
|
||||
public h: number,
|
||||
public s: number,
|
||||
public l: number,
|
||||
public a: number
|
||||
) { }
|
||||
|
||||
public static hue2rgb(p: number, q: number, t: number): number {
|
||||
if (t < 0) {
|
||||
t += 1;
|
||||
}
|
||||
if (t > 1) {
|
||||
t -= 1;
|
||||
}
|
||||
if (t < 1 / 6) {
|
||||
return p + (q - p) * 6 * t;
|
||||
}
|
||||
if (t < 1 / 2) {
|
||||
return q;
|
||||
}
|
||||
if (t < 2 / 3) {
|
||||
return p + (q - p) * (2 / 3 - t) * 6;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
public static fromRGBA(
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number
|
||||
): HSLColor {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = (max + min) / 2;
|
||||
let s = h;
|
||||
const l = h;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return new HSLColor(h, s, l, a);
|
||||
}
|
||||
|
||||
public toRGBA(): Color {
|
||||
let r: number;
|
||||
let g: number;
|
||||
let b: number;
|
||||
|
||||
if (this.s === 0) {
|
||||
r = g = b = this.l; // achromatic
|
||||
} else {
|
||||
const q =
|
||||
this.l < 0.5
|
||||
? this.l * (1 + this.s)
|
||||
: this.l + this.s - this.l * this.s;
|
||||
const p = 2 * this.l - q;
|
||||
r = HSLColor.hue2rgb(p, q, this.h + 1 / 3);
|
||||
g = HSLColor.hue2rgb(p, q, this.h);
|
||||
b = HSLColor.hue2rgb(p, q, this.h - 1 / 3);
|
||||
}
|
||||
|
||||
return new Color(r * 255, g * 255, b * 255, this.a);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
const h = this.h.toFixed(0);
|
||||
const s = this.s.toFixed(0);
|
||||
const l = this.l.toFixed(0);
|
||||
const a = this.a.toFixed(0);
|
||||
return `hsla(${h}, ${s}, ${l}, ${a})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
module es {
|
||||
export interface IBatcher {
|
||||
begin(cam: ICamera);
|
||||
end();
|
||||
drawPoints(points: Vector2[], color: Color, thickness?: number);
|
||||
drawPolygon(poisition: Vector2, points: Vector2[], color: Color, closePoly: boolean, thickness?: number);
|
||||
drawHollowRect(x: number, y: number, width: number, height: number, color: Color, thickness?: number);
|
||||
drawCircle(position: Vector2, raidus: number, color: Color, thickness?: number);
|
||||
drawCircleLow(position: es.Vector2, radius: number, color: Color, thickness?: number, resolution?: number);
|
||||
drawRect(x: number, y: number, width: number, height: number, color: Color);
|
||||
drawLine(start: Vector2, end: Vector2, color: Color, thickness: number);
|
||||
drawPixel(position: Vector2, color: Color, size?: number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module es {
|
||||
export interface ICamera extends Component {
|
||||
bounds: Rectangle;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module es {
|
||||
export class Graphics {
|
||||
public static instance: Graphics;
|
||||
public batcher: IBatcher;
|
||||
|
||||
constructor(batcher: IBatcher) {
|
||||
this.batcher = batcher;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user