feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 (#244)
* feat(physics): 集成 Rapier2D 物理引擎并修复预览重置问题 * fix: 修复 CI 流程并清理代码
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user