feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 (#244)
* feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 * fix: 修复 CI 流程并清理代码
This commit is contained in:
78
packages/physics-rapier2d/package.json
Normal file
78
packages/physics-rapier2d/package.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "@esengine/physics-rapier2d",
|
||||
"version": "1.0.0",
|
||||
"description": "Deterministic 2D physics engine based on Rapier2D with enhanced-determinism",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"types": "./dist/runtime.d.ts",
|
||||
"import": "./dist/runtime.js"
|
||||
},
|
||||
"./editor": {
|
||||
"types": "./dist/editor/index.d.ts",
|
||||
"import": "./dist/editor/index.js"
|
||||
},
|
||||
"./plugin.json": "./plugin.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"plugin.json"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"physics",
|
||||
"rapier2d",
|
||||
"deterministic",
|
||||
"game-physics",
|
||||
"2d-physics"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.0.0",
|
||||
"@esengine/ecs-components": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"react": "^18.3.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@esengine/editor-core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@dimforge/rapier2d-compat": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-dts": "^4.5.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/physics-rapier2d"
|
||||
}
|
||||
}
|
||||
22
packages/physics-rapier2d/plugin.json
Normal file
22
packages/physics-rapier2d/plugin.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "@esengine/physics-rapier2d",
|
||||
"name": "Rapier 2D Physics",
|
||||
"version": "1.0.0",
|
||||
"description": "Deterministic 2D physics engine based on Rapier2D with enhanced-determinism support",
|
||||
"category": "physics",
|
||||
"loadingPhase": "default",
|
||||
"enabledByDefault": true,
|
||||
"canContainContent": false,
|
||||
"isEnginePlugin": true,
|
||||
"modules": [
|
||||
{
|
||||
"name": "PhysicsRuntime",
|
||||
"type": "runtime",
|
||||
"entry": "./src/runtime.ts"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
"@esengine/ecs-framework",
|
||||
"@esengine/ecs-components"
|
||||
]
|
||||
}
|
||||
50
packages/physics-rapier2d/src/Physics2DComponentsModule.ts
Normal file
50
packages/physics-rapier2d/src/Physics2DComponentsModule.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Physics 2D Components Module (Lightweight)
|
||||
* 2D 物理组件模块(轻量级)
|
||||
*
|
||||
* 仅注册组件,不包含 WASM 依赖
|
||||
* 用于编辑器中的组件序列化/反序列化
|
||||
*/
|
||||
|
||||
import { ComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModuleLoader } from '@esengine/ecs-components';
|
||||
|
||||
// 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 IRuntimeModuleLoader {
|
||||
/**
|
||||
* 注册组件到 ComponentRegistry
|
||||
*/
|
||||
registerComponents(registry: typeof ComponentRegistry): 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;
|
||||
117
packages/physics-rapier2d/src/PhysicsRuntimeModule.ts
Normal file
117
packages/physics-rapier2d/src/PhysicsRuntimeModule.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Physics 2D Runtime Module
|
||||
* 2D 物理运行时模块
|
||||
*
|
||||
* 提供确定性 2D 物理模拟功能
|
||||
*/
|
||||
|
||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { ComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components';
|
||||
import * as RAPIER from '@dimforge/rapier2d-compat';
|
||||
|
||||
// Components
|
||||
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';
|
||||
|
||||
// Systems
|
||||
import { Physics2DSystem } from './systems/Physics2DSystem';
|
||||
|
||||
// Services
|
||||
import { Physics2DService } from './services/Physics2DService';
|
||||
|
||||
/**
|
||||
* Physics 2D Runtime Module
|
||||
* 2D 物理运行时模块
|
||||
*
|
||||
* 实现 IRuntimeModuleLoader 接口,提供:
|
||||
* - 物理组件注册
|
||||
* - 物理系统创建
|
||||
* - Rapier2D 初始化
|
||||
*/
|
||||
export class PhysicsRuntimeModule implements IRuntimeModuleLoader {
|
||||
private _rapierModule: typeof RAPIER | null = null;
|
||||
private _physicsSystem: Physics2DSystem | null = null;
|
||||
|
||||
/**
|
||||
* 初始化模块
|
||||
* 异步初始化 Rapier2D WASM 模块
|
||||
*/
|
||||
async onInitialize(): Promise<void> {
|
||||
// 初始化 Rapier2D WASM
|
||||
await RAPIER.init();
|
||||
this._rapierModule = RAPIER;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册组件
|
||||
*/
|
||||
registerComponents(registry: typeof ComponentRegistry): void {
|
||||
registry.register(Rigidbody2DComponent);
|
||||
registry.register(BoxCollider2DComponent);
|
||||
registry.register(CircleCollider2DComponent);
|
||||
registry.register(CapsuleCollider2DComponent);
|
||||
registry.register(PolygonCollider2DComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册服务
|
||||
*/
|
||||
registerServices?(services: ServiceContainer): void {
|
||||
// 注册物理服务
|
||||
services.registerSingleton(Physics2DService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统
|
||||
*/
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
// 创建物理系统
|
||||
const physicsSystem = new Physics2DSystem({
|
||||
physics: context.physicsConfig,
|
||||
updateOrder: -1000 // 在其他系统之前运行
|
||||
});
|
||||
|
||||
scene.addSystem(physicsSystem);
|
||||
this._physicsSystem = physicsSystem;
|
||||
|
||||
// 如果 Rapier 已加载,初始化物理系统
|
||||
if (this._rapierModule) {
|
||||
physicsSystem.initializeWithRapier(this._rapierModule);
|
||||
}
|
||||
|
||||
// 导出到上下文供其他系统使用
|
||||
context.physicsSystem = physicsSystem;
|
||||
context.physics2DWorld = physicsSystem.world;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁模块
|
||||
*/
|
||||
onDestroy(): void {
|
||||
this._physicsSystem = null;
|
||||
this._rapierModule = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Rapier 模块
|
||||
*/
|
||||
getRapierModule(): typeof RAPIER | null {
|
||||
return this._rapierModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物理系统
|
||||
*/
|
||||
getPhysicsSystem(): Physics2DSystem | null {
|
||||
return this._physicsSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认导出模块实例
|
||||
*/
|
||||
export default new PhysicsRuntimeModule();
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* BoxCollider2D Component
|
||||
* 2D 矩形碰撞体组件
|
||||
*/
|
||||
|
||||
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
import type { Vector2 } from '../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 矩形宽度(半宽度的2倍)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0.01, step: 0.1 })
|
||||
public width: number = 10;
|
||||
|
||||
/**
|
||||
* 矩形高度(半高度的2倍)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Height', min: 0.01, step: 0.1 })
|
||||
public height: number = 10;
|
||||
|
||||
/**
|
||||
* 获取半宽度
|
||||
*/
|
||||
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: Vector2; max: Vector2 } {
|
||||
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;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* CapsuleCollider2D Component
|
||||
* 2D 胶囊碰撞体组件
|
||||
*/
|
||||
|
||||
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
import type { Vector2 } from '../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 胶囊方向
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* 胶囊半径
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Radius', min: 0.01, step: 0.1 })
|
||||
public radius: number = 3;
|
||||
|
||||
/**
|
||||
* 胶囊总高度(包括两端的半圆)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Height', min: 0.01, step: 0.1 })
|
||||
public height: number = 10;
|
||||
|
||||
/**
|
||||
* 胶囊方向
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Direction',
|
||||
options: [
|
||||
{ label: 'Vertical', value: 0 },
|
||||
{ label: 'Horizontal', value: 1 }
|
||||
]
|
||||
})
|
||||
public direction: CapsuleDirection2D = CapsuleDirection2D.Vertical;
|
||||
|
||||
/**
|
||||
* 获取半高度(中间矩形部分的一半)
|
||||
*/
|
||||
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: Vector2; max: Vector2 } {
|
||||
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;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置方向
|
||||
* @param direction 方向
|
||||
*/
|
||||
public setDirection(direction: CapsuleDirection2D): void {
|
||||
this.direction = direction;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* CircleCollider2D Component
|
||||
* 2D 圆形碰撞体组件
|
||||
*/
|
||||
|
||||
import { Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { Collider2DBase } from './Collider2DBase';
|
||||
import type { Vector2 } from '../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 圆的半径
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Radius', min: 0.01, step: 0.1 })
|
||||
public radius: number = 5;
|
||||
|
||||
public override getShapeType(): string {
|
||||
return 'circle';
|
||||
}
|
||||
|
||||
public override calculateArea(): number {
|
||||
return Math.PI * this.radius * this.radius;
|
||||
}
|
||||
|
||||
public override calculateAABB(): { min: Vector2; max: Vector2 } {
|
||||
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;
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
}
|
||||
186
packages/physics-rapier2d/src/components/Collider2DBase.ts
Normal file
186
packages/physics-rapier2d/src/components/Collider2DBase.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Collider2D Base Component
|
||||
* 2D 碰撞体基类组件
|
||||
*/
|
||||
|
||||
import { Component, Property, Serialize } from '@esengine/ecs-framework';
|
||||
import { Vector2, 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: 'integer', label: 'Collision Layer', min: 0 })
|
||||
public collisionLayer: number = CollisionLayer2D.Default;
|
||||
|
||||
/**
|
||||
* 碰撞掩码(该碰撞体可以与哪些层碰撞)
|
||||
* 使用位掩码
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Collision Mask', min: 0 })
|
||||
public collisionMask: number = CollisionLayer2D.All;
|
||||
|
||||
// ==================== 偏移 ====================
|
||||
|
||||
/**
|
||||
* 相对于实体 Transform 的位置偏移
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'vector2', label: 'Offset' })
|
||||
public offset: Vector2 = { 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: Vector2; max: Vector2 };
|
||||
|
||||
// ==================== 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 { Collider2DBase } from './Collider2DBase';
|
||||
import type { Vector2 } from '../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 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: Vector2[] = [
|
||||
{ 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: Vector2; max: Vector2 } {
|
||||
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: Vector2[]): 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: Vector2[] = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
321
packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts
Normal file
321
packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Rigidbody2D Component
|
||||
* 2D 刚体组件
|
||||
*/
|
||||
|
||||
import { Component, Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { RigidbodyType2D, CollisionDetectionMode2D, Vector2 } 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: Vector2 = { x: 0, y: 0 };
|
||||
|
||||
/**
|
||||
* 当前角速度(弧度/秒)
|
||||
*/
|
||||
public angularVelocity: number = 0;
|
||||
|
||||
// ==================== 内部状态 ====================
|
||||
|
||||
/**
|
||||
* Rapier 刚体句柄
|
||||
* @internal
|
||||
*/
|
||||
public _bodyHandle: number | null = null;
|
||||
|
||||
/**
|
||||
* 是否需要同步 Transform 到物理世界
|
||||
* @internal
|
||||
*/
|
||||
public _needsSync: boolean = true;
|
||||
|
||||
/**
|
||||
* 上一帧的位置(用于插值)
|
||||
* @internal
|
||||
*/
|
||||
public _previousPosition: Vector2 = { x: 0, y: 0 };
|
||||
|
||||
/**
|
||||
* 上一帧的旋转角度
|
||||
* @internal
|
||||
*/
|
||||
public _previousRotation: number = 0;
|
||||
|
||||
// ==================== API 方法 ====================
|
||||
|
||||
/**
|
||||
* 添加力(在下一个物理步进中应用)
|
||||
* 这是一个标记方法,实际力的应用由 Physics2DSystem 处理
|
||||
*/
|
||||
public addForce(force: Vector2): void {
|
||||
this._pendingForce.x += force.x;
|
||||
this._pendingForce.y += force.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加冲量(立即改变速度)
|
||||
*/
|
||||
public addImpulse(impulse: Vector2): 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: Vector2): 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: Vector2 = { x: 0, y: 0 };
|
||||
/** @internal */
|
||||
public _pendingImpulse: Vector2 = { x: 0, y: 0 };
|
||||
/** @internal */
|
||||
public _pendingTorque: number = 0;
|
||||
/** @internal */
|
||||
public _pendingAngularImpulse: number = 0;
|
||||
/** @internal */
|
||||
public _targetVelocity: Vector2 = { 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-rapier2d/src/components/index.ts
Normal file
11
packages/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';
|
||||
59
packages/physics-rapier2d/src/editor/Physics2DPlugin.ts
Normal file
59
packages/physics-rapier2d/src/editor/Physics2DPlugin.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Physics 2D Unified Plugin
|
||||
* 2D 物理统一插件
|
||||
*
|
||||
* 编辑器专用插件入口
|
||||
* 使用完整运行时模块以支持编辑器预览
|
||||
*/
|
||||
|
||||
import type { IPluginLoader, PluginDescriptor } from '@esengine/editor-core';
|
||||
import { Physics2DEditorModule } from './index';
|
||||
import { PhysicsRuntimeModule } from '../PhysicsRuntimeModule';
|
||||
|
||||
/**
|
||||
* 插件描述符
|
||||
*/
|
||||
const descriptor: PluginDescriptor = {
|
||||
id: '@esengine/physics-rapier2d',
|
||||
name: 'Rapier 2D Physics',
|
||||
version: '1.0.0',
|
||||
description: '基于 Rapier2D 的确定性 2D 物理引擎',
|
||||
category: 'physics',
|
||||
enabledByDefault: true,
|
||||
canContainContent: false,
|
||||
isEnginePlugin: true,
|
||||
modules: [
|
||||
{
|
||||
name: 'PhysicsRuntime',
|
||||
type: 'runtime',
|
||||
loadingPhase: 'default',
|
||||
entry: './src/runtime.ts'
|
||||
},
|
||||
{
|
||||
name: 'PhysicsEditor',
|
||||
type: 'editor',
|
||||
loadingPhase: 'default',
|
||||
entry: './src/editor/index.ts'
|
||||
}
|
||||
],
|
||||
dependencies: [
|
||||
{ id: '@esengine/ecs-framework', version: '^2.0.0' },
|
||||
{ id: '@esengine/ecs-components', version: '^1.0.0' }
|
||||
],
|
||||
icon: 'Atom'
|
||||
};
|
||||
|
||||
/**
|
||||
* Physics 2D Plugin Loader
|
||||
* 2D 物理插件加载器
|
||||
*
|
||||
* - runtimeModule: 完整运行时模块(含 WASM 物理系统),支持编辑器预览和游戏运行
|
||||
* - editorModule: 编辑器功能模块(检视器、Gizmo、实体模板等)
|
||||
*/
|
||||
export const Physics2DPlugin: IPluginLoader = {
|
||||
descriptor,
|
||||
editorModule: new Physics2DEditorModule(),
|
||||
runtimeModule: new PhysicsRuntimeModule(),
|
||||
};
|
||||
|
||||
export default Physics2DPlugin;
|
||||
351
packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts
Normal file
351
packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* Physics 2D Gizmo Implementation
|
||||
* 2D 物理 Gizmo 实现
|
||||
*
|
||||
* Registers gizmo providers for physics components using the GizmoRegistry.
|
||||
* Rendered via Rust WebGL engine for optimal performance.
|
||||
* 使用 GizmoRegistry 为物理组件注册 gizmo 提供者。
|
||||
* 通过 Rust WebGL 引擎渲染以获得最佳性能。
|
||||
*/
|
||||
|
||||
import type { Entity } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IGizmoRenderData,
|
||||
IRectGizmoData,
|
||||
ICircleGizmoData,
|
||||
ILineGizmoData,
|
||||
GizmoColor
|
||||
} from '@esengine/editor-core';
|
||||
import { GizmoColors, GizmoRegistry } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/ecs-components';
|
||||
|
||||
import { BoxCollider2DComponent } from '../../components/BoxCollider2DComponent';
|
||||
import { CircleCollider2DComponent } from '../../components/CircleCollider2DComponent';
|
||||
import { CapsuleCollider2DComponent } from '../../components/CapsuleCollider2DComponent';
|
||||
import { PolygonCollider2DComponent } from '../../components/PolygonCollider2DComponent';
|
||||
import { Rigidbody2DComponent } from '../../components/Rigidbody2DComponent';
|
||||
import { RigidbodyType2D } from '../../types/Physics2DTypes';
|
||||
|
||||
/**
|
||||
* Collider gizmo color based on selection state
|
||||
* 根据选择状态设置碰撞体 gizmo 颜色
|
||||
*/
|
||||
function getColliderColor(isSelected: boolean, isTrigger: boolean): GizmoColor {
|
||||
if (isTrigger) {
|
||||
return isSelected
|
||||
? { r: 1, g: 0.5, b: 0, a: 0.8 } // Orange for selected trigger
|
||||
: { r: 1, g: 0.5, b: 0, a: 0.4 }; // Semi-transparent orange for unselected trigger
|
||||
}
|
||||
return isSelected
|
||||
? GizmoColors.collider // Cyan for selected collider
|
||||
: { ...GizmoColors.collider, a: 0.4 }; // Semi-transparent cyan for unselected
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigidbody indicator color based on body type
|
||||
* 根据刚体类型设置指示器颜色
|
||||
*/
|
||||
function getRigidbodyColor(bodyType: RigidbodyType2D, isSelected: boolean): GizmoColor {
|
||||
const alpha = isSelected ? 0.8 : 0.4;
|
||||
switch (bodyType) {
|
||||
case RigidbodyType2D.Dynamic:
|
||||
return { r: 0, g: 0.8, b: 1, a: alpha }; // Light blue for dynamic
|
||||
case RigidbodyType2D.Kinematic:
|
||||
return { r: 1, g: 0.8, b: 0, a: alpha }; // Yellow for kinematic
|
||||
case RigidbodyType2D.Static:
|
||||
return { r: 0.5, g: 0.5, b: 0.5, a: alpha }; // Gray for static
|
||||
default:
|
||||
return { r: 1, g: 1, b: 1, a: alpha };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BoxCollider2D gizmo provider
|
||||
* 矩形碰撞体 gizmo 提供者
|
||||
*/
|
||||
function boxCollider2DGizmoProvider(
|
||||
collider: BoxCollider2DComponent,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) return [];
|
||||
|
||||
const gizmos: IGizmoRenderData[] = [];
|
||||
const color = getColliderColor(isSelected, collider.isTrigger);
|
||||
|
||||
// Get rotation (handle both number and Vector3)
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
|
||||
// Calculate world position with offset
|
||||
const worldX = transform.position.x + collider.offset.x * transform.scale.x;
|
||||
const worldY = transform.position.y + collider.offset.y * transform.scale.y;
|
||||
|
||||
const rectGizmo: IRectGizmoData = {
|
||||
type: 'rect',
|
||||
x: worldX,
|
||||
y: worldY,
|
||||
width: collider.width * transform.scale.x,
|
||||
height: collider.height * transform.scale.y,
|
||||
rotation: rotation + collider.rotationOffset,
|
||||
originX: 0.5,
|
||||
originY: 0.5,
|
||||
color,
|
||||
showHandles: false
|
||||
};
|
||||
gizmos.push(rectGizmo);
|
||||
|
||||
return gizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* CircleCollider2D gizmo provider
|
||||
* 圆形碰撞体 gizmo 提供者
|
||||
*/
|
||||
function circleCollider2DGizmoProvider(
|
||||
collider: CircleCollider2DComponent,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) return [];
|
||||
|
||||
const gizmos: IGizmoRenderData[] = [];
|
||||
const color = getColliderColor(isSelected, collider.isTrigger);
|
||||
|
||||
// Calculate world position with offset
|
||||
const worldX = transform.position.x + collider.offset.x * transform.scale.x;
|
||||
const worldY = transform.position.y + collider.offset.y * transform.scale.y;
|
||||
|
||||
// Use the larger scale for radius (circles should remain circular)
|
||||
const scale = Math.max(Math.abs(transform.scale.x), Math.abs(transform.scale.y));
|
||||
|
||||
const circleGizmo: ICircleGizmoData = {
|
||||
type: 'circle',
|
||||
x: worldX,
|
||||
y: worldY,
|
||||
radius: collider.radius * scale,
|
||||
color
|
||||
};
|
||||
gizmos.push(circleGizmo);
|
||||
|
||||
return gizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* CapsuleCollider2D gizmo provider
|
||||
* 胶囊碰撞体 gizmo 提供者
|
||||
*/
|
||||
function capsuleCollider2DGizmoProvider(
|
||||
collider: CapsuleCollider2DComponent,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) return [];
|
||||
|
||||
const gizmos: IGizmoRenderData[] = [];
|
||||
const color = getColliderColor(isSelected, collider.isTrigger);
|
||||
|
||||
// Get rotation
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
const totalRotation = rotation + collider.rotationOffset;
|
||||
|
||||
// Calculate world position with offset
|
||||
const worldX = transform.position.x + collider.offset.x * transform.scale.x;
|
||||
const worldY = transform.position.y + collider.offset.y * transform.scale.y;
|
||||
|
||||
const radius = collider.radius * transform.scale.x;
|
||||
const halfHeight = collider.halfHeight * transform.scale.y;
|
||||
|
||||
// Draw capsule as two circles and connecting lines
|
||||
// 绘制胶囊体为两个圆和连接线
|
||||
const cos = Math.cos(totalRotation);
|
||||
const sin = Math.sin(totalRotation);
|
||||
|
||||
// Top circle center
|
||||
const topCenterX = worldX - sin * halfHeight;
|
||||
const topCenterY = worldY + cos * halfHeight;
|
||||
|
||||
// Bottom circle center
|
||||
const bottomCenterX = worldX + sin * halfHeight;
|
||||
const bottomCenterY = worldY - cos * halfHeight;
|
||||
|
||||
// Top semicircle
|
||||
gizmos.push({
|
||||
type: 'circle',
|
||||
x: topCenterX,
|
||||
y: topCenterY,
|
||||
radius,
|
||||
color
|
||||
} as ICircleGizmoData);
|
||||
|
||||
// Bottom semicircle
|
||||
gizmos.push({
|
||||
type: 'circle',
|
||||
x: bottomCenterX,
|
||||
y: bottomCenterY,
|
||||
radius,
|
||||
color
|
||||
} as ICircleGizmoData);
|
||||
|
||||
// Connecting lines (left and right sides)
|
||||
const perpX = cos * radius;
|
||||
const perpY = sin * radius;
|
||||
|
||||
gizmos.push({
|
||||
type: 'line',
|
||||
points: [
|
||||
{ x: topCenterX - perpX, y: topCenterY - perpY },
|
||||
{ x: bottomCenterX - perpX, y: bottomCenterY - perpY }
|
||||
],
|
||||
color,
|
||||
closed: false
|
||||
} as ILineGizmoData);
|
||||
|
||||
gizmos.push({
|
||||
type: 'line',
|
||||
points: [
|
||||
{ x: topCenterX + perpX, y: topCenterY + perpY },
|
||||
{ x: bottomCenterX + perpX, y: bottomCenterY + perpY }
|
||||
],
|
||||
color,
|
||||
closed: false
|
||||
} as ILineGizmoData);
|
||||
|
||||
return gizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* PolygonCollider2D gizmo provider
|
||||
* 多边形碰撞体 gizmo 提供者
|
||||
*/
|
||||
function polygonCollider2DGizmoProvider(
|
||||
collider: PolygonCollider2DComponent,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) return [];
|
||||
|
||||
if (collider.vertices.length < 3) return [];
|
||||
|
||||
const gizmos: IGizmoRenderData[] = [];
|
||||
const color = getColliderColor(isSelected, collider.isTrigger);
|
||||
|
||||
// Get rotation
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
const totalRotation = rotation + collider.rotationOffset;
|
||||
const cos = Math.cos(totalRotation);
|
||||
const sin = Math.sin(totalRotation);
|
||||
|
||||
// Transform vertices to world space
|
||||
const worldPoints = collider.vertices.map(v => {
|
||||
// Apply scale
|
||||
const scaledX = (v.x + collider.offset.x) * transform.scale.x;
|
||||
const scaledY = (v.y + collider.offset.y) * transform.scale.y;
|
||||
|
||||
// Apply rotation
|
||||
const rotatedX = scaledX * cos - scaledY * sin;
|
||||
const rotatedY = scaledX * sin + scaledY * cos;
|
||||
|
||||
// Apply translation
|
||||
return {
|
||||
x: transform.position.x + rotatedX,
|
||||
y: transform.position.y + rotatedY
|
||||
};
|
||||
});
|
||||
|
||||
gizmos.push({
|
||||
type: 'line',
|
||||
points: worldPoints,
|
||||
color,
|
||||
closed: true
|
||||
} as ILineGizmoData);
|
||||
|
||||
return gizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigidbody2D gizmo provider - shows velocity indicator when playing
|
||||
* 刚体 gizmo 提供者 - 播放时显示速度指示器
|
||||
*/
|
||||
function rigidbody2DGizmoProvider(
|
||||
rigidbody: Rigidbody2DComponent,
|
||||
entity: Entity,
|
||||
isSelected: boolean
|
||||
): IGizmoRenderData[] {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) return [];
|
||||
|
||||
const gizmos: IGizmoRenderData[] = [];
|
||||
|
||||
// Only show velocity indicator when selected and has significant velocity
|
||||
if (isSelected) {
|
||||
const velMagnitude = Math.sqrt(
|
||||
rigidbody.velocity.x * rigidbody.velocity.x +
|
||||
rigidbody.velocity.y * rigidbody.velocity.y
|
||||
);
|
||||
|
||||
// Draw velocity indicator if moving
|
||||
if (velMagnitude > 0.1) {
|
||||
const color = getRigidbodyColor(rigidbody.bodyType, isSelected);
|
||||
const scale = 0.5; // Scale factor for velocity visualization
|
||||
|
||||
gizmos.push({
|
||||
type: 'line',
|
||||
points: [
|
||||
{ x: transform.position.x, y: transform.position.y },
|
||||
{
|
||||
x: transform.position.x + rigidbody.velocity.x * scale,
|
||||
y: transform.position.y + rigidbody.velocity.y * scale
|
||||
}
|
||||
],
|
||||
color,
|
||||
closed: false
|
||||
} as ILineGizmoData);
|
||||
}
|
||||
|
||||
// Show body type indicator as small marker
|
||||
const markerColor = getRigidbodyColor(rigidbody.bodyType, true);
|
||||
gizmos.push({
|
||||
type: 'circle',
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
radius: 0.1,
|
||||
color: markerColor
|
||||
} as ICircleGizmoData);
|
||||
}
|
||||
|
||||
return gizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register gizmo providers for all physics components.
|
||||
* 为所有物理组件注册 gizmo 提供者。
|
||||
*/
|
||||
export function registerPhysics2DGizmos(): void {
|
||||
GizmoRegistry.register(BoxCollider2DComponent, boxCollider2DGizmoProvider);
|
||||
GizmoRegistry.register(CircleCollider2DComponent, circleCollider2DGizmoProvider);
|
||||
GizmoRegistry.register(CapsuleCollider2DComponent, capsuleCollider2DGizmoProvider);
|
||||
GizmoRegistry.register(PolygonCollider2DComponent, polygonCollider2DGizmoProvider);
|
||||
GizmoRegistry.register(Rigidbody2DComponent, rigidbody2DGizmoProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister gizmo providers for all physics components.
|
||||
* 取消注册所有物理组件的 gizmo 提供者。
|
||||
*/
|
||||
export function unregisterPhysics2DGizmos(): void {
|
||||
GizmoRegistry.unregister(BoxCollider2DComponent);
|
||||
GizmoRegistry.unregister(CircleCollider2DComponent);
|
||||
GizmoRegistry.unregister(CapsuleCollider2DComponent);
|
||||
GizmoRegistry.unregister(PolygonCollider2DComponent);
|
||||
GizmoRegistry.unregister(Rigidbody2DComponent);
|
||||
}
|
||||
227
packages/physics-rapier2d/src/editor/index.ts
Normal file
227
packages/physics-rapier2d/src/editor/index.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Physics 2D Editor Module Entry
|
||||
* 2D 物理编辑器模块入口
|
||||
*/
|
||||
|
||||
import type { ServiceContainer, Entity } from '@esengine/ecs-framework';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IEditorModuleLoader,
|
||||
EntityCreationTemplate,
|
||||
ComponentAction
|
||||
} from '@esengine/editor-core';
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
ComponentRegistry
|
||||
} from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/ecs-components';
|
||||
|
||||
// Local imports
|
||||
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 { registerPhysics2DGizmos } from './gizmos/Physics2DGizmo';
|
||||
|
||||
/**
|
||||
* Physics 2D Editor Module
|
||||
* 2D 物理编辑器模块
|
||||
*/
|
||||
export class Physics2DEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
// 注册组件到编辑器组件注册表
|
||||
// 组件检视器现在通过 @Property 装饰器自动生成
|
||||
const componentRegistry = services.resolve(ComponentRegistry);
|
||||
if (componentRegistry) {
|
||||
componentRegistry.register({
|
||||
name: 'Rigidbody2D',
|
||||
type: Rigidbody2DComponent,
|
||||
category: 'components.category.physics',
|
||||
description: '2D rigidbody for physics simulation',
|
||||
icon: 'Box'
|
||||
});
|
||||
|
||||
componentRegistry.register({
|
||||
name: 'BoxCollider2D',
|
||||
type: BoxCollider2DComponent,
|
||||
category: 'components.category.physics',
|
||||
description: '2D box collider shape',
|
||||
icon: 'Square'
|
||||
});
|
||||
|
||||
componentRegistry.register({
|
||||
name: 'CircleCollider2D',
|
||||
type: CircleCollider2DComponent,
|
||||
category: 'components.category.physics',
|
||||
description: '2D circle collider shape',
|
||||
icon: 'Circle'
|
||||
});
|
||||
|
||||
componentRegistry.register({
|
||||
name: 'CapsuleCollider2D',
|
||||
type: CapsuleCollider2DComponent,
|
||||
category: 'components.category.physics',
|
||||
description: '2D capsule collider shape',
|
||||
icon: 'Pill'
|
||||
});
|
||||
|
||||
componentRegistry.register({
|
||||
name: 'PolygonCollider2D',
|
||||
type: PolygonCollider2DComponent,
|
||||
category: 'components.category.physics',
|
||||
description: '2D polygon collider shape',
|
||||
icon: 'Pentagon'
|
||||
});
|
||||
}
|
||||
|
||||
// 注册 Physics Gizmos
|
||||
registerPhysics2DGizmos();
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
// 清理资源
|
||||
}
|
||||
|
||||
getInspectorProviders() {
|
||||
// 使用 @Property 装饰器自动生成检视器,不再需要自定义
|
||||
return [];
|
||||
}
|
||||
|
||||
getEntityCreationTemplates(): EntityCreationTemplate[] {
|
||||
const createPhysicsEntity = (
|
||||
name: string,
|
||||
colliderType: 'box' | 'circle' | 'capsule',
|
||||
isStatic: boolean = false
|
||||
): number => {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('Scene not available');
|
||||
}
|
||||
|
||||
const entityStore = Core.services.resolve(EntityStoreService);
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
|
||||
if (!entityStore || !messageHub) {
|
||||
throw new Error('EntityStoreService or MessageHub not available');
|
||||
}
|
||||
|
||||
const count = entityStore.getAllEntities()
|
||||
.filter((e: Entity) => e.name.startsWith(name)).length;
|
||||
const entityName = `${name} ${count + 1}`;
|
||||
|
||||
const entity = scene.createEntity(entityName);
|
||||
entity.addComponent(new TransformComponent());
|
||||
|
||||
const rb = new Rigidbody2DComponent();
|
||||
if (isStatic) {
|
||||
rb.bodyType = 2; // Static
|
||||
}
|
||||
entity.addComponent(rb);
|
||||
|
||||
switch (colliderType) {
|
||||
case 'box':
|
||||
entity.addComponent(new BoxCollider2DComponent());
|
||||
break;
|
||||
case 'circle':
|
||||
entity.addComponent(new CircleCollider2DComponent());
|
||||
break;
|
||||
case 'capsule':
|
||||
entity.addComponent(new CapsuleCollider2DComponent());
|
||||
break;
|
||||
}
|
||||
|
||||
entityStore.addEntity(entity);
|
||||
messageHub.publish('entity:added', { entity });
|
||||
messageHub.publish('scene:modified', {});
|
||||
entityStore.selectEntity(entity);
|
||||
|
||||
return entity.id;
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'create-physics-box',
|
||||
label: '物理方块',
|
||||
icon: 'Square',
|
||||
category: 'physics',
|
||||
order: 100,
|
||||
create: () => createPhysicsEntity('PhysicsBox', 'box')
|
||||
},
|
||||
{
|
||||
id: 'create-physics-circle',
|
||||
label: '物理圆球',
|
||||
icon: 'Circle',
|
||||
category: 'physics',
|
||||
order: 101,
|
||||
create: () => createPhysicsEntity('PhysicsBall', 'circle')
|
||||
},
|
||||
{
|
||||
id: 'create-physics-capsule',
|
||||
label: '物理胶囊',
|
||||
icon: 'Pill',
|
||||
category: 'physics',
|
||||
order: 102,
|
||||
create: () => createPhysicsEntity('PhysicsCapsule', 'capsule')
|
||||
},
|
||||
{
|
||||
id: 'create-static-platform',
|
||||
label: '静态平台',
|
||||
icon: 'Minus',
|
||||
category: 'physics',
|
||||
order: 110,
|
||||
create: () => createPhysicsEntity('Platform', 'box', true)
|
||||
},
|
||||
{
|
||||
id: 'create-static-ground',
|
||||
label: '地面',
|
||||
icon: 'AlignVerticalJustifyEnd',
|
||||
category: 'physics',
|
||||
order: 111,
|
||||
create: (): number => {
|
||||
const scene = Core.scene;
|
||||
if (!scene) {
|
||||
throw new Error('Scene not available');
|
||||
}
|
||||
|
||||
const entityStore = Core.services.resolve(EntityStoreService);
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
|
||||
if (!entityStore || !messageHub) {
|
||||
throw new Error('EntityStoreService or MessageHub not available');
|
||||
}
|
||||
|
||||
const entity = scene.createEntity('Ground');
|
||||
entity.addComponent(new TransformComponent());
|
||||
|
||||
const rb = new Rigidbody2DComponent();
|
||||
rb.bodyType = 2; // Static
|
||||
entity.addComponent(rb);
|
||||
|
||||
const collider = new BoxCollider2DComponent();
|
||||
collider.width = 200;
|
||||
collider.height = 10;
|
||||
entity.addComponent(collider);
|
||||
|
||||
entityStore.addEntity(entity);
|
||||
messageHub.publish('entity:added', { entity });
|
||||
messageHub.publish('scene:modified', {});
|
||||
entityStore.selectEntity(entity);
|
||||
|
||||
return entity.id;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getComponentActions(): ComponentAction[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const physics2DEditorModule = new Physics2DEditorModule();
|
||||
|
||||
// Plugin exports
|
||||
export { Physics2DPlugin } from './Physics2DPlugin';
|
||||
export default physics2DEditorModule;
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* BoxCollider2D Inspector Provider
|
||||
* 2D 矩形碰撞体检视器
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core';
|
||||
import { BoxCollider2DComponent } from '../../components/BoxCollider2DComponent';
|
||||
import { CollisionLayer2D } from '../../types/Physics2DTypes';
|
||||
|
||||
export class BoxCollider2DInspectorProvider implements IComponentInspector<BoxCollider2DComponent> {
|
||||
readonly id = 'boxcollider2d-inspector';
|
||||
readonly name = 'BoxCollider2D Inspector';
|
||||
readonly priority = 100;
|
||||
readonly targetComponents = ['BoxCollider2D', 'BoxCollider2DComponent'];
|
||||
|
||||
canHandle(component: Component): component is BoxCollider2DComponent {
|
||||
return component instanceof BoxCollider2DComponent ||
|
||||
component.constructor.name === 'BoxCollider2DComponent';
|
||||
}
|
||||
|
||||
render(context: ComponentInspectorContext): React.ReactElement {
|
||||
const component = context.component as BoxCollider2DComponent;
|
||||
const onChange = context.onChange;
|
||||
|
||||
const handleChange = (prop: string, value: unknown) => {
|
||||
onChange?.(prop, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Box Collider 2D</div>
|
||||
|
||||
{/* Size */}
|
||||
<div className="section-subtitle">Size</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Width</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.width}
|
||||
min={0.001}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('width', parseFloat(e.target.value) || 1)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Height</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.height}
|
||||
min={0.001}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('height', parseFloat(e.target.value) || 1)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Offset */}
|
||||
<div className="section-subtitle">Offset</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>X</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.offset.x}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('offset', {
|
||||
...component.offset,
|
||||
x: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Y</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.offset.y}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('offset', {
|
||||
...component.offset,
|
||||
y: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Material */}
|
||||
<div className="section-subtitle">Material</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Friction</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.friction}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(e) => handleChange('friction', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Restitution</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.restitution}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(e) => handleChange('restitution', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Density</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.density}
|
||||
min={0.001}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('density', parseFloat(e.target.value) || 1)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Collision */}
|
||||
<div className="section-subtitle">Collision</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Is Trigger</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.isTrigger}
|
||||
onChange={(e) => handleChange('isTrigger', e.target.checked)}
|
||||
className="property-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Layer</label>
|
||||
<select
|
||||
value={component.collisionLayer}
|
||||
onChange={(e) => handleChange('collisionLayer', parseInt(e.target.value, 10))}
|
||||
className="property-select"
|
||||
>
|
||||
<option value={CollisionLayer2D.Default}>Default</option>
|
||||
<option value={CollisionLayer2D.Player}>Player</option>
|
||||
<option value={CollisionLayer2D.Enemy}>Enemy</option>
|
||||
<option value={CollisionLayer2D.Ground}>Ground</option>
|
||||
<option value={CollisionLayer2D.Projectile}>Projectile</option>
|
||||
<option value={CollisionLayer2D.Trigger}>Trigger</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* CircleCollider2D Inspector Provider
|
||||
* 2D 圆形碰撞体检视器
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core';
|
||||
import { CircleCollider2DComponent } from '../../components/CircleCollider2DComponent';
|
||||
import { CollisionLayer2D } from '../../types/Physics2DTypes';
|
||||
|
||||
export class CircleCollider2DInspectorProvider implements IComponentInspector<CircleCollider2DComponent> {
|
||||
readonly id = 'circlecollider2d-inspector';
|
||||
readonly name = 'CircleCollider2D Inspector';
|
||||
readonly priority = 100;
|
||||
readonly targetComponents = ['CircleCollider2D', 'CircleCollider2DComponent'];
|
||||
|
||||
canHandle(component: Component): component is CircleCollider2DComponent {
|
||||
return component instanceof CircleCollider2DComponent ||
|
||||
component.constructor.name === 'CircleCollider2DComponent';
|
||||
}
|
||||
|
||||
render(context: ComponentInspectorContext): React.ReactElement {
|
||||
const component = context.component as CircleCollider2DComponent;
|
||||
const onChange = context.onChange;
|
||||
|
||||
const handleChange = (prop: string, value: unknown) => {
|
||||
onChange?.(prop, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Circle Collider 2D</div>
|
||||
|
||||
{/* Radius */}
|
||||
<div className="property-row">
|
||||
<label>Radius</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.radius}
|
||||
min={0.001}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('radius', parseFloat(e.target.value) || 0.5)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Offset */}
|
||||
<div className="section-subtitle">Offset</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>X</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.offset.x}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('offset', {
|
||||
...component.offset,
|
||||
x: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Y</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.offset.y}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('offset', {
|
||||
...component.offset,
|
||||
y: parseFloat(e.target.value) || 0
|
||||
})}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Material */}
|
||||
<div className="section-subtitle">Material</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Friction</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.friction}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(e) => handleChange('friction', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Restitution</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.restitution}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(e) => handleChange('restitution', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Density</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.density}
|
||||
min={0.001}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('density', parseFloat(e.target.value) || 1)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Collision */}
|
||||
<div className="section-subtitle">Collision</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Is Trigger</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.isTrigger}
|
||||
onChange={(e) => handleChange('isTrigger', e.target.checked)}
|
||||
className="property-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Layer</label>
|
||||
<select
|
||||
value={component.collisionLayer}
|
||||
onChange={(e) => handleChange('collisionLayer', parseInt(e.target.value, 10))}
|
||||
className="property-select"
|
||||
>
|
||||
<option value={CollisionLayer2D.Default}>Default</option>
|
||||
<option value={CollisionLayer2D.Player}>Player</option>
|
||||
<option value={CollisionLayer2D.Enemy}>Enemy</option>
|
||||
<option value={CollisionLayer2D.Ground}>Ground</option>
|
||||
<option value={CollisionLayer2D.Projectile}>Projectile</option>
|
||||
<option value={CollisionLayer2D.Trigger}>Trigger</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Rigidbody2D Inspector Provider
|
||||
* 2D 刚体检视器
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core';
|
||||
import { Rigidbody2DComponent } from '../../components/Rigidbody2DComponent';
|
||||
import { RigidbodyType2D, CollisionDetectionMode2D } from '../../types/Physics2DTypes';
|
||||
|
||||
export class Rigidbody2DInspectorProvider implements IComponentInspector<Rigidbody2DComponent> {
|
||||
readonly id = 'rigidbody2d-inspector';
|
||||
readonly name = 'Rigidbody2D Inspector';
|
||||
readonly priority = 100;
|
||||
readonly targetComponents = ['Rigidbody2D', 'Rigidbody2DComponent'];
|
||||
|
||||
canHandle(component: Component): component is Rigidbody2DComponent {
|
||||
return component instanceof Rigidbody2DComponent ||
|
||||
component.constructor.name === 'Rigidbody2DComponent';
|
||||
}
|
||||
|
||||
render(context: ComponentInspectorContext): React.ReactElement {
|
||||
const component = context.component as Rigidbody2DComponent;
|
||||
const onChange = context.onChange;
|
||||
|
||||
const handleChange = (prop: string, value: unknown) => {
|
||||
onChange?.(prop, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Rigidbody 2D</div>
|
||||
|
||||
{/* Body Type */}
|
||||
<div className="property-row">
|
||||
<label>Body Type</label>
|
||||
<select
|
||||
value={component.bodyType}
|
||||
onChange={(e) => handleChange('bodyType', parseInt(e.target.value, 10) as RigidbodyType2D)}
|
||||
className="property-select"
|
||||
>
|
||||
<option value={RigidbodyType2D.Dynamic}>Dynamic</option>
|
||||
<option value={RigidbodyType2D.Kinematic}>Kinematic</option>
|
||||
<option value={RigidbodyType2D.Static}>Static</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Mass - only for Dynamic */}
|
||||
{component.bodyType === RigidbodyType2D.Dynamic && (
|
||||
<div className="property-row">
|
||||
<label>Mass</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.mass}
|
||||
min={0.001}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('mass', parseFloat(e.target.value) || 1)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gravity Scale */}
|
||||
<div className="property-row">
|
||||
<label>Gravity Scale</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.gravityScale}
|
||||
step={0.1}
|
||||
onChange={(e) => handleChange('gravityScale', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Damping Section */}
|
||||
<div className="section-subtitle">Damping</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Linear</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.linearDamping}
|
||||
min={0}
|
||||
step={0.01}
|
||||
onChange={(e) => handleChange('linearDamping', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Angular</label>
|
||||
<input
|
||||
type="number"
|
||||
value={component.angularDamping}
|
||||
min={0}
|
||||
step={0.01}
|
||||
onChange={(e) => handleChange('angularDamping', parseFloat(e.target.value) || 0)}
|
||||
className="property-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Constraints Section */}
|
||||
<div className="section-subtitle">Constraints</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Freeze Position X</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.constraints.freezePositionX}
|
||||
onChange={(e) => handleChange('constraints', {
|
||||
...component.constraints,
|
||||
freezePositionX: e.target.checked
|
||||
})}
|
||||
className="property-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Freeze Position Y</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.constraints.freezePositionY}
|
||||
onChange={(e) => handleChange('constraints', {
|
||||
...component.constraints,
|
||||
freezePositionY: e.target.checked
|
||||
})}
|
||||
className="property-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Freeze Rotation</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.constraints.freezeRotation}
|
||||
onChange={(e) => handleChange('constraints', {
|
||||
...component.constraints,
|
||||
freezeRotation: e.target.checked
|
||||
})}
|
||||
className="property-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Collision Detection */}
|
||||
<div className="section-subtitle">Collision</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Detection</label>
|
||||
<select
|
||||
value={component.collisionDetection}
|
||||
onChange={(e) => handleChange('collisionDetection', parseInt(e.target.value, 10) as CollisionDetectionMode2D)}
|
||||
className="property-select"
|
||||
>
|
||||
<option value={CollisionDetectionMode2D.Discrete}>Discrete</option>
|
||||
<option value={CollisionDetectionMode2D.Continuous}>Continuous</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Sleep */}
|
||||
<div className="section-subtitle">Sleep</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Can Sleep</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={component.canSleep}
|
||||
onChange={(e) => handleChange('canSleep', e.target.checked)}
|
||||
className="property-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Runtime Info (read-only) */}
|
||||
<div className="section-subtitle">Runtime Info</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Velocity</label>
|
||||
<span className="property-readonly">
|
||||
({component.velocity.x.toFixed(2)}, {component.velocity.y.toFixed(2)})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Angular Vel</label>
|
||||
<span className="property-readonly">
|
||||
{component.angularVelocity.toFixed(2)} rad/s
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Is Awake</label>
|
||||
<span className="property-readonly">
|
||||
{component.isAwake ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
26
packages/physics-rapier2d/src/index.ts
Normal file
26
packages/physics-rapier2d/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
// Plugin (for editor, no WASM dependency)
|
||||
export { Physics2DPlugin } from './editor/Physics2DPlugin';
|
||||
57
packages/physics-rapier2d/src/runtime.ts
Normal file
57
packages/physics-rapier2d/src/runtime.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @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 Vector2,
|
||||
type Physics2DConfig,
|
||||
DEFAULT_PHYSICS_CONFIG,
|
||||
CollisionLayer2D,
|
||||
ForceMode2D,
|
||||
type RaycastHit2D,
|
||||
type ShapeCastHit2D,
|
||||
type OverlapResult2D,
|
||||
PhysicsMaterial2DPreset,
|
||||
getPhysicsMaterialPreset,
|
||||
JointType2D
|
||||
} from './types/Physics2DTypes';
|
||||
|
||||
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
|
||||
export { PhysicsRuntimeModule } from './PhysicsRuntimeModule';
|
||||
export { default as physicsRuntimeModule } from './PhysicsRuntimeModule';
|
||||
17
packages/physics-rapier2d/src/runtime/index.ts
Normal file
17
packages/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 with WASM
|
||||
export { PhysicsRuntimeModule, default as physicsRuntimeModule } 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';
|
||||
213
packages/physics-rapier2d/src/services/Physics2DService.ts
Normal file
213
packages/physics-rapier2d/src/services/Physics2DService.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Physics2DService
|
||||
* 2D 物理服务
|
||||
*
|
||||
* 提供全局物理配置和实用方法
|
||||
*/
|
||||
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import type { Vector2, 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: Vector2): void {
|
||||
this._config.gravity = { ...gravity };
|
||||
this._physicsSystem?.setGravity(gravity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重力
|
||||
*/
|
||||
public getGravity(): Vector2 {
|
||||
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: Vector2,
|
||||
direction: Vector2,
|
||||
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: Vector2,
|
||||
direction: Vector2,
|
||||
maxDistance: number,
|
||||
collisionMask: number = CollisionLayer2D.All
|
||||
): RaycastHit2D[] {
|
||||
return this._physicsSystem?.raycastAll(origin, direction, maxDistance, collisionMask) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 点重叠检测
|
||||
* @param point 检测点
|
||||
* @param collisionMask 碰撞掩码
|
||||
*/
|
||||
public overlapPoint(point: Vector2, collisionMask: number = CollisionLayer2D.All): OverlapResult2D {
|
||||
return this._physicsSystem?.overlapPoint(point, collisionMask) ?? { entityIds: [], colliderHandles: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆形重叠检测
|
||||
* @param center 圆心
|
||||
* @param radius 半径
|
||||
* @param collisionMask 碰撞掩码
|
||||
*/
|
||||
public overlapCircle(
|
||||
center: Vector2,
|
||||
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: Vector2,
|
||||
halfExtents: Vector2,
|
||||
rotation: number = 0,
|
||||
collisionMask: number = CollisionLayer2D.All
|
||||
): OverlapResult2D {
|
||||
return (
|
||||
this._physicsSystem?.overlapBox(center, halfExtents, rotation, collisionMask) ?? {
|
||||
entityIds: [],
|
||||
colliderHandles: []
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 归一化向量
|
||||
*/
|
||||
public normalize(v: Vector2): Vector2 {
|
||||
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: Vector2, b: Vector2): number {
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算向量长度
|
||||
*/
|
||||
public magnitude(v: Vector2): number {
|
||||
return Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量点积
|
||||
*/
|
||||
public dot(a: Vector2, b: Vector2): number {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量叉积(返回标量,2D 特有)
|
||||
*/
|
||||
public cross(a: Vector2, b: Vector2): number {
|
||||
return a.x * b.y - a.y * b.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._physicsSystem = null;
|
||||
}
|
||||
}
|
||||
5
packages/physics-rapier2d/src/services/index.ts
Normal file
5
packages/physics-rapier2d/src/services/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D Services exports
|
||||
*/
|
||||
|
||||
export { Physics2DService } from './Physics2DService';
|
||||
562
packages/physics-rapier2d/src/systems/Physics2DSystem.ts
Normal file
562
packages/physics-rapier2d/src/systems/Physics2DSystem.ts
Normal file
@@ -0,0 +1,562 @@
|
||||
/**
|
||||
* Physics2DSystem
|
||||
* 2D 物理系统
|
||||
*
|
||||
* 负责更新物理世界并同步 Transform
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework';
|
||||
import { TransformComponent } from '@esengine/ecs-components';
|
||||
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 { Physics2DConfig, Vector2 } 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('@dimforge/rapier2d-compat') | null = null;
|
||||
private _rapierInitialized: boolean = false;
|
||||
private _config: Physics2DSystemConfig;
|
||||
|
||||
// 实体到物理对象的映射
|
||||
private _entityBodies: Map<number, { bodyHandle: number; colliderHandles: number[] }> = new Map();
|
||||
|
||||
// 待处理的新实体队列
|
||||
private _pendingEntities: Entity[] = [];
|
||||
|
||||
// Transform 组件类型(用于检查)
|
||||
private _transformType = TransformComponent;
|
||||
|
||||
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('@dimforge/rapier2d-compat')): 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: Vector2): void {
|
||||
this._world.setGravity(gravity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重力
|
||||
*/
|
||||
public getGravity(): Vector2 {
|
||||
return this._world.getGravity();
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测
|
||||
*/
|
||||
public raycast(origin: Vector2, direction: Vector2, maxDistance: number, collisionMask?: number) {
|
||||
return this._world.raycast(origin, direction, maxDistance, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测所有
|
||||
*/
|
||||
public raycastAll(origin: Vector2, direction: Vector2, maxDistance: number, collisionMask?: number) {
|
||||
return this._world.raycastAll(origin, direction, maxDistance, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点重叠检测
|
||||
*/
|
||||
public overlapPoint(point: Vector2, collisionMask?: number) {
|
||||
return this._world.overlapPoint(point, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆形重叠检测
|
||||
*/
|
||||
public overlapCircle(center: Vector2, radius: number, collisionMask?: number) {
|
||||
return this._world.overlapCircle(center, radius, collisionMask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 矩形重叠检测
|
||||
*/
|
||||
public overlapBox(center: Vector2, halfExtents: Vector2, 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(this._transformType);
|
||||
|
||||
if (!rigidbody || !transform) {
|
||||
this.logger.warn(`Entity ${entity.name} missing required components for physics`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取位置和旋转
|
||||
const position: Vector2 = {
|
||||
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);
|
||||
|
||||
for (const collider of colliders) {
|
||||
const colliderHandle = this._world.createCollider(entity.id, collider, bodyHandle);
|
||||
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(this._transformType);
|
||||
const mapping = this._entityBodies.get(entity.id);
|
||||
|
||||
if (!rigidbody || !transform || !mapping) continue;
|
||||
|
||||
// 只有当需要同步时才更新物理世界
|
||||
if (rigidbody._needsSync) {
|
||||
const position: Vector2 = {
|
||||
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);
|
||||
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);
|
||||
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(this._transformType);
|
||||
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-rapier2d/src/systems/index.ts
Normal file
5
packages/physics-rapier2d/src/systems/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D Systems exports
|
||||
*/
|
||||
|
||||
export { Physics2DSystem, type Physics2DSystemConfig } from './Physics2DSystem';
|
||||
104
packages/physics-rapier2d/src/types/Physics2DEvents.ts
Normal file
104
packages/physics-rapier2d/src/types/Physics2DEvents.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Physics 2D Events
|
||||
* 2D 物理事件定义
|
||||
*/
|
||||
|
||||
import type { Vector2 } from './Physics2DTypes';
|
||||
|
||||
/**
|
||||
* 碰撞事件类型
|
||||
*/
|
||||
export type CollisionEventType = 'enter' | 'stay' | 'exit';
|
||||
|
||||
/**
|
||||
* 触发器事件类型
|
||||
*/
|
||||
export type TriggerEventType = 'enter' | 'stay' | 'exit';
|
||||
|
||||
/**
|
||||
* 碰撞接触点信息
|
||||
*/
|
||||
export interface ContactPoint2D {
|
||||
/** 接触点位置 */
|
||||
point: Vector2;
|
||||
/** 接触点法线 */
|
||||
normal: Vector2;
|
||||
/** 穿透深度 */
|
||||
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: Vector2;
|
||||
/** 总冲量大小 */
|
||||
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 };
|
||||
}
|
||||
183
packages/physics-rapier2d/src/types/Physics2DTypes.ts
Normal file
183
packages/physics-rapier2d/src/types/Physics2DTypes.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Physics 2D Types
|
||||
* 2D 物理引擎类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 刚体类型
|
||||
*/
|
||||
export enum RigidbodyType2D {
|
||||
/** 动态刚体,受力和碰撞影响 */
|
||||
Dynamic = 0,
|
||||
/** 运动学刚体,手动控制位置,不受力影响 */
|
||||
Kinematic = 1,
|
||||
/** 静态刚体,不移动,用于地形等 */
|
||||
Static = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 碰撞检测模式
|
||||
*/
|
||||
export enum CollisionDetectionMode2D {
|
||||
/** 离散检测,性能好但可能穿透 */
|
||||
Discrete = 0,
|
||||
/** 连续检测,防止高速物体穿透 */
|
||||
Continuous = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D 向量
|
||||
*/
|
||||
export interface Vector2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理配置
|
||||
*/
|
||||
export interface Physics2DConfig {
|
||||
/** 重力向量 */
|
||||
gravity: Vector2;
|
||||
/** 固定时间步长(秒) */
|
||||
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: Vector2;
|
||||
/** 命中面的法线 */
|
||||
normal: Vector2;
|
||||
/** 射线起点到命中点的距离 */
|
||||
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
|
||||
}
|
||||
29
packages/physics-rapier2d/src/types/index.ts
Normal file
29
packages/physics-rapier2d/src/types/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Physics 2D Types exports
|
||||
*/
|
||||
|
||||
export {
|
||||
RigidbodyType2D,
|
||||
CollisionDetectionMode2D,
|
||||
type Vector2,
|
||||
type Physics2DConfig,
|
||||
DEFAULT_PHYSICS_CONFIG,
|
||||
CollisionLayer2D,
|
||||
ForceMode2D,
|
||||
type RaycastHit2D,
|
||||
type ShapeCastHit2D,
|
||||
type OverlapResult2D,
|
||||
PhysicsMaterial2DPreset,
|
||||
getPhysicsMaterialPreset,
|
||||
JointType2D
|
||||
} from './Physics2DTypes';
|
||||
|
||||
export {
|
||||
type CollisionEventType,
|
||||
type TriggerEventType,
|
||||
type ContactPoint2D,
|
||||
type CollisionEvent2D,
|
||||
type TriggerEvent2D,
|
||||
PHYSICS_EVENTS,
|
||||
type Physics2DEventMap
|
||||
} from './Physics2DEvents';
|
||||
1073
packages/physics-rapier2d/src/world/Physics2DWorld.ts
Normal file
1073
packages/physics-rapier2d/src/world/Physics2DWorld.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
packages/physics-rapier2d/src/world/index.ts
Normal file
5
packages/physics-rapier2d/src/world/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Physics 2D World exports
|
||||
*/
|
||||
|
||||
export { Physics2DWorld, type Physics2DWorldState } from './Physics2DWorld';
|
||||
30
packages/physics-rapier2d/tsconfig.json
Normal file
30
packages/physics-rapier2d/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*", "plugin.json"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../components" }
|
||||
]
|
||||
}
|
||||
47
packages/physics-rapier2d/vite.config.ts
Normal file
47
packages/physics-rapier2d/vite.config.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: false,
|
||||
tsconfigPath: './tsconfig.json'
|
||||
})
|
||||
],
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: {
|
||||
index: resolve(__dirname, 'src/index.ts'),
|
||||
runtime: resolve(__dirname, 'src/runtime.ts'),
|
||||
'editor/index': resolve(__dirname, 'src/editor/index.ts')
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (format, entryName) => `${entryName}.js`
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/ecs-components',
|
||||
'@esengine/editor-core',
|
||||
'react',
|
||||
'react/jsx-runtime',
|
||||
/^@esengine\//
|
||||
],
|
||||
output: {
|
||||
exports: 'named',
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user