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:
@@ -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;
|
||||
47
packages/physics/physics-rapier2d/src/PhysicsEditorPlugin.ts
Normal file
47
packages/physics/physics-rapier2d/src/PhysicsEditorPlugin.ts
Normal 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()
|
||||
};
|
||||
207
packages/physics/physics-rapier2d/src/PhysicsRuntimeModule.ts
Normal file
207
packages/physics/physics-rapier2d/src/PhysicsRuntimeModule.ts
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
11
packages/physics/physics-rapier2d/src/components/index.ts
Normal file
11
packages/physics/physics-rapier2d/src/components/index.ts
Normal 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';
|
||||
42
packages/physics/physics-rapier2d/src/index.ts
Normal file
42
packages/physics/physics-rapier2d/src/index.ts
Normal 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';
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
58
packages/physics/physics-rapier2d/src/loaders/index.ts
Normal file
58
packages/physics/physics-rapier2d/src/loaders/index.ts
Normal 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();
|
||||
65
packages/physics/physics-rapier2d/src/runtime.ts
Normal file
65
packages/physics/physics-rapier2d/src/runtime.ts
Normal 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';
|
||||
17
packages/physics/physics-rapier2d/src/runtime/index.ts
Normal file
17
packages/physics/physics-rapier2d/src/runtime/index.ts
Normal 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';
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
14
packages/physics/physics-rapier2d/src/services/index.ts
Normal file
14
packages/physics/physics-rapier2d/src/services/index.ts
Normal 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';
|
||||
565
packages/physics/physics-rapier2d/src/systems/Physics2DSystem.ts
Normal file
565
packages/physics/physics-rapier2d/src/systems/Physics2DSystem.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
5
packages/physics/physics-rapier2d/src/systems/index.ts
Normal file
5
packages/physics/physics-rapier2d/src/systems/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D Systems exports
|
||||
*/
|
||||
|
||||
export { Physics2DSystem, type Physics2DSystemConfig } from './Physics2DSystem';
|
||||
213
packages/physics/physics-rapier2d/src/tokens.ts
Normal file
213
packages/physics/physics-rapier2d/src/tokens.ts
Normal 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');
|
||||
104
packages/physics/physics-rapier2d/src/types/Physics2DEvents.ts
Normal file
104
packages/physics/physics-rapier2d/src/types/Physics2DEvents.ts
Normal 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 };
|
||||
}
|
||||
178
packages/physics/physics-rapier2d/src/types/Physics2DTypes.ts
Normal file
178
packages/physics/physics-rapier2d/src/types/Physics2DTypes.ts
Normal 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
|
||||
}
|
||||
31
packages/physics/physics-rapier2d/src/types/index.ts
Normal file
31
packages/physics/physics-rapier2d/src/types/index.ts
Normal 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';
|
||||
1206
packages/physics/physics-rapier2d/src/world/Physics2DWorld.ts
Normal file
1206
packages/physics/physics-rapier2d/src/world/Physics2DWorld.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
packages/physics/physics-rapier2d/src/world/index.ts
Normal file
5
packages/physics/physics-rapier2d/src/world/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D World exports
|
||||
*/
|
||||
|
||||
export { Physics2DWorld, type Physics2DWorldState } from './Physics2DWorld';
|
||||
Reference in New Issue
Block a user