refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

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

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

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

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

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

* docs: update README to focus on framework over engine

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

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

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

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

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

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

* fix: update network tsconfig references to new paths

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

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

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,51 @@
/**
* Physics 2D Components Module (Lightweight)
* 2D 物理组件模块(轻量级)
*
* 仅注册组件,不包含 WASM 依赖
* 用于编辑器中的组件序列化/反序列化
*/
import type { IComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule } from '@esengine/engine-core';
// Components (no WASM dependency)
import { Rigidbody2DComponent } from './components/Rigidbody2DComponent';
import { BoxCollider2DComponent } from './components/BoxCollider2DComponent';
import { CircleCollider2DComponent } from './components/CircleCollider2DComponent';
import { CapsuleCollider2DComponent } from './components/CapsuleCollider2DComponent';
import { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent';
/**
* Physics 2D Components Module (Lightweight)
* 2D 物理组件模块(轻量级)
*
* 仅实现组件注册,不包含系统创建和 WASM 初始化
* 用于编辑器场景序列化
*/
export class Physics2DComponentsModule implements IRuntimeModule {
/**
* 注册组件到 ComponentRegistry
* Register components to ComponentRegistry
*/
registerComponents(registry: IComponentRegistry): void {
registry.register(Rigidbody2DComponent);
registry.register(BoxCollider2DComponent);
registry.register(CircleCollider2DComponent);
registry.register(CapsuleCollider2DComponent);
registry.register(PolygonCollider2DComponent);
}
/**
* 不创建系统(完整运行时模块负责)
*/
createSystems(): void {
// No-op: Systems are created by the full runtime module
}
}
/**
* 默认导出模块实例
*/
export const physics2DComponentsModule = new Physics2DComponentsModule();
export default physics2DComponentsModule;

View File

@@ -0,0 +1,47 @@
/**
* Physics Editor Plugin
*
* 编辑器版本的物理插件,不包含 WASM 依赖。
* Editor version of physics plugin, without WASM dependencies.
*
* 使用轻量级 Physics2DComponentsModule 注册组件,
* 使场景中的物理组件可以正确序列化/反序列化。
* Uses lightweight Physics2DComponentsModule to register components,
* enabling proper serialization/deserialization of physics components in scenes.
*/
import type { IRuntimePlugin, ModuleManifest } from '@esengine/engine-core';
import { Physics2DComponentsModule } from './Physics2DComponentsModule';
const manifest: ModuleManifest = {
id: '@esengine/physics-rapier2d',
name: '@esengine/physics-rapier2d',
displayName: 'Physics 2D',
version: '1.0.0',
description: 'Deterministic 2D physics with Rapier2D',
category: 'Physics',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
canContainContent: false,
requiresWasm: true,
dependencies: ['engine-core'],
exports: {
components: ['Rigidbody2DComponent', 'BoxCollider2DComponent', 'CircleCollider2DComponent'],
systems: ['PhysicsSystem']
}
};
/**
* 编辑器物理插件(轻量级运行时模块)
* Editor physics plugin (lightweight runtime module)
*
* 使用 Physics2DComponentsModule 注册组件,用于场景反序列化。
* 不包含 WASM 依赖,不创建物理系统。
* Uses Physics2DComponentsModule for component registration (scene deserialization).
* No WASM dependency, no physics system creation.
*/
export const Physics2DPlugin: IRuntimePlugin = {
manifest,
runtimeModule: new Physics2DComponentsModule()
};

View File

@@ -0,0 +1,207 @@
/**
* 物理运行时模块
*
* 提供 Rapier2D 物理引擎的 ECS 集成
*/
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { WasmLibraryLoaderFactory } from '@esengine/platform-common';
import type * as RAPIER from '@esengine/rapier2d';
import { Rigidbody2DComponent } from './components/Rigidbody2DComponent';
import { BoxCollider2DComponent } from './components/BoxCollider2DComponent';
import { CircleCollider2DComponent } from './components/CircleCollider2DComponent';
import { CapsuleCollider2DComponent } from './components/CapsuleCollider2DComponent';
import { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent';
import { Physics2DSystem } from './systems/Physics2DSystem';
import { Physics2DService } from './services/Physics2DService';
import {
Physics2DQueryToken,
Physics2DSystemToken,
Physics2DWorldToken,
PhysicsConfigToken,
CollisionLayerConfigToken,
type IPhysics2DQuery,
type PhysicsConfig
} from './tokens';
import { CollisionLayerConfig } from './services/CollisionLayerConfig';
// 注册 Rapier2D 加载器
import './loaders';
// 重新导出 tokens 和类型 | Re-export tokens and types
export {
Physics2DQueryToken,
Physics2DSystemToken,
Physics2DWorldToken,
PhysicsConfigToken,
CollisionLayerConfigToken,
type IPhysics2DQuery,
type PhysicsConfig
} from './tokens';
/**
* 物理运行时模块
*
* 负责:
* 1. 加载并初始化 Rapier2D WASM 模块(跨平台)
* 2. 注册物理组件
* 3. 注册物理服务
* 4. 创建物理系统
*
* @example
* ```typescript
* // 作为插件使用
* runtimePluginManager.register(PhysicsPlugin);
* runtimePluginManager.enable('@esengine/physics-rapier2d');
*
* // 插件会自动:
* // 1. 检测平台并选择合适的加载器
* // 2. 安装必要的 polyfills如微信小游戏的 TextDecoder
* // 3. 加载 Rapier2D WASM 模块
* // 4. 注册物理组件和系统
* ```
*/
class PhysicsRuntimeModule implements IRuntimeModule {
private _rapierModule: typeof RAPIER | null = null;
private _physicsSystem: Physics2DSystem | null = null;
/**
* 初始化物理模块
*
* 使用平台适配的加载器加载 Rapier2D
*/
async onInitialize(): Promise<void> {
// 使用工厂创建平台对应的加载器
const loader = WasmLibraryLoaderFactory.createLoader<typeof RAPIER>('rapier2d');
// 获取平台信息
const platformInfo = loader.getPlatformInfo();
console.log(`[Physics] 平台: ${platformInfo.type}`);
console.log(`[Physics] WASM 支持: ${platformInfo.supportsWasm}`);
if (platformInfo.needsPolyfills.length > 0) {
console.log(`[Physics] 需要 Polyfills: ${platformInfo.needsPolyfills.join(', ')}`);
}
// 检查平台支持
if (!loader.isSupported()) {
throw new Error(
`[Physics] 当前平台不支持 Rapier2D: ${platformInfo.type}` +
'请检查 WebAssembly 支持情况。'
);
}
// 加载 Rapier2D
this._rapierModule = await loader.load();
console.log('[Physics] Rapier2D 加载完成');
}
/**
* 注册物理组件
* Register physics components
*
* @param registry - 组件注册表 | Component registry
*/
registerComponents(registry: IComponentRegistry): void {
registry.register(Rigidbody2DComponent);
registry.register(BoxCollider2DComponent);
registry.register(CircleCollider2DComponent);
registry.register(CapsuleCollider2DComponent);
registry.register(PolygonCollider2DComponent);
}
/**
* 注册物理服务
*
* @param services - 服务容器
*/
registerServices(services: ServiceContainer): void {
services.registerSingleton(Physics2DService);
}
/**
* 创建物理系统
*
* @param scene - 目标场景
* @param context - 系统上下文
*/
createSystems(scene: IScene, context: SystemContext): void {
// 从服务注册表获取配置 | Get config from service registry
const physicsConfig = context.services.get(PhysicsConfigToken);
const physicsSystem = new Physics2DSystem({
physics: physicsConfig,
updateOrder: -1000
});
scene.addSystem(physicsSystem);
this._physicsSystem = physicsSystem;
if (this._rapierModule) {
physicsSystem.initializeWithRapier(this._rapierModule);
}
// 注册服务 | Register services
context.services.register(Physics2DSystemToken, physicsSystem);
context.services.register(Physics2DWorldToken, physicsSystem.world);
context.services.register(Physics2DQueryToken, physicsSystem);
context.services.register(CollisionLayerConfigToken, CollisionLayerConfig.getInstance());
}
/**
* 销毁物理模块
*/
onDestroy(): void {
this._physicsSystem = null;
this._rapierModule = null;
}
/**
* 获取 Rapier 模块
*
* @returns Rapier 模块,如果未加载则返回 null
*/
getRapierModule(): typeof RAPIER | null {
return this._rapierModule;
}
/**
* 获取物理系统
*
* @returns 物理系统,如果未创建则返回 null
*/
getPhysicsSystem(): Physics2DSystem | null {
return this._physicsSystem;
}
}
/**
* 模块清单
*/
const manifest: ModuleManifest = {
id: 'physics-rapier2d',
name: '@esengine/physics-rapier2d',
displayName: 'Physics 2D (Rapier)',
version: '1.0.0',
description: '基于 Rapier2D 的确定性 2D 物理引擎(支持跨平台)',
category: 'Physics',
icon: 'Atom',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
dependencies: ['core', 'math'],
exports: { components: ['RigidBody2D'] },
requiresWasm: true
};
/**
* 物理插件
*/
export const PhysicsPlugin: IRuntimePlugin = {
manifest,
runtimeModule: new PhysicsRuntimeModule()
};
export { PhysicsRuntimeModule };

View File

@@ -0,0 +1,103 @@
/**
* BoxCollider2D Component
* 2D 矩形碰撞体组件
*/
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import { Collider2DBase } from './Collider2DBase';
/**
* 2D 矩形碰撞体
*
* 用于创建矩形形状的碰撞体。
*
* @example
* ```typescript
* const entity = scene.createEntity('Box');
* const collider = entity.addComponent(BoxCollider2DComponent);
* collider.width = 2;
* collider.height = 1;
* ```
*/
@ECSComponent('BoxCollider2D')
@Serializable({ version: 1, typeId: 'BoxCollider2D' })
export class BoxCollider2DComponent extends Collider2DBase {
private _width: number = 10;
private _height: number = 10;
/**
* 矩形宽度半宽度的2倍
*/
@Serialize()
@Property({ type: 'number', label: 'Width', min: 0.01, step: 0.1 })
public get width(): number {
return this._width;
}
public set width(value: number) {
if (this._width !== value) {
this._width = value;
this._needsRebuild = true;
}
}
/**
* 矩形高度半高度的2倍
*/
@Serialize()
@Property({ type: 'number', label: 'Height', min: 0.01, step: 0.1 })
public get height(): number {
return this._height;
}
public set height(value: number) {
if (this._height !== value) {
this._height = value;
this._needsRebuild = true;
}
}
/**
* 获取半宽度
*/
public get halfWidth(): number {
return this.width / 2;
}
/**
* 获取半高度
*/
public get halfHeight(): number {
return this.height / 2;
}
public override getShapeType(): string {
return 'box';
}
public override calculateArea(): number {
return this.width * this.height;
}
public override calculateAABB(): { min: IVector2; max: IVector2 } {
const hw = this.halfWidth;
const hh = this.halfHeight;
// 简化版本,不考虑旋转偏移
return {
min: { x: this.offset.x - hw, y: this.offset.y - hh },
max: { x: this.offset.x + hw, y: this.offset.y + hh }
};
}
/**
* 设置尺寸
* @param width 宽度
* @param height 高度
*/
public setSize(width: number, height: number): void {
this.width = width;
this.height = height;
}
}

View File

@@ -0,0 +1,146 @@
/**
* CapsuleCollider2D Component
* 2D 胶囊碰撞体组件
*/
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import { Collider2DBase } from './Collider2DBase';
/**
* 胶囊方向
*/
export enum CapsuleDirection2D {
/** 垂直方向(默认) */
Vertical = 0,
/** 水平方向 */
Horizontal = 1
}
/**
* 2D 胶囊碰撞体
*
* 胶囊由两个半圆和一个矩形组成。
* 常用于角色碰撞体。
*
* @example
* ```typescript
* const entity = scene.createEntity('Character');
* const collider = entity.addComponent(CapsuleCollider2DComponent);
* collider.radius = 0.25;
* collider.height = 1;
* collider.direction = CapsuleDirection2D.Vertical;
* ```
*/
@ECSComponent('CapsuleCollider2D')
@Serializable({ version: 1, typeId: 'CapsuleCollider2D' })
export class CapsuleCollider2DComponent extends Collider2DBase {
private _radius: number = 3;
private _height: number = 10;
private _direction: CapsuleDirection2D = CapsuleDirection2D.Vertical;
/**
* 胶囊半径
*/
@Serialize()
@Property({ type: 'number', label: 'Radius', min: 0.01, step: 0.1 })
public get radius(): number {
return this._radius;
}
public set radius(value: number) {
if (this._radius !== value) {
this._radius = value;
this._needsRebuild = true;
}
}
/**
* 胶囊总高度(包括两端的半圆)
*/
@Serialize()
@Property({ type: 'number', label: 'Height', min: 0.01, step: 0.1 })
public get height(): number {
return this._height;
}
public set height(value: number) {
if (this._height !== value) {
this._height = value;
this._needsRebuild = true;
}
}
/**
* 胶囊方向
*/
@Serialize()
@Property({
type: 'enum',
label: 'Direction',
options: [
{ label: 'Vertical', value: 0 },
{ label: 'Horizontal', value: 1 }
]
})
public get direction(): CapsuleDirection2D {
return this._direction;
}
public set direction(value: CapsuleDirection2D) {
if (this._direction !== value) {
this._direction = value;
this._needsRebuild = true;
}
}
/**
* 获取半高度(中间矩形部分的一半)
*/
public get halfHeight(): number {
return Math.max(0, (this.height - this.radius * 2) / 2);
}
public override getShapeType(): string {
return 'capsule';
}
public override calculateArea(): number {
// 胶囊面积 = 矩形面积 + 圆面积
const rectArea = this.radius * 2 * this.halfHeight * 2;
const circleArea = Math.PI * this.radius * this.radius;
return rectArea + circleArea;
}
public override calculateAABB(): { min: IVector2; max: IVector2 } {
if (this.direction === CapsuleDirection2D.Vertical) {
return {
min: { x: this.offset.x - this.radius, y: this.offset.y - this.height / 2 },
max: { x: this.offset.x + this.radius, y: this.offset.y + this.height / 2 }
};
} else {
return {
min: { x: this.offset.x - this.height / 2, y: this.offset.y - this.radius },
max: { x: this.offset.x + this.height / 2, y: this.offset.y + this.radius }
};
}
}
/**
* 设置胶囊尺寸
* @param radius 半径
* @param height 总高度
*/
public setSize(radius: number, height: number): void {
this.radius = radius;
this.height = height;
}
/**
* 设置方向
* @param direction 方向
*/
public setDirection(direction: CapsuleDirection2D): void {
this.direction = direction;
}
}

View File

@@ -0,0 +1,65 @@
/**
* CircleCollider2D Component
* 2D 圆形碰撞体组件
*/
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import { Collider2DBase } from './Collider2DBase';
/**
* 2D 圆形碰撞体
*
* 用于创建圆形形状的碰撞体。
*
* @example
* ```typescript
* const entity = scene.createEntity('Ball');
* const collider = entity.addComponent(CircleCollider2DComponent);
* collider.radius = 0.5;
* ```
*/
@ECSComponent('CircleCollider2D')
@Serializable({ version: 1, typeId: 'CircleCollider2D' })
export class CircleCollider2DComponent extends Collider2DBase {
private _radius: number = 5;
/**
* 圆的半径
*/
@Serialize()
@Property({ type: 'number', label: 'Radius', min: 0.01, step: 0.1 })
public get radius(): number {
return this._radius;
}
public set radius(value: number) {
if (this._radius !== value) {
this._radius = value;
this._needsRebuild = true;
}
}
public override getShapeType(): string {
return 'circle';
}
public override calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
public override calculateAABB(): { min: IVector2; max: IVector2 } {
return {
min: { x: this.offset.x - this.radius, y: this.offset.y - this.radius },
max: { x: this.offset.x + this.radius, y: this.offset.y + this.radius }
};
}
/**
* 设置半径
* @param radius 半径
*/
public setRadius(radius: number): void {
this.radius = radius;
}
}

View File

@@ -0,0 +1,187 @@
/**
* Collider2D Base Component
* 2D 碰撞体基类组件
*/
import { Component, Property, Serialize } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import { CollisionLayer2D } from '../types/Physics2DTypes';
/**
* 2D 碰撞体基类
*
* 定义了所有 2D 碰撞体的共同属性和接口。
* 具体的碰撞体形状由子类实现。
*/
export abstract class Collider2DBase extends Component {
// ==================== 物理材质属性 ====================
/**
* 摩擦系数 [0, 1]
* 0 = 完全光滑1 = 最大摩擦
*/
@Serialize()
@Property({ type: 'number', label: 'Friction', min: 0, max: 1, step: 0.01 })
public friction: number = 0.5;
/**
* 弹性系数(恢复系数)[0, 1]
* 0 = 完全非弹性碰撞1 = 完全弹性碰撞
*/
@Serialize()
@Property({ type: 'number', label: 'Restitution', min: 0, max: 1, step: 0.01 })
public restitution: number = 0;
/**
* 密度 (kg/m²)
* 用于计算质量(与碰撞体面积相乘)
*/
@Serialize()
@Property({ type: 'number', label: 'Density', min: 0.001, step: 0.1 })
public density: number = 1;
// ==================== 碰撞过滤 ====================
/**
* 是否为触发器
* 触发器不产生物理碰撞响应,只触发事件
*/
@Serialize()
@Property({ type: 'boolean', label: 'Is Trigger' })
public isTrigger: boolean = false;
/**
* 碰撞层(该碰撞体所在的层)
* 使用位掩码,可以属于多个层
*/
@Serialize()
@Property({ type: 'collisionLayer', label: 'Collision Layer' })
public collisionLayer: number = CollisionLayer2D.Default;
/**
* 碰撞掩码(该碰撞体可以与哪些层碰撞)
* 使用位掩码
*/
@Serialize()
@Property({ type: 'collisionMask', label: 'Collision Mask' })
public collisionMask: number = CollisionLayer2D.All;
// ==================== 偏移 ====================
/**
* 相对于实体 Transform 的位置偏移
*/
@Serialize()
@Property({ type: 'vector2', label: 'Offset' })
public offset: IVector2 = { x: 0, y: 0 };
/**
* 相对于实体 Transform 的旋转偏移(度)
*/
@Serialize()
@Property({ type: 'number', label: 'Rotation Offset', min: -180, max: 180, step: 1 })
public rotationOffset: number = 0;
// ==================== 内部状态 ====================
/**
* Rapier 碰撞体句柄
* @internal
*/
public _colliderHandle: number | null = null;
/**
* 关联的刚体实体 ID如果有
* @internal
*/
public _attachedBodyEntityId: number | null = null;
/**
* 是否需要重建碰撞体
* @internal
*/
public _needsRebuild: boolean = false;
// ==================== 抽象方法 ====================
/**
* 获取碰撞体形状类型名称
*/
public abstract getShapeType(): string;
/**
* 计算碰撞体的面积(用于质量计算)
*/
public abstract calculateArea(): number;
/**
* 计算碰撞体的 AABB轴对齐包围盒
*/
public abstract calculateAABB(): { min: IVector2; max: IVector2 };
// ==================== API 方法 ====================
/**
* 设置碰撞层
* @param layer 层标识
*/
public setLayer(layer: CollisionLayer2D): void {
this.collisionLayer = layer;
this._needsRebuild = true;
}
/**
* 添加碰撞层
* @param layer 层标识
*/
public addLayer(layer: CollisionLayer2D): void {
this.collisionLayer |= layer;
this._needsRebuild = true;
}
/**
* 移除碰撞层
* @param layer 层标识
*/
public removeLayer(layer: CollisionLayer2D): void {
this.collisionLayer &= ~layer;
this._needsRebuild = true;
}
/**
* 检查是否在指定层
* @param layer 层标识
*/
public isInLayer(layer: CollisionLayer2D): boolean {
return (this.collisionLayer & layer) !== 0;
}
/**
* 设置碰撞掩码
* @param mask 掩码值
*/
public setCollisionMask(mask: number): void {
this.collisionMask = mask;
this._needsRebuild = true;
}
/**
* 检查是否可以与指定层碰撞
* @param layer 层标识
*/
public canCollideWith(layer: CollisionLayer2D): boolean {
return (this.collisionMask & layer) !== 0;
}
/**
* 标记需要重建
*/
public markNeedsRebuild(): void {
this._needsRebuild = true;
}
public override onRemovedFromEntity(): void {
this._colliderHandle = null;
this._attachedBodyEntityId = null;
}
}

View File

@@ -0,0 +1,154 @@
/**
* PolygonCollider2D Component
* 2D 多边形碰撞体组件
*/
import { Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import { Collider2DBase } from './Collider2DBase';
/**
* 2D 多边形碰撞体
*
* 用于创建任意凸多边形形状的碰撞体。
* 注意Rapier 只支持凸多边形,非凸多边形需要分解。
*
* @example
* ```typescript
* const entity = scene.createEntity('Triangle');
* const collider = entity.addComponent(PolygonCollider2DComponent);
* collider.setVertices([
* { x: 0, y: 1 },
* { x: -1, y: -1 },
* { x: 1, y: -1 }
* ]);
* ```
*/
@ECSComponent('PolygonCollider2D')
@Serializable({ version: 1, typeId: 'PolygonCollider2D' })
export class PolygonCollider2DComponent extends Collider2DBase {
/**
* 多边形顶点(局部坐标,逆时针顺序)
* 最少3个最多不超过引擎限制通常是 8-16 个)
*/
@Serialize()
public vertices: IVector2[] = [
{ x: -5, y: -5 },
{ x: 5, y: -5 },
{ x: 5, y: 5 },
{ x: -5, y: 5 }
];
public override getShapeType(): string {
return 'polygon';
}
public override calculateArea(): number {
// 使用鞋带公式计算多边形面积
if (this.vertices.length < 3) return 0;
let area = 0;
const n = this.vertices.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
area += this.vertices[i].x * this.vertices[j].y;
area -= this.vertices[j].x * this.vertices[i].y;
}
return Math.abs(area) / 2;
}
public override calculateAABB(): { min: IVector2; max: IVector2 } {
if (this.vertices.length === 0) {
return {
min: { x: this.offset.x, y: this.offset.y },
max: { x: this.offset.x, y: this.offset.y }
};
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const v of this.vertices) {
minX = Math.min(minX, v.x);
minY = Math.min(minY, v.y);
maxX = Math.max(maxX, v.x);
maxY = Math.max(maxY, v.y);
}
return {
min: { x: this.offset.x + minX, y: this.offset.y + minY },
max: { x: this.offset.x + maxX, y: this.offset.y + maxY }
};
}
/**
* 设置顶点
* @param vertices 顶点数组(逆时针顺序)
*/
public setVertices(vertices: IVector2[]): void {
if (vertices.length < 3) {
console.warn('PolygonCollider2D: 至少需要3个顶点');
return;
}
this.vertices = vertices.map((v) => ({ x: v.x, y: v.y }));
this._needsRebuild = true;
}
/**
* 创建正多边形
* @param sides 边数至少3
* @param radius 外接圆半径
*/
public setRegularPolygon(sides: number, radius: number): void {
if (sides < 3) {
console.warn('PolygonCollider2D: 正多边形至少需要3条边');
return;
}
const vertices: IVector2[] = [];
const angleStep = (Math.PI * 2) / sides;
for (let i = 0; i < sides; i++) {
const angle = angleStep * i - Math.PI / 2; // 从顶部开始
vertices.push({
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius
});
}
this.setVertices(vertices);
}
/**
* 验证多边形是否为凸多边形
* @returns 是否为凸多边形
*/
public isConvex(): boolean {
if (this.vertices.length < 3) return false;
const n = this.vertices.length;
let sign = 0;
for (let i = 0; i < n; i++) {
const v0 = this.vertices[i];
const v1 = this.vertices[(i + 1) % n];
const v2 = this.vertices[(i + 2) % n];
const cross = (v1.x - v0.x) * (v2.y - v1.y) - (v1.y - v0.y) * (v2.x - v1.x);
if (cross !== 0) {
if (sign === 0) {
sign = cross > 0 ? 1 : -1;
} else if ((cross > 0 ? 1 : -1) !== sign) {
return false;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,322 @@
/**
* Rigidbody2D Component
* 2D 刚体组件
*/
import { Component, Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import { RigidbodyType2D, CollisionDetectionMode2D } from '../types/Physics2DTypes';
/**
* 刚体约束配置
*/
export interface RigidbodyConstraints2D {
/** 冻结 X 轴位置 */
freezePositionX: boolean;
/** 冻结 Y 轴位置 */
freezePositionY: boolean;
/** 冻结旋转 */
freezeRotation: boolean;
}
/**
* 2D 刚体组件
*
* 用于给实体添加物理模拟能力。必须与 TransformComponent 配合使用。
*
* @example
* ```typescript
* const entity = scene.createEntity('Player');
* entity.addComponent(TransformComponent);
* const rb = entity.addComponent(Rigidbody2DComponent);
* rb.bodyType = RigidbodyType2D.Dynamic;
* rb.mass = 1;
* rb.gravityScale = 1;
* ```
*/
@ECSComponent('Rigidbody2D')
@Serializable({ version: 1, typeId: 'Rigidbody2D' })
export class Rigidbody2DComponent extends Component {
// ==================== 基础属性 ====================
/**
* 刚体类型
* - Dynamic: 动态刚体,受力和碰撞影响
* - Kinematic: 运动学刚体,手动控制
* - Static: 静态刚体,不移动
*/
@Serialize()
@Property({
type: 'enum',
label: 'Body Type',
options: [
{ label: 'Dynamic', value: 0 },
{ label: 'Kinematic', value: 1 },
{ label: 'Static', value: 2 }
]
})
public bodyType: RigidbodyType2D = RigidbodyType2D.Dynamic;
/**
* 质量kg
* 仅对 Dynamic 刚体有效
*/
@Serialize()
@Property({ type: 'number', label: 'Mass', min: 0.001, step: 0.1 })
public mass: number = 1;
/**
* 重力缩放
* 0 = 不受重力影响1 = 正常重力,-1 = 反重力
*/
@Serialize()
@Property({ type: 'number', label: 'Gravity Scale', min: -10, max: 10, step: 0.1 })
public gravityScale: number = 1;
// ==================== 阻尼 ====================
/**
* 线性阻尼
* 值越大,移动速度衰减越快
*/
@Serialize()
@Property({ type: 'number', label: 'Linear Damping', min: 0, max: 100, step: 0.1 })
public linearDamping: number = 0;
/**
* 角速度阻尼
* 值越大,旋转速度衰减越快
*/
@Serialize()
@Property({ type: 'number', label: 'Angular Damping', min: 0, max: 100, step: 0.01 })
public angularDamping: number = 0.05;
// ==================== 约束 ====================
/**
* 冻结 X 轴位置
*/
@Serialize()
@Property({ type: 'boolean', label: 'Freeze Position X' })
public freezePositionX: boolean = false;
/**
* 冻结 Y 轴位置
*/
@Serialize()
@Property({ type: 'boolean', label: 'Freeze Position Y' })
public freezePositionY: boolean = false;
/**
* 冻结旋转
*/
@Serialize()
@Property({ type: 'boolean', label: 'Freeze Rotation' })
public freezeRotation: boolean = false;
/**
* 运动约束(兼容旧代码)
* @deprecated 使用 freezePositionX, freezePositionY, freezeRotation 代替
*/
public get constraints(): RigidbodyConstraints2D {
return {
freezePositionX: this.freezePositionX,
freezePositionY: this.freezePositionY,
freezeRotation: this.freezeRotation
};
}
public set constraints(value: RigidbodyConstraints2D) {
this.freezePositionX = value.freezePositionX;
this.freezePositionY = value.freezePositionY;
this.freezeRotation = value.freezeRotation;
}
// ==================== 碰撞检测 ====================
/**
* 碰撞检测模式
* - Discrete: 离散检测,性能好
* - Continuous: 连续检测,防穿透
*/
@Serialize()
@Property({
type: 'enum',
label: 'Collision Detection',
options: [
{ label: 'Discrete', value: 0 },
{ label: 'Continuous', value: 1 }
]
})
public collisionDetection: CollisionDetectionMode2D = CollisionDetectionMode2D.Discrete;
// ==================== 休眠 ====================
/**
* 是否允许休眠
* 休眠的刚体不参与物理计算,提高性能
*/
@Serialize()
@Property({ type: 'boolean', label: 'Can Sleep' })
public canSleep: boolean = true;
/**
* 是否处于唤醒状态
*/
@Property({ type: 'boolean', label: 'Is Awake', readOnly: true })
public isAwake: boolean = true;
// ==================== 运行时状态(不序列化)====================
/**
* 当前线速度
*/
public velocity: IVector2 = { x: 0, y: 0 };
/**
* 当前角速度(弧度/秒)
*/
public angularVelocity: number = 0;
// ==================== 内部状态 ====================
/**
* Rapier 刚体句柄
* @internal
*/
public _bodyHandle: number | null = null;
/**
* 是否需要同步 Transform 到物理世界
* @internal
*/
public _needsSync: boolean = true;
/**
* 上一帧的位置(用于插值)
* @internal
*/
public _previousPosition: IVector2 = { x: 0, y: 0 };
/**
* 上一帧的旋转角度
* @internal
*/
public _previousRotation: number = 0;
// ==================== API 方法 ====================
/**
* 添加力(在下一个物理步进中应用)
* 这是一个标记方法,实际力的应用由 Physics2DSystem 处理
*/
public addForce(force: IVector2): void {
this._pendingForce.x += force.x;
this._pendingForce.y += force.y;
}
/**
* 添加冲量(立即改变速度)
*/
public addImpulse(impulse: IVector2): void {
this._pendingImpulse.x += impulse.x;
this._pendingImpulse.y += impulse.y;
}
/**
* 添加扭矩
*/
public addTorque(torque: number): void {
this._pendingTorque += torque;
}
/**
* 添加角冲量
*/
public addAngularImpulse(impulse: number): void {
this._pendingAngularImpulse += impulse;
}
/**
* 设置线速度
*/
public setVelocity(velocity: IVector2): void {
this._targetVelocity = { ...velocity };
this._hasTargetVelocity = true;
}
/**
* 设置角速度
*/
public setAngularVelocity(angularVelocity: number): void {
this._targetAngularVelocity = angularVelocity;
this._hasTargetAngularVelocity = true;
}
/**
* 唤醒刚体
*/
public wakeUp(): void {
this._shouldWakeUp = true;
}
/**
* 使刚体休眠
*/
public sleep(): void {
this._shouldSleep = true;
}
/**
* 标记需要重新同步 Transform
*/
public markNeedsSync(): void {
this._needsSync = true;
}
// ==================== 待处理的力和冲量 ====================
/** @internal */
public _pendingForce: IVector2 = { x: 0, y: 0 };
/** @internal */
public _pendingImpulse: IVector2 = { x: 0, y: 0 };
/** @internal */
public _pendingTorque: number = 0;
/** @internal */
public _pendingAngularImpulse: number = 0;
/** @internal */
public _targetVelocity: IVector2 = { x: 0, y: 0 };
/** @internal */
public _hasTargetVelocity: boolean = false;
/** @internal */
public _targetAngularVelocity: number = 0;
/** @internal */
public _hasTargetAngularVelocity: boolean = false;
/** @internal */
public _shouldWakeUp: boolean = false;
/** @internal */
public _shouldSleep: boolean = false;
/**
* 清除待处理的力和冲量
* @internal
*/
public _clearPendingForces(): void {
this._pendingForce.x = 0;
this._pendingForce.y = 0;
this._pendingImpulse.x = 0;
this._pendingImpulse.y = 0;
this._pendingTorque = 0;
this._pendingAngularImpulse = 0;
this._hasTargetVelocity = false;
this._hasTargetAngularVelocity = false;
this._shouldWakeUp = false;
this._shouldSleep = false;
}
public override onRemovedFromEntity(): void {
// 清理句柄,实际的物理对象清理由系统处理
this._bodyHandle = null;
this._clearPendingForces();
}
}

View File

@@ -0,0 +1,11 @@
/**
* Physics 2D Components
* 2D 物理组件导出
*/
export { Rigidbody2DComponent, type RigidbodyConstraints2D } from './Rigidbody2DComponent';
export { Collider2DBase } from './Collider2DBase';
export { BoxCollider2DComponent } from './BoxCollider2DComponent';
export { CircleCollider2DComponent } from './CircleCollider2DComponent';
export { CapsuleCollider2DComponent, CapsuleDirection2D } from './CapsuleCollider2DComponent';
export { PolygonCollider2DComponent } from './PolygonCollider2DComponent';

View File

@@ -0,0 +1,42 @@
/**
* @esengine/physics-rapier2d
*
* Deterministic 2D physics engine based on Rapier2D with enhanced-determinism support.
* 基于 Rapier2D 的确定性 2D 物理引擎。
*
* 注意:此入口不包含 WASM 依赖,可安全地在编辑器中同步导入。
* 运行时模块(含 WASM请使用 '@esengine/physics-rapier2d/runtime' 导入。
*
* @packageDocumentation
*/
// Types (no WASM dependency)
export * from './types';
// Components (no WASM dependency)
export * from './components';
// Services (no WASM dependency)
export * from './services';
// Systems (type only for editor usage)
export type { Physics2DSystem } from './systems/Physics2DSystem';
// Editor plugin (no WASM dependency)
export { Physics2DPlugin } from './PhysicsEditorPlugin';
// Runtime plugin (for game builds)
export { PhysicsPlugin } from './PhysicsRuntimeModule';
// Service tokens and interfaces (谁定义接口,谁导出 Token)
export {
Physics2DQueryToken,
Physics2DSystemToken,
Physics2DWorldToken,
PhysicsConfigToken,
CollisionLayerConfigToken,
type IPhysics2DQuery,
type IPhysics2DWorld,
type ICollisionLayerConfig,
type PhysicsConfig
} from './tokens';

View File

@@ -0,0 +1,64 @@
/**
* Rapier2D 加载器配置
* Rapier2D loader configuration
*/
import type { WasmLibraryConfig } from '@esengine/platform-common';
import { isEditorEnvironment } from '@esengine/platform-common';
/**
* 获取 WASM 路径
* Get WASM path based on environment
*
* Editor: engine/rapier2d/pkg/rapier_wasm2d_bg.wasm (deployed by vite build plugin)
* Runtime: wasm/rapier_wasm2d_bg.wasm (deployed by game build)
*/
function getWasmPath(): string {
const isEditor = isEditorEnvironment();
// Editor uses dist/engine/rapier2d/pkg/ structure (from vite copy-engine-modules plugin)
// 编辑器使用 dist/engine/rapier2d/pkg/ 结构(来自 vite copy-engine-modules 插件)
const path = isEditor
? 'engine/rapier2d/pkg/rapier_wasm2d_bg.wasm'
: 'wasm/rapier_wasm2d_bg.wasm';
console.log(`[Rapier2D] isEditor=${isEditor}, wasmPath=${path}`);
return path;
}
/**
* Rapier2D 加载器配置
*
* Web 平台:使用标准版(独立 WASM 文件)
* 小游戏平台:使用独立 WASM 文件 + WXWebAssembly 加载
*/
export const Rapier2DLoaderConfig: WasmLibraryConfig = {
name: 'Rapier2D',
web: {
/**
* WASM 文件路径
* 编辑器: engine/rapier2d/pkg/rapier_wasm2d_bg.wasm
* 运行时: wasm/rapier_wasm2d_bg.wasm
*/
get wasmPath(): string {
return getWasmPath();
}
},
minigame: {
/**
* WASM 文件路径(相对于小游戏根目录)
*/
wasmPath: 'wasm/rapier_wasm2d_bg.wasm',
/**
* iOS 微信小游戏需要 TextDecoder polyfill
*/
needsTextDecoderPolyfill: true,
/**
* iOS 微信小游戏需要 TextEncoder polyfill
*/
needsTextEncoderPolyfill: true,
}
};

View File

@@ -0,0 +1,237 @@
/**
* 微信小游戏平台 Rapier2D 加载器
*
* 使用 WXWebAssembly 加载独立的 .wasm 文件
*/
import type {
IWasmLibraryLoader,
WasmLibraryConfig,
PlatformInfo
} from '@esengine/platform-common';
import {
PlatformType,
installTextDecoderPolyfill,
installTextEncoderPolyfill
} from '@esengine/platform-common';
/**
* Rapier2D 模块类型
*/
type RapierModule = typeof import('@esengine/rapier2d');
/**
* 微信小游戏 WASM API 类型声明
*/
declare const WXWebAssembly: {
instantiate(
path: string,
imports?: WebAssembly.Imports
): Promise<WebAssembly.Instance>;
Memory: typeof WebAssembly.Memory;
Table: typeof WebAssembly.Table;
};
/**
* 微信小游戏平台 Rapier2D 加载器
*
* 特殊处理:
* 1. 安装 TextDecoder/TextEncoder polyfill
* 2. 使用 WXWebAssembly 加载 .wasm 文件
* 3. 临时替换全局 WebAssembly 对象
*
* @example
* ```typescript
* const loader = new WeChatRapier2DLoader(config);
* if (loader.isSupported()) {
* const RAPIER = await loader.load();
* // 使用 RAPIER...
* }
* ```
*/
export class WeChatRapier2DLoader implements IWasmLibraryLoader<RapierModule> {
private _config: WasmLibraryConfig;
/**
* 创建微信小游戏平台 Rapier2D 加载器
*
* @param config - 加载器配置
*/
constructor(config: WasmLibraryConfig) {
this._config = config;
}
/**
* 加载 Rapier2D 模块
*
* @returns 初始化完成的 Rapier2D 模块
*/
async load(): Promise<RapierModule> {
console.log(`[${this._config.name}] 正在加载微信小游戏版本...`);
// 1. 安装必要的 polyfills
this.installPolyfills();
// 2. 检查 WXWebAssembly 支持
if (typeof WXWebAssembly === 'undefined') {
throw new Error(
`[${this._config.name}] 当前微信基础库版本不支持 WebAssembly` +
'请升级微信或使用更高版本的基础库'
);
}
// 3. 加载 Rapier2D
const RAPIER = await this.loadRapierWithWXWasm();
console.log(`[${this._config.name}] 加载完成`);
return RAPIER;
}
/**
* 安装必要的 polyfills
*/
private installPolyfills(): void {
const config = this._config.minigame;
if (config?.needsTextDecoderPolyfill) {
installTextDecoderPolyfill();
}
if (config?.needsTextEncoderPolyfill) {
installTextEncoderPolyfill();
}
}
/**
* 使用 WXWebAssembly 加载 Rapier2D
*
* 通过临时替换全局 WebAssembly 对象来使 Rapier2D 使用 WXWebAssembly
*
* @returns 初始化完成的 Rapier2D 模块
*/
private async loadRapierWithWXWasm(): Promise<RapierModule> {
// 保存原始 WebAssembly 对象
const originalWebAssembly = (globalThis as any).WebAssembly;
try {
// 创建一个包装的 WebAssembly 对象
// 让 Rapier2D 的初始化代码使用 WXWebAssembly
(globalThis as any).WebAssembly = this.createWXWebAssemblyWrapper();
// 导入 Rapier2D 标准版
const RAPIER = await import('@esengine/rapier2d');
// 初始化 WASM - 标准版需要提供 WASM 路径
const wasmPath = this._config.minigame?.wasmPath || 'wasm/rapier_wasm2d_bg.wasm';
await RAPIER.init(wasmPath);
return RAPIER;
} finally {
// 恢复原始 WebAssembly 对象
if (originalWebAssembly) {
(globalThis as any).WebAssembly = originalWebAssembly;
}
}
}
/**
* 创建 WXWebAssembly 包装器
*
* 将 WXWebAssembly 包装成与标准 WebAssembly API 兼容的形式
*
* @returns 包装后的 WebAssembly 对象
*/
private createWXWebAssemblyWrapper(): typeof WebAssembly {
const wasmPath = this._config.minigame?.wasmPath || 'wasm/rapier2d_bg.wasm';
return {
instantiate: async (
bufferSource: BufferSource | WebAssembly.Module,
imports?: WebAssembly.Imports
): Promise<WebAssembly.WebAssemblyInstantiatedSource> => {
// WXWebAssembly.instantiate 直接接受文件路径
const instance = await WXWebAssembly.instantiate(wasmPath, imports);
return {
instance,
module: {} as WebAssembly.Module
};
},
instantiateStreaming: async (
response: Response | PromiseLike<Response>,
imports?: WebAssembly.Imports
): Promise<WebAssembly.WebAssemblyInstantiatedSource> => {
// 微信不支持 streaming直接使用 instantiate
const instance = await WXWebAssembly.instantiate(wasmPath, imports);
return {
instance,
module: {} as WebAssembly.Module
};
},
compile: async (bytes: BufferSource): Promise<WebAssembly.Module> => {
// 微信小游戏不支持单独编译
throw new Error('WXWebAssembly 不支持 compile 方法');
},
compileStreaming: async (source: Response | PromiseLike<Response>): Promise<WebAssembly.Module> => {
throw new Error('WXWebAssembly 不支持 compileStreaming 方法');
},
validate: (bytes: BufferSource): boolean => {
// 简单返回 true实际验证在 instantiate 时进行
return true;
},
Memory: WXWebAssembly.Memory,
Table: WXWebAssembly.Table,
Global: (globalThis as any).WebAssembly?.Global,
Tag: (globalThis as any).WebAssembly?.Tag,
Exception: (globalThis as any).WebAssembly?.Exception,
CompileError: (globalThis as any).WebAssembly?.CompileError || Error,
LinkError: (globalThis as any).WebAssembly?.LinkError || Error,
RuntimeError: (globalThis as any).WebAssembly?.RuntimeError || Error,
} as unknown as typeof WebAssembly;
}
/**
* 检查是否支持 WXWebAssembly
*
* @returns 是否支持
*/
isSupported(): boolean {
return typeof WXWebAssembly !== 'undefined';
}
/**
* 获取平台信息
* Get platform information
*/
getPlatformInfo(): PlatformInfo {
const needsPolyfills: string[] = [];
if (typeof globalThis.TextDecoder === 'undefined') {
needsPolyfills.push('TextDecoder');
}
if (typeof globalThis.TextEncoder === 'undefined') {
needsPolyfills.push('TextEncoder');
}
return {
type: PlatformType.WeChatMiniGame,
supportsWasm: typeof WXWebAssembly !== 'undefined',
supportsSharedArrayBuffer: false,
needsPolyfills,
isEditor: false // 微信小游戏不可能是编辑器环境 | WeChat cannot be editor
};
}
/**
* 获取加载器配置
*
* @returns 配置对象
*/
getConfig(): WasmLibraryConfig {
return this._config;
}
}

View File

@@ -0,0 +1,96 @@
/**
* Web 平台 Rapier2D 加载器
*
* 使用 @esengine/rapier2d 标准版(独立 WASM 文件)
*/
import type {
IWasmLibraryLoader,
WasmLibraryConfig,
PlatformInfo
} from '@esengine/platform-common';
import { PlatformType, isEditorEnvironment } from '@esengine/platform-common';
/**
* Rapier2D 模块类型
*/
type RapierModule = typeof import('@esengine/rapier2d');
/**
* Web 平台 Rapier2D 加载器
*
* 使用标准版,需要配置 WASM 路径
*
* @example
* ```typescript
* const loader = new WebRapier2DLoader(config);
* if (loader.isSupported()) {
* const RAPIER = await loader.load();
* // 使用 RAPIER...
* }
* ```
*/
export class WebRapier2DLoader implements IWasmLibraryLoader<RapierModule> {
private _config: WasmLibraryConfig;
/**
* 创建 Web 平台 Rapier2D 加载器
*
* @param config - 加载器配置
*/
constructor(config: WasmLibraryConfig) {
this._config = config;
}
/**
* 加载 Rapier2D 模块
*
* @returns 初始化完成的 Rapier2D 模块
*/
async load(): Promise<RapierModule> {
console.log(`[${this._config.name}] 正在加载 Web 版本...`);
// 动态导入标准版
const RAPIER = await import('@esengine/rapier2d');
// 初始化 WASM - 标准版需要提供 WASM 路径
// 构建时 WASM 文件会被复制到 wasm/ 目录
const wasmPath = this._config.web?.wasmPath || 'wasm/rapier_wasm2d_bg.wasm';
await RAPIER.init(wasmPath);
console.log(`[${this._config.name}] 加载完成`);
return RAPIER;
}
/**
* 检查是否支持 WebAssembly
*
* @returns 是否支持
*/
isSupported(): boolean {
return typeof WebAssembly !== 'undefined';
}
/**
* 获取平台信息
* Get platform information
*/
getPlatformInfo(): PlatformInfo {
return {
type: PlatformType.Web,
supportsWasm: typeof WebAssembly !== 'undefined',
supportsSharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
needsPolyfills: [],
isEditor: isEditorEnvironment()
};
}
/**
* 获取加载器配置
*
* @returns 配置对象
*/
getConfig(): WasmLibraryConfig {
return this._config;
}
}

View File

@@ -0,0 +1,58 @@
/**
* Rapier2D 加载器
*
* 提供跨平台的 Rapier2D 物理引擎加载支持
*/
export { Rapier2DLoaderConfig } from './Rapier2DLoaderConfig';
export { WebRapier2DLoader } from './WebRapier2DLoader';
export { WeChatRapier2DLoader } from './WeChatRapier2DLoader';
import { PlatformType, WasmLibraryLoaderFactory } from '@esengine/platform-common';
import { Rapier2DLoaderConfig } from './Rapier2DLoaderConfig';
import { WebRapier2DLoader } from './WebRapier2DLoader';
import { WeChatRapier2DLoader } from './WeChatRapier2DLoader';
/**
* 注册 Rapier2D 加载器到工厂
*
* 在模块加载时自动执行
*/
export function registerRapier2DLoaders(): void {
// Web 平台加载器
WasmLibraryLoaderFactory.registerLoader(
'rapier2d',
PlatformType.Web,
() => new WebRapier2DLoader(Rapier2DLoaderConfig)
);
// 微信小游戏平台加载器
WasmLibraryLoaderFactory.registerLoader(
'rapier2d',
PlatformType.WeChatMiniGame,
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
);
// 其他小游戏平台可以复用微信加载器API 类似)
// 如果需要特殊处理,可以创建专门的加载器
WasmLibraryLoaderFactory.registerLoader(
'rapier2d',
PlatformType.ByteDanceMiniGame,
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
);
WasmLibraryLoaderFactory.registerLoader(
'rapier2d',
PlatformType.AlipayMiniGame,
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
);
WasmLibraryLoaderFactory.registerLoader(
'rapier2d',
PlatformType.BaiduMiniGame,
() => new WeChatRapier2DLoader(Rapier2DLoaderConfig)
);
}
// 模块加载时自动注册
registerRapier2DLoaders();

View File

@@ -0,0 +1,65 @@
/**
* @esengine/physics-rapier2d Runtime Entry Point
*
* This entry point exports only runtime-related code without any editor dependencies.
* Use this for standalone game runtime builds.
*/
// Types
export {
RigidbodyType2D,
CollisionDetectionMode2D,
type Physics2DConfig,
DEFAULT_PHYSICS_CONFIG,
CollisionLayer2D,
ForceMode2D,
type RaycastHit2D,
type ShapeCastHit2D,
type OverlapResult2D,
PhysicsMaterial2DPreset,
getPhysicsMaterialPreset,
JointType2D
} from './types/Physics2DTypes';
// Re-export IVector2 from math package
export type { IVector2 } from '@esengine/ecs-framework-math';
export {
type CollisionEventType,
type TriggerEventType,
type ContactPoint2D,
type CollisionEvent2D,
type TriggerEvent2D,
PHYSICS_EVENTS,
type Physics2DEventMap
} from './types/Physics2DEvents';
// Components
export { Rigidbody2DComponent, type RigidbodyConstraints2D } from './components/Rigidbody2DComponent';
export { Collider2DBase } from './components/Collider2DBase';
export { BoxCollider2DComponent } from './components/BoxCollider2DComponent';
export { CircleCollider2DComponent } from './components/CircleCollider2DComponent';
export { CapsuleCollider2DComponent, CapsuleDirection2D } from './components/CapsuleCollider2DComponent';
export { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent';
// World
export { Physics2DWorld, type Physics2DWorldState } from './world/Physics2DWorld';
// Systems
export { Physics2DSystem, type Physics2DSystemConfig } from './systems/Physics2DSystem';
// Services
export { Physics2DService } from './services/Physics2DService';
// Runtime module and plugin
export { PhysicsRuntimeModule, PhysicsPlugin } from './PhysicsRuntimeModule';
// Service tokens
export {
Physics2DQueryToken,
Physics2DSystemToken,
Physics2DWorldToken,
PhysicsConfigToken,
type IPhysics2DQuery,
type PhysicsConfig
} from './tokens';

View File

@@ -0,0 +1,17 @@
/**
* Physics 2D Runtime Entry
* 2D 物理运行时入口
*
* 包含 WASM 依赖,用于实际运行时环境
* Contains WASM dependencies, for actual runtime environment
*/
// Re-export runtime module and plugin with WASM
export { PhysicsRuntimeModule, PhysicsPlugin } from '../PhysicsRuntimeModule';
// Re-export world and system (they have WASM type dependencies)
export { Physics2DWorld } from '../world/Physics2DWorld';
export type { Physics2DWorldState } from '../world/Physics2DWorld';
export { Physics2DSystem } from '../systems/Physics2DSystem';
export type { Physics2DSystemConfig } from '../systems/Physics2DSystem';

View File

@@ -0,0 +1,245 @@
/**
* Collision Layer Configuration Service
* 碰撞层配置服务
*
* 管理碰撞层的定义和碰撞矩阵配置
*/
/**
* 碰撞层定义
*/
export interface CollisionLayerDefinition {
/** 层索引 (0-15) */
index: number;
/** 层名称 */
name: string;
/** 层描述 */
description?: string;
}
/**
* 碰撞层配置
*/
export interface CollisionLayerSettings {
/** 层定义列表 */
layers: CollisionLayerDefinition[];
/** 碰撞矩阵 (16x16 位图matrix[i] 表示第 i 层可以与哪些层碰撞) */
collisionMatrix: number[];
}
/**
* 默认碰撞层配置
*/
export const DEFAULT_COLLISION_LAYERS: CollisionLayerDefinition[] = [
{ index: 0, name: 'Default', description: '默认层' },
{ index: 1, name: 'Player', description: '玩家' },
{ index: 2, name: 'Enemy', description: '敌人' },
{ index: 3, name: 'Projectile', description: '投射物' },
{ index: 4, name: 'Ground', description: '地面' },
{ index: 5, name: 'Platform', description: '平台' },
{ index: 6, name: 'Trigger', description: '触发器' },
{ index: 7, name: 'Item', description: '物品' },
{ index: 8, name: 'Layer8', description: '自定义层8' },
{ index: 9, name: 'Layer9', description: '自定义层9' },
{ index: 10, name: 'Layer10', description: '自定义层10' },
{ index: 11, name: 'Layer11', description: '自定义层11' },
{ index: 12, name: 'Layer12', description: '自定义层12' },
{ index: 13, name: 'Layer13', description: '自定义层13' },
{ index: 14, name: 'Layer14', description: '自定义层14' },
{ index: 15, name: 'Layer15', description: '自定义层15' },
];
/**
* 默认碰撞矩阵 - 所有层都可以互相碰撞
*/
export const DEFAULT_COLLISION_MATRIX: number[] = Array(16).fill(0xFFFF);
/**
* 碰撞层配置管理器
*/
export class CollisionLayerConfig {
private static instance: CollisionLayerConfig | null = null;
private layers: CollisionLayerDefinition[] = [...DEFAULT_COLLISION_LAYERS];
private collisionMatrix: number[] = [...DEFAULT_COLLISION_MATRIX];
private listeners: Set<() => void> = new Set();
private constructor() {}
public static getInstance(): CollisionLayerConfig {
if (!CollisionLayerConfig.instance) {
CollisionLayerConfig.instance = new CollisionLayerConfig();
}
return CollisionLayerConfig.instance;
}
/**
* 获取所有层定义
*/
public getLayers(): readonly CollisionLayerDefinition[] {
return this.layers;
}
/**
* 获取层名称
*/
public getLayerName(index: number): string {
if (index < 0 || index >= 16) return `Layer${index}`;
return this.layers[index]?.name ?? `Layer${index}`;
}
/**
* 设置层名称
*/
public setLayerName(index: number, name: string): void {
if (index < 0 || index >= 16) return;
if (this.layers[index]) {
this.layers[index].name = name;
this.notifyListeners();
}
}
/**
* 设置层描述
*/
public setLayerDescription(index: number, description: string): void {
if (index < 0 || index >= 16) return;
if (this.layers[index]) {
this.layers[index].description = description;
this.notifyListeners();
}
}
/**
* 获取碰撞矩阵
*/
public getCollisionMatrix(): readonly number[] {
return this.collisionMatrix;
}
/**
* 检查两个层是否可以碰撞
*/
public canLayersCollide(layerA: number, layerB: number): boolean {
if (layerA < 0 || layerA >= 16 || layerB < 0 || layerB >= 16) {
return false;
}
return (this.collisionMatrix[layerA] & (1 << layerB)) !== 0;
}
/**
* 设置两个层是否可以碰撞
*/
public setLayersCanCollide(layerA: number, layerB: number, canCollide: boolean): void {
if (layerA < 0 || layerA >= 16 || layerB < 0 || layerB >= 16) {
return;
}
if (canCollide) {
this.collisionMatrix[layerA] |= (1 << layerB);
this.collisionMatrix[layerB] |= (1 << layerA);
} else {
this.collisionMatrix[layerA] &= ~(1 << layerB);
this.collisionMatrix[layerB] &= ~(1 << layerA);
}
this.notifyListeners();
}
/**
* 获取指定层的碰撞掩码
*/
public getLayerMask(layerIndex: number): number {
if (layerIndex < 0 || layerIndex >= 16) return 0xFFFF;
return this.collisionMatrix[layerIndex];
}
/**
* 根据层索引获取层位值
*/
public getLayerBit(layerIndex: number): number {
if (layerIndex < 0 || layerIndex >= 16) return 1;
return 1 << layerIndex;
}
/**
* 从层位值获取层索引
*/
public getLayerIndex(layerBit: number): number {
for (let i = 0; i < 16; i++) {
if (layerBit === (1 << i)) {
return i;
}
}
// 如果是多层位值,返回第一个设置的位
for (let i = 0; i < 16; i++) {
if ((layerBit & (1 << i)) !== 0) {
return i;
}
}
return 0;
}
/**
* 加载配置
*/
public loadSettings(settings: Partial<CollisionLayerSettings>): void {
if (settings.layers) {
this.layers = settings.layers.map((layer, i) => ({
index: layer.index ?? i,
name: layer.name ?? `Layer${i}`,
description: layer.description
}));
// 确保有16个层
while (this.layers.length < 16) {
const idx = this.layers.length;
this.layers.push({ index: idx, name: `Layer${idx}` });
}
}
if (settings.collisionMatrix) {
this.collisionMatrix = [...settings.collisionMatrix];
while (this.collisionMatrix.length < 16) {
this.collisionMatrix.push(0xFFFF);
}
}
this.notifyListeners();
}
/**
* 导出配置
*/
public exportSettings(): CollisionLayerSettings {
return {
layers: [...this.layers],
collisionMatrix: [...this.collisionMatrix]
};
}
/**
* 重置为默认配置
*/
public resetToDefault(): void {
this.layers = [...DEFAULT_COLLISION_LAYERS];
this.collisionMatrix = [...DEFAULT_COLLISION_MATRIX];
this.notifyListeners();
}
/**
* 添加监听器
*/
public addListener(listener: () => void): void {
this.listeners.add(listener);
}
/**
* 移除监听器
*/
public removeListener(listener: () => void): void {
this.listeners.delete(listener);
}
private notifyListeners(): void {
for (const listener of this.listeners) {
listener();
}
}
}

View File

@@ -0,0 +1,214 @@
/**
* Physics2DService
* 2D 物理服务
*
* 提供全局物理配置和实用方法
*/
import type { IService } from '@esengine/ecs-framework';
import type { IVector2 } from '@esengine/ecs-framework-math';
import type { Physics2DConfig, RaycastHit2D, OverlapResult2D } from '../types/Physics2DTypes';
import { DEFAULT_PHYSICS_CONFIG, CollisionLayer2D } from '../types/Physics2DTypes';
import type { Physics2DSystem } from '../systems/Physics2DSystem';
/**
* 2D 物理服务
*
* 提供场景级别的物理配置和全局查询方法。
* 作为服务注册到 ServiceContainer 中。
*
* @example
* ```typescript
* // 从服务容器获取
* const physicsService = scene.services.resolve(Physics2DService);
*
* // 使用射线检测
* const hit = physicsService.raycast(origin, direction, 100);
* if (hit) {
* console.log('Hit entity:', hit.entityId);
* }
* ```
*/
export class Physics2DService implements IService {
private _config: Physics2DConfig = { ...DEFAULT_PHYSICS_CONFIG };
private _physicsSystem: Physics2DSystem | null = null;
/**
* 设置物理系统引用
* @internal
*/
public setPhysicsSystem(system: Physics2DSystem): void {
this._physicsSystem = system;
}
/**
* 获取物理系统
*/
public getPhysicsSystem(): Physics2DSystem | null {
return this._physicsSystem;
}
// ==================== 配置 ====================
/**
* 获取物理配置
*/
public getConfig(): Readonly<Physics2DConfig> {
return this._config;
}
/**
* 设置重力
*/
public setGravity(gravity: IVector2): void {
this._config.gravity = { ...gravity };
this._physicsSystem?.setGravity(gravity);
}
/**
* 获取重力
*/
public getGravity(): IVector2 {
return this._physicsSystem?.getGravity() ?? { ...this._config.gravity };
}
/**
* 设置时间步长
*/
public setTimestep(timestep: number): void {
this._config.timestep = timestep;
}
/**
* 获取时间步长
*/
public getTimestep(): number {
return this._config.timestep;
}
// ==================== 查询 ====================
/**
* 射线检测(第一个命中)
* @param origin 起点
* @param direction 方向(归一化)
* @param maxDistance 最大距离
* @param collisionMask 碰撞掩码(默认所有层)
*/
public raycast(
origin: IVector2,
direction: IVector2,
maxDistance: number,
collisionMask: number = CollisionLayer2D.All
): RaycastHit2D | null {
return this._physicsSystem?.raycast(origin, direction, maxDistance, collisionMask) ?? null;
}
/**
* 射线检测(所有命中)
* @param origin 起点
* @param direction 方向(归一化)
* @param maxDistance 最大距离
* @param collisionMask 碰撞掩码(默认所有层)
*/
public raycastAll(
origin: IVector2,
direction: IVector2,
maxDistance: number,
collisionMask: number = CollisionLayer2D.All
): RaycastHit2D[] {
return this._physicsSystem?.raycastAll(origin, direction, maxDistance, collisionMask) ?? [];
}
/**
* 点重叠检测
* @param point 检测点
* @param collisionMask 碰撞掩码
*/
public overlapPoint(point: IVector2, collisionMask: number = CollisionLayer2D.All): OverlapResult2D {
return this._physicsSystem?.overlapPoint(point, collisionMask) ?? { entityIds: [], colliderHandles: [] };
}
/**
* 圆形重叠检测
* @param center 圆心
* @param radius 半径
* @param collisionMask 碰撞掩码
*/
public overlapCircle(
center: IVector2,
radius: number,
collisionMask: number = CollisionLayer2D.All
): OverlapResult2D {
return this._physicsSystem?.overlapCircle(center, radius, collisionMask) ?? { entityIds: [], colliderHandles: [] };
}
/**
* 矩形重叠检测
* @param center 中心点
* @param halfExtents 半宽高
* @param rotation 旋转角度
* @param collisionMask 碰撞掩码
*/
public overlapBox(
center: IVector2,
halfExtents: IVector2,
rotation: number = 0,
collisionMask: number = CollisionLayer2D.All
): OverlapResult2D {
return (
this._physicsSystem?.overlapBox(center, halfExtents, rotation, collisionMask) ?? {
entityIds: [],
colliderHandles: []
}
);
}
// ==================== 工具方法 ====================
/**
* 归一化向量
*/
public normalize(v: IVector2): IVector2 {
const length = Math.sqrt(v.x * v.x + v.y * v.y);
if (length === 0) return { x: 0, y: 0 };
return { x: v.x / length, y: v.y / length };
}
/**
* 计算两点之间的距离
*/
public distance(a: IVector2, b: IVector2): number {
const dx = b.x - a.x;
const dy = b.y - a.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 计算向量长度
*/
public magnitude(v: IVector2): number {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
/**
* 向量点积
*/
public dot(a: IVector2, b: IVector2): number {
return a.x * b.x + a.y * b.y;
}
/**
* 向量叉积返回标量2D 特有)
*/
public cross(a: IVector2, b: IVector2): number {
return a.x * b.y - a.y * b.x;
}
/**
* 释放资源
*/
public dispose(): void {
this._physicsSystem = null;
}
}

View File

@@ -0,0 +1,14 @@
/**
* Physics 2D Services exports
*/
export { Physics2DService } from './Physics2DService';
export {
CollisionLayerConfig,
DEFAULT_COLLISION_LAYERS,
DEFAULT_COLLISION_MATRIX
} from './CollisionLayerConfig';
export type {
CollisionLayerDefinition,
CollisionLayerSettings
} from './CollisionLayerConfig';

View File

@@ -0,0 +1,565 @@
/**
* Physics2DSystem
* 2D 物理系统
*
* 负责更新物理世界并同步 Transform
*/
import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework';
import { TransformComponent } from '@esengine/engine-core';
import { Physics2DWorld } from '../world/Physics2DWorld';
import { Rigidbody2DComponent } from '../components/Rigidbody2DComponent';
import { Collider2DBase } from '../components/Collider2DBase';
import { BoxCollider2DComponent } from '../components/BoxCollider2DComponent';
import { CircleCollider2DComponent } from '../components/CircleCollider2DComponent';
import { CapsuleCollider2DComponent } from '../components/CapsuleCollider2DComponent';
import { PolygonCollider2DComponent } from '../components/PolygonCollider2DComponent';
import type { IVector2 } from '@esengine/ecs-framework-math';
import type { Physics2DConfig } from '../types/Physics2DTypes';
import { PHYSICS_EVENTS, type CollisionEvent2D, type TriggerEvent2D } from '../types/Physics2DEvents';
/**
* 物理系统配置
*/
export interface Physics2DSystemConfig {
/** 物理世界配置 */
physics?: Partial<Physics2DConfig>;
/** 是否在 lateUpdate 中同步 Transform用于渲染插值 */
interpolateInLateUpdate?: boolean;
/** 更新优先级(默认 -1000保证在其他系统之前更新 */
updateOrder?: number;
}
/**
* 2D 物理系统
*
* 管理物理世界的更新和实体的物理属性同步。
*
* 职责:
* - 初始化和管理 Physics2DWorld
* - 同步 Entity Transform 与物理世界
* - 应用力和冲量
* - 发送碰撞/触发器事件
*
* @example
* ```typescript
* // 注册物理系统
* scene.addEntityProcessor(Physics2DSystem);
*
* // 或使用自定义配置
* const physicsSystem = new Physics2DSystem({
* physics: {
* gravity: { x: 0, y: -20 }
* }
* });
* scene.addEntityProcessor(physicsSystem);
* ```
*/
export class Physics2DSystem extends EntitySystem {
private _world: Physics2DWorld;
private _rapierModule: typeof import('@esengine/rapier2d') | null = null;
private _rapierInitialized: boolean = false;
private _config: Physics2DSystemConfig;
// 实体到物理对象的映射
private _entityBodies: Map<number, { bodyHandle: number; colliderHandles: number[] }> = new Map();
// 待处理的新实体队列
private _pendingEntities: Entity[] = [];
constructor(config?: Physics2DSystemConfig) {
// 匹配所有拥有 Rigidbody2DComponent 的实体
super(Matcher.empty().all(Rigidbody2DComponent));
this._config = {
interpolateInLateUpdate: true,
updateOrder: -1000,
...config
};
this._world = new Physics2DWorld(this._config.physics);
this.setUpdateOrder(this._config.updateOrder ?? -1000);
}
/**
* 获取物理世界实例
*/
public get world(): Physics2DWorld {
return this._world;
}
/**
* 系统初始化
*/
protected override onInitialize(): void {
// Rapier 模块由外部通过 initializeWithRapier 注入
this.logger.debug('Physics2DSystem initialized, waiting for Rapier module');
}
/**
* 使用 Rapier 模块初始化物理世界
*
* 必须在系统开始处理前调用此方法
*
* @param rapier Rapier2D 模块
*/
public async initializeWithRapier(rapier: typeof import('@esengine/rapier2d')): Promise<void> {
if (this._rapierInitialized) {
this.logger.warn('Physics2DSystem already initialized');
return;
}
this._rapierModule = rapier;
await this._world.initialize(rapier);
this._rapierInitialized = true;
// 注册碰撞事件回调
this._world.onCollision((event) => this._handleCollisionEvent(event));
this._world.onTrigger((event) => this._handleTriggerEvent(event));
// 处理在初始化前添加的实体
for (const entity of this._pendingEntities) {
this._createPhysicsBody(entity);
}
this._pendingEntities = [];
this.logger.info('Physics2DSystem initialized with Rapier2D');
}
/**
* 检查是否可以处理
*/
protected override onCheckProcessing(): boolean {
return this._rapierInitialized;
}
/**
* 当实体添加到系统时
*/
protected override onAdded(entity: Entity): void {
if (!this._rapierInitialized) {
// 延迟创建物理体,等待 Rapier 初始化
this._pendingEntities.push(entity);
return;
}
this._createPhysicsBody(entity);
}
/**
* 当实体从系统移除时
*/
protected override onRemoved(entity: Entity): void {
this._removePhysicsBody(entity);
// 从待处理队列中移除(如果存在)
const pendingIndex = this._pendingEntities.indexOf(entity);
if (pendingIndex >= 0) {
this._pendingEntities.splice(pendingIndex, 1);
}
}
/**
* 物理更新
*/
protected override process(entities: readonly Entity[]): void {
if (!this._rapierInitialized || !this.scene) return;
const deltaTime = this._getDeltaTime();
// 发送 pre-step 事件
this.scene.eventSystem.emitSync(PHYSICS_EVENTS.PRE_STEP, { deltaTime });
// 同步 Transform 到物理世界
this._syncTransformsToPhysics(entities);
// 应用待处理的力和冲量
this._applyPendingForces(entities);
// 物理世界步进
this._world.step(deltaTime);
// 同步物理世界到 Transform
this._syncPhysicsToTransforms(entities);
// 发送 post-step 事件
this.scene.eventSystem.emitSync(PHYSICS_EVENTS.POST_STEP, { deltaTime });
}
/**
* 后期更新(用于渲染插值)
*/
protected override lateProcess(_entities: readonly Entity[]): void {
if (!this._config.interpolateInLateUpdate || !this._rapierInitialized) return;
// 可在此处实现渲染插值
// const alpha = this._world.getAlpha();
// 插值逻辑...
}
/**
* 系统销毁
*/
protected override onDestroy(): void {
this._world.destroy();
this._entityBodies.clear();
this._pendingEntities = [];
this._rapierInitialized = false;
this.logger.info('Physics2DSystem destroyed');
}
/**
* 重置物理系统状态(保持初始化状态,但清除所有物理对象)
* 用于场景重载/预览重置
*/
public reset(): void {
this._world.reset();
this._entityBodies.clear();
this._pendingEntities = [];
// 完全重置实体跟踪,强制下次 update 时重新扫描所有实体并触发 onAdded
this.resetEntityTracking();
this.logger.info('Physics2DSystem reset');
}
// ==================== 物理 API ====================
/**
* 设置重力
* @param gravity 重力向量
*/
public setGravity(gravity: IVector2): void {
this._world.setGravity(gravity);
}
/**
* 获取重力
*/
public getGravity(): IVector2 {
return this._world.getGravity();
}
/**
* 射线检测
*/
public raycast(origin: IVector2, direction: IVector2, maxDistance: number, collisionMask?: number) {
return this._world.raycast(origin, direction, maxDistance, collisionMask);
}
/**
* 射线检测所有
*/
public raycastAll(origin: IVector2, direction: IVector2, maxDistance: number, collisionMask?: number) {
return this._world.raycastAll(origin, direction, maxDistance, collisionMask);
}
/**
* 点重叠检测
*/
public overlapPoint(point: IVector2, collisionMask?: number) {
return this._world.overlapPoint(point, collisionMask);
}
/**
* 圆形重叠检测
*/
public overlapCircle(center: IVector2, radius: number, collisionMask?: number) {
return this._world.overlapCircle(center, radius, collisionMask);
}
/**
* 矩形重叠检测
*/
public overlapBox(center: IVector2, halfExtents: IVector2, rotation?: number, collisionMask?: number) {
return this._world.overlapBox(center, halfExtents, rotation, collisionMask);
}
// ==================== 私有方法 ====================
/**
* 获取时间增量
*/
private _getDeltaTime(): number {
// TODO: 从全局 Time 服务获取
return 1 / 60;
}
/**
* 创建物理体
*/
private _createPhysicsBody(entity: Entity): void {
const rigidbody = entity.getComponent(Rigidbody2DComponent);
const transform = entity.getComponent(TransformComponent);
if (!rigidbody || !transform) {
const missing: string[] = [];
if (!rigidbody) missing.push('Rigidbody2DComponent');
if (!transform) missing.push('TransformComponent');
this.logger.warn(`Entity ${entity.name} missing required components: ${missing.join(', ')}`);
return;
}
// 获取位置和旋转
const position: IVector2 = {
x: transform.position.x,
y: transform.position.y
};
const rotation = transform.rotation.z;
// 创建刚体
const bodyHandle = this._world.createBody(entity.id, rigidbody, position, rotation);
if (bodyHandle === null) {
this.logger.error(`Failed to create physics body for entity ${entity.name}`);
return;
}
// 收集并创建碰撞体
const colliderHandles: number[] = [];
const colliders = this._getColliders(entity);
const scale: IVector2 = { x: transform.scale.x, y: transform.scale.y };
for (const collider of colliders) {
const colliderHandle = this._world.createCollider(entity.id, collider, bodyHandle, scale);
if (colliderHandle !== null) {
colliderHandles.push(colliderHandle);
}
}
// 记录映射
this._entityBodies.set(entity.id, { bodyHandle, colliderHandles });
// 存储初始位置用于插值
rigidbody._previousPosition = { ...position };
rigidbody._previousRotation = rotation;
rigidbody._needsSync = false;
this.logger.debug(`Created physics body for entity ${entity.name}`);
}
/**
* 移除物理体
*/
private _removePhysicsBody(entity: Entity): void {
const mapping = this._entityBodies.get(entity.id);
if (!mapping) return;
// 移除碰撞体
for (const colliderHandle of mapping.colliderHandles) {
this._world.removeCollider(colliderHandle);
}
// 移除刚体
this._world.removeBody(mapping.bodyHandle);
// 清除映射
this._entityBodies.delete(entity.id);
this.logger.debug(`Removed physics body for entity ${entity.name}`);
}
/**
* 同步 Transform 到物理世界
*/
private _syncTransformsToPhysics(entities: readonly Entity[]): void {
for (const entity of entities) {
const rigidbody = entity.getComponent(Rigidbody2DComponent);
const transform = entity.getComponent(TransformComponent);
const mapping = this._entityBodies.get(entity.id);
if (!rigidbody || !transform || !mapping) continue;
// 只有当需要同步时才更新物理世界
if (rigidbody._needsSync) {
const position: IVector2 = {
x: transform.position.x,
y: transform.position.y
};
const rotation = transform.rotation.z;
this._world.setBodyTransform(mapping.bodyHandle, position, rotation);
rigidbody._needsSync = false;
}
// 检查碰撞体是否需要重建
const colliders = this._getColliders(entity);
const scale: IVector2 = { x: transform.scale.x, y: transform.scale.y };
for (const collider of colliders) {
if (collider._needsRebuild) {
// 移除旧碰撞体
if (collider._colliderHandle !== null) {
this._world.removeCollider(collider._colliderHandle);
const handleIndex = mapping.colliderHandles.indexOf(collider._colliderHandle);
if (handleIndex >= 0) {
mapping.colliderHandles.splice(handleIndex, 1);
}
}
// 创建新碰撞体
const newHandle = this._world.createCollider(entity.id, collider, mapping.bodyHandle, scale);
if (newHandle !== null) {
mapping.colliderHandles.push(newHandle);
}
collider._needsRebuild = false;
}
}
}
}
/**
* 应用待处理的力和冲量
*/
private _applyPendingForces(entities: readonly Entity[]): void {
for (const entity of entities) {
const rigidbody = entity.getComponent(Rigidbody2DComponent);
const mapping = this._entityBodies.get(entity.id);
if (!rigidbody || !mapping) continue;
const bodyHandle = mapping.bodyHandle;
// 应用力
if (rigidbody._pendingForce.x !== 0 || rigidbody._pendingForce.y !== 0) {
this._world.applyForce(bodyHandle, rigidbody._pendingForce);
}
// 应用冲量
if (rigidbody._pendingImpulse.x !== 0 || rigidbody._pendingImpulse.y !== 0) {
this._world.applyImpulse(bodyHandle, rigidbody._pendingImpulse);
}
// 应用扭矩
if (rigidbody._pendingTorque !== 0) {
this._world.applyTorque(bodyHandle, rigidbody._pendingTorque);
}
// 应用角冲量
if (rigidbody._pendingAngularImpulse !== 0) {
this._world.applyAngularImpulse(bodyHandle, rigidbody._pendingAngularImpulse);
}
// 设置目标速度
if (rigidbody._hasTargetVelocity) {
this._world.setVelocity(bodyHandle, rigidbody._targetVelocity);
}
// 设置目标角速度
if (rigidbody._hasTargetAngularVelocity) {
this._world.setAngularVelocity(bodyHandle, rigidbody._targetAngularVelocity);
}
// 唤醒/休眠
if (rigidbody._shouldWakeUp) {
this._world.wakeUp(bodyHandle);
}
if (rigidbody._shouldSleep) {
this._world.sleep(bodyHandle);
}
// 清除待处理状态
rigidbody._clearPendingForces();
}
}
/**
* 同步物理世界到 Transform
*/
private _syncPhysicsToTransforms(entities: readonly Entity[]): void {
for (const entity of entities) {
const rigidbody = entity.getComponent(Rigidbody2DComponent);
const transform = entity.getComponent(TransformComponent);
const mapping = this._entityBodies.get(entity.id);
if (!rigidbody || !transform || !mapping) continue;
// 存储上一帧位置用于插值
rigidbody._previousPosition = {
x: transform.position.x,
y: transform.position.y
};
rigidbody._previousRotation = transform.rotation.z;
// 从物理世界获取新位置
const newPosition = this._world.getBodyPosition(mapping.bodyHandle);
const newRotation = this._world.getBodyRotation(mapping.bodyHandle);
const newVelocity = this._world.getBodyVelocity(mapping.bodyHandle);
const newAngularVelocity = this._world.getBodyAngularVelocity(mapping.bodyHandle);
if (newPosition) {
transform.position.x = newPosition.x;
transform.position.y = newPosition.y;
}
if (newRotation !== null) {
transform.rotation.z = newRotation;
}
if (newVelocity) {
rigidbody.velocity = newVelocity;
}
if (newAngularVelocity !== null) {
rigidbody.angularVelocity = newAngularVelocity;
}
}
}
/**
* 处理碰撞事件
*/
private _handleCollisionEvent(event: CollisionEvent2D): void {
if (!this.scene) return;
let eventName: string;
switch (event.type) {
case 'enter':
eventName = PHYSICS_EVENTS.COLLISION_ENTER;
break;
case 'stay':
eventName = PHYSICS_EVENTS.COLLISION_STAY;
break;
case 'exit':
eventName = PHYSICS_EVENTS.COLLISION_EXIT;
break;
}
this.scene.eventSystem.emitSync(eventName, event);
}
/**
* 处理触发器事件
*/
private _handleTriggerEvent(event: TriggerEvent2D): void {
if (!this.scene) return;
let eventName: string;
switch (event.type) {
case 'enter':
eventName = PHYSICS_EVENTS.TRIGGER_ENTER;
break;
case 'stay':
eventName = PHYSICS_EVENTS.TRIGGER_STAY;
break;
case 'exit':
eventName = PHYSICS_EVENTS.TRIGGER_EXIT;
break;
}
this.scene.eventSystem.emitSync(eventName, event);
}
/**
* 获取实体上的所有碰撞体组件
* @param entity 实体
*/
private _getColliders(entity: Entity): Collider2DBase[] {
const colliders: Collider2DBase[] = [];
// 收集所有类型的碰撞体
colliders.push(...entity.getComponents(BoxCollider2DComponent));
colliders.push(...entity.getComponents(CircleCollider2DComponent));
colliders.push(...entity.getComponents(CapsuleCollider2DComponent));
colliders.push(...entity.getComponents(PolygonCollider2DComponent));
return colliders;
}
}

View File

@@ -0,0 +1,5 @@
/**
* Physics 2D Systems exports
*/
export { Physics2DSystem, type Physics2DSystemConfig } from './Physics2DSystem';

View File

@@ -0,0 +1,213 @@
/**
* 物理模块服务令牌
* Physics module service tokens
*
* 定义 physics-rapier2d 模块导出的服务令牌和接口。
* 谁定义接口,谁导出 Token。
*
* Defines service tokens and interfaces exported by physics-rapier2d module.
* Who defines the interface, who exports the Token.
*
* @example
* ```typescript
* // 消费方导入 Token | Consumer imports Token
* import { Physics2DQueryToken, type IPhysics2DQuery } from '@esengine/physics-rapier2d';
*
* // 获取服务 | Get service
* const physicsQuery = context.services.get(Physics2DQueryToken);
* if (physicsQuery) {
* physicsQuery.raycast(...);
* }
* ```
*/
import { createServiceToken } from '@esengine/ecs-framework';
import type { Physics2DSystem } from './systems/Physics2DSystem';
// ============================================================================
// 共享物理接口 | Shared Physics Interfaces
// ============================================================================
/**
* 2D 物理查询接口
* 2D Physics query interface
*
* 跨模块共享的物理查询契约。
* 由 Physics2DWorld 实现,粒子等模块可选依赖。
*
* Cross-module shared physics query contract.
* Implemented by Physics2DWorld, optionally depended by particle and other modules.
*/
export interface IPhysics2DQuery {
/**
* 圆形区域检测
* Circle overlap detection
*
* @param center 圆心 | Circle center
* @param radius 半径 | Radius
* @param collisionMask 碰撞掩码 | Collision mask
* @returns 检测结果 | Detection result
*/
overlapCircle(
center: { x: number; y: number },
radius: number,
collisionMask?: number
): { entityIds: number[]; colliderHandles: number[] };
/**
* 射线检测
* Raycast detection
*
* @param origin 起点 | Origin point
* @param direction 方向(归一化)| Direction (normalized)
* @param maxDistance 最大距离 | Max distance
* @param collisionMask 碰撞掩码 | Collision mask
* @returns 命中结果或 null | Hit result or null
*/
raycast(
origin: { x: number; y: number },
direction: { x: number; y: number },
maxDistance: number,
collisionMask?: number
): {
entityId: number;
point: { x: number; y: number };
normal: { x: number; y: number };
distance: number;
colliderHandle: number;
} | null;
}
/**
* 2D 物理世界接口
* 2D Physics world interface
*
* 跨模块共享的物理世界契约。
* 由 Physics2DWorld 实现tilemap 等模块可选依赖。
*
* Cross-module shared physics world contract.
* Implemented by Physics2DWorld, optionally depended by tilemap and other modules.
*/
export interface IPhysics2DWorld {
/**
* 创建静态碰撞体
* Create static collider
*
* @param entityId 实体 ID | Entity ID
* @param position 碰撞体中心位置 | Collider center position
* @param halfExtents 半宽高 | Half extents
* @param collisionLayer 碰撞层 | Collision layer
* @param collisionMask 碰撞掩码 | Collision mask
* @param friction 摩擦系数 | Friction coefficient
* @param restitution 弹性系数 | Restitution coefficient
* @param isTrigger 是否为触发器 | Whether is trigger
* @returns 碰撞体句柄或 null | Collider handle or null
*/
createStaticCollider(
entityId: number,
position: { x: number; y: number },
halfExtents: { x: number; y: number },
collisionLayer: number,
collisionMask: number,
friction: number,
restitution: number,
isTrigger: boolean
): number | null;
/**
* 移除碰撞体
* Remove collider
*
* @param handle 碰撞体句柄 | Collider handle
*/
removeCollider(handle: number): void;
}
// ============================================================================
// 物理模块特有的接口 | Physics module specific interfaces
// ============================================================================
/**
* 物理配置
* Physics configuration
*/
export interface PhysicsConfig {
gravity?: { x: number; y: number };
timestep?: number;
}
/**
* 碰撞层配置接口
* Collision layer config interface
*
* 跨模块共享的碰撞层配置契约。
* Cross-module shared collision layer config contract.
*/
export interface ICollisionLayerConfig {
/**
* 获取所有层定义
* Get all layer definitions
*/
getLayers(): ReadonlyArray<{ name: string }>;
/**
* 添加监听器
* Add listener
*/
addListener(callback: () => void): void;
/**
* 移除监听器
* Remove listener
*/
removeListener(callback: () => void): void;
}
// ============================================================================
// 服务令牌 | Service Tokens
// ============================================================================
/**
* 2D 物理查询服务令牌
* 2D Physics query service token
*
* 用于获取物理查询能力(射线检测、区域检测等)。
* For getting physics query capabilities (raycast, overlap detection, etc.).
*/
export const Physics2DQueryToken = createServiceToken<IPhysics2DQuery>('physics2DQuery');
/**
* 2D 物理世界服务令牌
* 2D Physics world service token
*
* 用于获取物理世界实例(需要底层访问时使用)。
* For getting physics world instance (when low-level access is needed).
*/
export const Physics2DWorldToken = createServiceToken<IPhysics2DWorld>('physics2DWorld');
/**
* 物理系统令牌
* Physics system token
*
* 用于获取完整的物理系统实例。
* For getting the full physics system instance.
*/
export const Physics2DSystemToken = createServiceToken<Physics2DSystem>('physics2DSystem');
/**
* 物理配置令牌
* Physics config token
*
* 用于传入物理配置(如重力、时间步)。
* For passing physics configuration (gravity, timestep, etc.).
*/
export const PhysicsConfigToken = createServiceToken<PhysicsConfig>('physicsConfig');
/**
* 碰撞层配置令牌
* Collision layer config token
*
* 用于获取碰撞层配置服务。
* For getting collision layer config service.
*/
export const CollisionLayerConfigToken = createServiceToken<ICollisionLayerConfig>('collisionLayerConfig');

View File

@@ -0,0 +1,104 @@
/**
* Physics 2D Events
* 2D 物理事件定义
*/
import type { IVector2 } from '@esengine/ecs-framework-math';
/**
* 碰撞事件类型
*/
export type CollisionEventType = 'enter' | 'stay' | 'exit';
/**
* 触发器事件类型
*/
export type TriggerEventType = 'enter' | 'stay' | 'exit';
/**
* 碰撞接触点信息
*/
export interface ContactPoint2D {
/** 接触点位置 */
point: IVector2;
/** 接触点法线 */
normal: IVector2;
/** 穿透深度 */
penetration: number;
/** 冲量大小 */
impulse: number;
}
/**
* 碰撞事件数据
*/
export interface CollisionEvent2D {
/** 事件类型 */
type: CollisionEventType;
/** 实体 A 的 ID */
entityA: number;
/** 实体 B 的 ID */
entityB: number;
/** 碰撞体 A 的句柄 */
colliderHandleA: number;
/** 碰撞体 B 的句柄 */
colliderHandleB: number;
/** 接触点列表(仅在 enter 和 stay 时有效) */
contacts: ContactPoint2D[];
/** 相对速度 */
relativeVelocity: IVector2;
/** 总冲量大小 */
totalImpulse: number;
}
/**
* 触发器事件数据
*/
export interface TriggerEvent2D {
/** 事件类型 */
type: TriggerEventType;
/** 触发器实体 ID */
triggerEntityId: number;
/** 进入触发器的实体 ID */
otherEntityId: number;
/** 触发器碰撞体句柄 */
triggerColliderHandle: number;
/** 其他碰撞体句柄 */
otherColliderHandle: number;
}
/**
* 物理事件名称常量
*/
export const PHYSICS_EVENTS = {
/** 碰撞开始 */
COLLISION_ENTER: 'physics2d:collision-enter',
/** 碰撞持续 */
COLLISION_STAY: 'physics2d:collision-stay',
/** 碰撞结束 */
COLLISION_EXIT: 'physics2d:collision-exit',
/** 触发器进入 */
TRIGGER_ENTER: 'physics2d:trigger-enter',
/** 触发器持续 */
TRIGGER_STAY: 'physics2d:trigger-stay',
/** 触发器离开 */
TRIGGER_EXIT: 'physics2d:trigger-exit',
/** 物理世界步进前 */
PRE_STEP: 'physics2d:pre-step',
/** 物理世界步进后 */
POST_STEP: 'physics2d:post-step'
} as const;
/**
* 物理事件映射类型
*/
export interface Physics2DEventMap {
[PHYSICS_EVENTS.COLLISION_ENTER]: CollisionEvent2D;
[PHYSICS_EVENTS.COLLISION_STAY]: CollisionEvent2D;
[PHYSICS_EVENTS.COLLISION_EXIT]: CollisionEvent2D;
[PHYSICS_EVENTS.TRIGGER_ENTER]: TriggerEvent2D;
[PHYSICS_EVENTS.TRIGGER_STAY]: TriggerEvent2D;
[PHYSICS_EVENTS.TRIGGER_EXIT]: TriggerEvent2D;
[PHYSICS_EVENTS.PRE_STEP]: { deltaTime: number };
[PHYSICS_EVENTS.POST_STEP]: { deltaTime: number };
}

View File

@@ -0,0 +1,178 @@
/**
* Physics 2D Types
* 2D 物理引擎类型定义
*/
/**
* 刚体类型
*/
export enum RigidbodyType2D {
/** 动态刚体,受力和碰撞影响 */
Dynamic = 0,
/** 运动学刚体,手动控制位置,不受力影响 */
Kinematic = 1,
/** 静态刚体,不移动,用于地形等 */
Static = 2
}
/**
* 碰撞检测模式
*/
export enum CollisionDetectionMode2D {
/** 离散检测,性能好但可能穿透 */
Discrete = 0,
/** 连续检测,防止高速物体穿透 */
Continuous = 1
}
// 使用 IVector2 接口 | Use IVector2 interface
import type { IVector2 } from '@esengine/ecs-framework-math';
/**
* 物理配置
*/
export interface Physics2DConfig {
/** 重力向量 */
gravity: IVector2;
/** 固定时间步长(秒) */
timestep: number;
/** 每帧最大子步数 */
maxSubsteps: number;
/** 速度求解器迭代次数 */
velocityIterations: number;
/** 位置求解器迭代次数 */
positionIterations: number;
/** 是否启用休眠 */
allowSleep: boolean;
}
/**
* 默认物理配置
*/
export const DEFAULT_PHYSICS_CONFIG: Physics2DConfig = {
gravity: { x: 0, y: -9.81 },
timestep: 1 / 60,
maxSubsteps: 8,
velocityIterations: 4,
positionIterations: 1,
allowSleep: true
};
/**
* 碰撞层定义
*/
export enum CollisionLayer2D {
Default = 1 << 0,
Player = 1 << 1,
Enemy = 1 << 2,
Projectile = 1 << 3,
Ground = 1 << 4,
Platform = 1 << 5,
Trigger = 1 << 6,
All = 0xFFFF
}
/**
* 力的模式
*/
export enum ForceMode2D {
/** 持续力(考虑质量) */
Force = 0,
/** 瞬时冲量(考虑质量) */
Impulse = 1,
/** 直接设置速度变化(不考虑质量) */
VelocityChange = 2,
/** 持续加速度(不考虑质量) */
Acceleration = 3
}
/**
* 射线检测结果
*/
export interface RaycastHit2D {
/** 命中的实体 ID */
entityId: number;
/** 命中点 */
point: IVector2;
/** 命中面的法线 */
normal: IVector2;
/** 射线起点到命中点的距离 */
distance: number;
/** 命中的碰撞体句柄 */
colliderHandle: number;
}
/**
* 形状投射结果
*/
export interface ShapeCastHit2D extends RaycastHit2D {
/** 投射开始时与命中物体的穿透深度 */
penetration: number;
}
/**
* 重叠检测结果
*/
export interface OverlapResult2D {
/** 重叠的实体 ID 列表 */
entityIds: number[];
/** 重叠的碰撞体句柄列表 */
colliderHandles: number[];
}
/**
* 物理材质预设
*/
export enum PhysicsMaterial2DPreset {
/** 默认材质 */
Default = 0,
/** 弹性材质 */
Bouncy = 1,
/** 光滑材质(低摩擦) */
Slippery = 2,
/** 粘性材质(高摩擦) */
Sticky = 3,
/** 金属材质 */
Metal = 4,
/** 橡胶材质 */
Rubber = 5
}
/**
* 获取预设材质参数
*/
export function getPhysicsMaterialPreset(preset: PhysicsMaterial2DPreset): { friction: number; restitution: number } {
switch (preset) {
case PhysicsMaterial2DPreset.Bouncy:
return { friction: 0.2, restitution: 0.9 };
case PhysicsMaterial2DPreset.Slippery:
return { friction: 0.05, restitution: 0.1 };
case PhysicsMaterial2DPreset.Sticky:
return { friction: 1.0, restitution: 0.0 };
case PhysicsMaterial2DPreset.Metal:
return { friction: 0.4, restitution: 0.3 };
case PhysicsMaterial2DPreset.Rubber:
return { friction: 0.8, restitution: 0.7 };
case PhysicsMaterial2DPreset.Default:
default:
return { friction: 0.5, restitution: 0.0 };
}
}
/**
* 关节类型
*/
export enum JointType2D {
/** 固定关节 */
Fixed = 0,
/** 铰链关节(旋转) */
Revolute = 1,
/** 棱柱关节(滑动) */
Prismatic = 2,
/** 弹簧关节 */
Spring = 3,
/** 绳索关节 */
Rope = 4,
/** 距离关节 */
Distance = 5
}

View File

@@ -0,0 +1,31 @@
/**
* Physics 2D Types exports
*/
export {
RigidbodyType2D,
CollisionDetectionMode2D,
type Physics2DConfig,
DEFAULT_PHYSICS_CONFIG,
CollisionLayer2D,
ForceMode2D,
type RaycastHit2D,
type ShapeCastHit2D,
type OverlapResult2D,
PhysicsMaterial2DPreset,
getPhysicsMaterialPreset,
JointType2D
} from './Physics2DTypes';
// Re-export IVector2 for type compatibility
export type { IVector2 } from '@esengine/ecs-framework-math';
export {
type CollisionEventType,
type TriggerEventType,
type ContactPoint2D,
type CollisionEvent2D,
type TriggerEvent2D,
PHYSICS_EVENTS,
type Physics2DEventMap
} from './Physics2DEvents';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
/**
* Physics 2D World exports
*/
export { Physics2DWorld, type Physics2DWorldState } from './Physics2DWorld';