feat(particle): 添加粒子与 Rapier2D 物理碰撞集成 (#295)
* feat(particle): 添加粒子与 Rapier2D 物理碰撞集成 - 新增 Physics2DCollisionModule 模块,支持粒子与场景碰撞体交互 - 支持圆形重叠检测和射线检测两种模式 - 支持 Kill/Bounce/Stop 三种碰撞行为 - 修复 module.update() 参数顺序 bug - 物理模块自动通过 context 注入,用户只需添加模块即可 * chore: update pnpm-lock.yaml for particle physics dependency Update lockfile to include @esengine/physics-rapier2d as optional peer dependency.
This commit is contained in:
@@ -30,10 +30,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/asset-system": "workspace:*"
|
"@esengine/asset-system": "workspace:*"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@esengine/physics-rapier2d": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@esengine/physics-rapier2d": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/ecs-framework-math": "workspace:*",
|
"@esengine/ecs-framework-math": "workspace:*",
|
||||||
"@esengine/engine-core": "workspace:*",
|
"@esengine/engine-core": "workspace:*",
|
||||||
|
"@esengine/physics-rapier2d": "workspace:*",
|
||||||
"@esengine/build-config": "workspace:*",
|
"@esengine/build-config": "workspace:*",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { assetManager as globalAssetManager, type AssetManager } from '@esengine
|
|||||||
import { ParticleSystemComponent } from './ParticleSystemComponent';
|
import { ParticleSystemComponent } from './ParticleSystemComponent';
|
||||||
import { ParticleUpdateSystem } from './systems/ParticleSystem';
|
import { ParticleUpdateSystem } from './systems/ParticleSystem';
|
||||||
import { ParticleLoader, ParticleAssetType } from './loaders/ParticleLoader';
|
import { ParticleLoader, ParticleAssetType } from './loaders/ParticleLoader';
|
||||||
|
import type { IPhysics2DQuery } from './modules/Physics2DCollisionModule';
|
||||||
|
|
||||||
export type { SystemContext, ModuleManifest, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader };
|
export type { SystemContext, ModuleManifest, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader };
|
||||||
|
|
||||||
@@ -42,6 +43,17 @@ export interface ParticleSystemContext extends SystemContext {
|
|||||||
engineBridge?: IEngineBridge;
|
engineBridge?: IEngineBridge;
|
||||||
/** 资产管理器(用于注册加载器)| Asset manager (for registering loaders) */
|
/** 资产管理器(用于注册加载器)| Asset manager (for registering loaders) */
|
||||||
assetManager?: AssetManager;
|
assetManager?: AssetManager;
|
||||||
|
/**
|
||||||
|
* 2D 物理查询接口(可选)
|
||||||
|
* 2D Physics query interface (optional)
|
||||||
|
*
|
||||||
|
* 如果提供,将自动注入到使用 Physics2DCollisionModule 的粒子系统中。
|
||||||
|
* 通常传入 Physics2DService 实例。
|
||||||
|
*
|
||||||
|
* If provided, will be auto-injected into particle systems using Physics2DCollisionModule.
|
||||||
|
* Typically pass a Physics2DService instance.
|
||||||
|
*/
|
||||||
|
physics2DQuery?: IPhysics2DQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParticleRuntimeModule implements IRuntimeModule {
|
class ParticleRuntimeModule implements IRuntimeModule {
|
||||||
@@ -90,6 +102,11 @@ class ParticleRuntimeModule implements IRuntimeModule {
|
|||||||
this._updateSystem.setEngineBridge(particleContext.engineBridge);
|
this._updateSystem.setEngineBridge(particleContext.engineBridge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置 2D 物理查询(用于粒子与场景碰撞)| Set 2D physics query (for particle-scene collision)
|
||||||
|
if (particleContext.physics2DQuery) {
|
||||||
|
this._updateSystem.setPhysics2DQuery(particleContext.physics2DQuery);
|
||||||
|
}
|
||||||
|
|
||||||
scene.addSystem(this._updateSystem);
|
scene.addSystem(this._updateSystem);
|
||||||
particleContext.particleUpdateSystem = this._updateSystem;
|
particleContext.particleUpdateSystem = this._updateSystem;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ColorOverLifetimeModule } from './modules/ColorOverLifetimeModule';
|
|||||||
import { SizeOverLifetimeModule } from './modules/SizeOverLifetimeModule';
|
import { SizeOverLifetimeModule } from './modules/SizeOverLifetimeModule';
|
||||||
import { CollisionModule } from './modules/CollisionModule';
|
import { CollisionModule } from './modules/CollisionModule';
|
||||||
import { ForceFieldModule } from './modules/ForceFieldModule';
|
import { ForceFieldModule } from './modules/ForceFieldModule';
|
||||||
|
import { Physics2DCollisionModule } from './modules/Physics2DCollisionModule';
|
||||||
import type { IParticleAsset, IBurstConfig } from './loaders/ParticleLoader';
|
import type { IParticleAsset, IBurstConfig } from './loaders/ParticleLoader';
|
||||||
|
|
||||||
// Re-export for backward compatibility
|
// Re-export for backward compatibility
|
||||||
@@ -751,7 +752,33 @@ export class ParticleSystemComponent extends Component {
|
|||||||
const normalizedAge = p.age / p.lifetime;
|
const normalizedAge = p.age / p.lifetime;
|
||||||
for (const module of this._modules) {
|
for (const module of this._modules) {
|
||||||
if (module.enabled) {
|
if (module.enabled) {
|
||||||
module.update(p, normalizedAge, dt);
|
module.update(p, dt, normalizedAge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理碰撞模块标记的需销毁粒子 | Process particles marked for death by collision modules
|
||||||
|
for (const module of this._modules) {
|
||||||
|
if (module.enabled) {
|
||||||
|
// 处理边界碰撞模块 | Handle boundary collision module
|
||||||
|
if (module instanceof CollisionModule) {
|
||||||
|
const toKill = module.getParticlesToKill();
|
||||||
|
for (const p of toKill) {
|
||||||
|
if (p.alive) {
|
||||||
|
particlesToRecycle.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.clearDeathFlags();
|
||||||
|
}
|
||||||
|
// 处理物理碰撞模块 | Handle physics collision module
|
||||||
|
if (module instanceof Physics2DCollisionModule) {
|
||||||
|
const toKill = module.getParticlesToKill();
|
||||||
|
for (const p of toKill) {
|
||||||
|
if (p.alive) {
|
||||||
|
particlesToRecycle.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.clearDeathFlags();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ export {
|
|||||||
ForceFieldModule,
|
ForceFieldModule,
|
||||||
ForceFieldType,
|
ForceFieldType,
|
||||||
createDefaultForceField,
|
createDefaultForceField,
|
||||||
type ForceField
|
type ForceField,
|
||||||
|
// Physics 2D collision module | 2D 物理碰撞模块
|
||||||
|
Physics2DCollisionModule,
|
||||||
|
Physics2DCollisionBehavior,
|
||||||
|
type IPhysics2DQuery,
|
||||||
|
type ParticleCollisionInfo
|
||||||
} from './modules';
|
} from './modules';
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
|
|||||||
380
packages/particle/src/modules/Physics2DCollisionModule.ts
Normal file
380
packages/particle/src/modules/Physics2DCollisionModule.ts
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import type { Particle } from '../Particle';
|
||||||
|
import type { IParticleModule } from './IParticleModule';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物理碰撞行为
|
||||||
|
* Physics collision behavior
|
||||||
|
*/
|
||||||
|
export enum Physics2DCollisionBehavior {
|
||||||
|
/** 销毁粒子 | Kill particle */
|
||||||
|
Kill = 'kill',
|
||||||
|
/** 反弹 | Bounce */
|
||||||
|
Bounce = 'bounce',
|
||||||
|
/** 停止运动 | Stop movement */
|
||||||
|
Stop = 'stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物理查询接口
|
||||||
|
* Physics query interface
|
||||||
|
*
|
||||||
|
* 抽象物理服务查询,避免直接依赖 physics-rapier2d 包
|
||||||
|
* Abstract physics service query to avoid direct dependency on physics-rapier2d
|
||||||
|
*/
|
||||||
|
export interface IPhysics2DQuery {
|
||||||
|
/**
|
||||||
|
* 圆形重叠检测
|
||||||
|
* Circle overlap detection
|
||||||
|
*
|
||||||
|
* @param center - 圆心位置 | Center position
|
||||||
|
* @param radius - 半径 | Radius
|
||||||
|
* @param collisionMask - 碰撞掩码 | Collision mask
|
||||||
|
* @returns 重叠结果 | Overlap result
|
||||||
|
*/
|
||||||
|
overlapCircle(
|
||||||
|
center: { x: number; y: number },
|
||||||
|
radius: number,
|
||||||
|
collisionMask?: number
|
||||||
|
): { entityIds: number[]; colliderHandles: number[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 射线检测
|
||||||
|
* Raycast detection
|
||||||
|
*
|
||||||
|
* @param origin - 起点 | Origin
|
||||||
|
* @param direction - 方向(归一化)| Direction (normalized)
|
||||||
|
* @param maxDistance - 最大距离 | Max distance
|
||||||
|
* @param collisionMask - 碰撞掩码 | Collision mask
|
||||||
|
* @returns 射线结果或 null | Raycast result or null
|
||||||
|
*/
|
||||||
|
raycast(
|
||||||
|
origin: { x: number; y: number },
|
||||||
|
direction: { x: number; y: number },
|
||||||
|
maxDistance: number,
|
||||||
|
collisionMask?: number
|
||||||
|
): {
|
||||||
|
entityId: number;
|
||||||
|
point: { x: number; y: number };
|
||||||
|
normal: { x: number; y: number };
|
||||||
|
distance: number;
|
||||||
|
colliderHandle: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 碰撞回调数据
|
||||||
|
* Collision callback data
|
||||||
|
*/
|
||||||
|
export interface ParticleCollisionInfo {
|
||||||
|
/** 粒子引用 | Particle reference */
|
||||||
|
particle: Particle;
|
||||||
|
/** 碰撞位置 | Collision point */
|
||||||
|
point: { x: number; y: number };
|
||||||
|
/** 碰撞法线 | Collision normal */
|
||||||
|
normal: { x: number; y: number };
|
||||||
|
/** 碰撞的实体 ID | Collided entity ID */
|
||||||
|
entityId: number;
|
||||||
|
/** 碰撞体句柄 | Collider handle */
|
||||||
|
colliderHandle: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2D 物理碰撞模块
|
||||||
|
* 2D Physics collision module
|
||||||
|
*
|
||||||
|
* 使用 Physics2DService 的查询 API 检测粒子与场景碰撞体的碰撞。
|
||||||
|
* Uses Physics2DService query API to detect particle collisions with scene colliders.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 获取物理服务
|
||||||
|
* const physicsService = scene.services.resolve(Physics2DService);
|
||||||
|
*
|
||||||
|
* // 创建模块
|
||||||
|
* const collisionModule = new Physics2DCollisionModule();
|
||||||
|
* collisionModule.setPhysicsQuery(physicsService);
|
||||||
|
* collisionModule.particleRadius = 4;
|
||||||
|
* collisionModule.behavior = Physics2DCollisionBehavior.Bounce;
|
||||||
|
*
|
||||||
|
* // 添加到粒子系统
|
||||||
|
* particleSystem.addModule(collisionModule);
|
||||||
|
*
|
||||||
|
* // 监听碰撞事件
|
||||||
|
* collisionModule.onCollision = (info) => {
|
||||||
|
* console.log('Particle hit entity:', info.entityId);
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Physics2DCollisionModule implements IParticleModule {
|
||||||
|
readonly name = 'Physics2DCollision';
|
||||||
|
enabled = true;
|
||||||
|
|
||||||
|
// ============= 物理查询 | Physics Query =============
|
||||||
|
|
||||||
|
/** 物理查询接口 | Physics query interface */
|
||||||
|
private _physicsQuery: IPhysics2DQuery | null = null;
|
||||||
|
|
||||||
|
// ============= 碰撞设置 | Collision Settings =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 粒子碰撞半径
|
||||||
|
* Particle collision radius
|
||||||
|
*
|
||||||
|
* 用于圆形重叠检测的半径
|
||||||
|
*/
|
||||||
|
particleRadius: number = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 碰撞行为
|
||||||
|
* Collision behavior
|
||||||
|
*/
|
||||||
|
behavior: Physics2DCollisionBehavior = Physics2DCollisionBehavior.Bounce;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 碰撞层掩码
|
||||||
|
* Collision layer mask
|
||||||
|
*
|
||||||
|
* 默认 0xFFFF 表示与所有层碰撞
|
||||||
|
* Default 0xFFFF means collide with all layers
|
||||||
|
*/
|
||||||
|
collisionMask: number = 0xFFFF;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反弹系数 (0-1)
|
||||||
|
* Bounce factor (0-1)
|
||||||
|
*
|
||||||
|
* 1 = 完全弹性,0 = 无弹性
|
||||||
|
* 1 = fully elastic, 0 = no bounce
|
||||||
|
*/
|
||||||
|
bounceFactor: number = 0.6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反弹时的生命损失 (0-1)
|
||||||
|
* Life loss on bounce (0-1)
|
||||||
|
*/
|
||||||
|
lifeLossOnBounce: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小速度阈值
|
||||||
|
* Minimum velocity threshold
|
||||||
|
*
|
||||||
|
* 低于此速度时销毁粒子(防止无限小弹跳)
|
||||||
|
* Kill particle when velocity falls below this (prevents infinite tiny bounces)
|
||||||
|
*/
|
||||||
|
minVelocityThreshold: number = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用射线检测代替重叠检测
|
||||||
|
* Use raycast instead of overlap detection
|
||||||
|
*
|
||||||
|
* 射线检测更精确,可防止快速粒子穿透,但性能开销更大
|
||||||
|
* Raycast is more accurate and prevents fast particle tunneling, but more expensive
|
||||||
|
*/
|
||||||
|
useRaycast: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测频率(每 N 帧检测一次)
|
||||||
|
* Detection frequency (detect every N frames)
|
||||||
|
*
|
||||||
|
* 增大此值可提高性能,但降低精度
|
||||||
|
* Increase to improve performance at cost of accuracy
|
||||||
|
*/
|
||||||
|
detectionInterval: number = 1;
|
||||||
|
|
||||||
|
// ============= 内部状态 | Internal State =============
|
||||||
|
|
||||||
|
/** 帧计数器 | Frame counter */
|
||||||
|
private _frameCounter: number = 0;
|
||||||
|
|
||||||
|
/** 需要销毁的粒子 | Particles to kill */
|
||||||
|
private _particlesToKill: Set<Particle> = new Set();
|
||||||
|
|
||||||
|
// ============= 回调 | Callbacks =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 碰撞回调
|
||||||
|
* Collision callback
|
||||||
|
*
|
||||||
|
* 每次粒子碰撞时调用
|
||||||
|
*/
|
||||||
|
onCollision: ((info: ParticleCollisionInfo) => void) | null = null;
|
||||||
|
|
||||||
|
// ============= 公开方法 | Public Methods =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置物理查询接口
|
||||||
|
* Set physics query interface
|
||||||
|
*
|
||||||
|
* @param query - 物理查询接口(通常是 Physics2DService)| Physics query (usually Physics2DService)
|
||||||
|
*/
|
||||||
|
setPhysicsQuery(query: IPhysics2DQuery | null): void {
|
||||||
|
this._physicsQuery = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取需要销毁的粒子
|
||||||
|
* Get particles to kill
|
||||||
|
*/
|
||||||
|
getParticlesToKill(): Set<Particle> {
|
||||||
|
return this._particlesToKill;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除死亡标记
|
||||||
|
* Clear death flags
|
||||||
|
*/
|
||||||
|
clearDeathFlags(): void {
|
||||||
|
this._particlesToKill.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置帧计数器
|
||||||
|
* Reset frame counter
|
||||||
|
*/
|
||||||
|
resetFrameCounter(): void {
|
||||||
|
this._frameCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= IParticleModule 实现 | IParticleModule Implementation =============
|
||||||
|
|
||||||
|
update(p: Particle, dt: number, _normalizedAge: number): void {
|
||||||
|
if (!this._physicsQuery) return;
|
||||||
|
|
||||||
|
// 检测频率控制 | Detection frequency control
|
||||||
|
this._frameCounter++;
|
||||||
|
if (this._frameCounter % this.detectionInterval !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.useRaycast) {
|
||||||
|
this._updateWithRaycast(p, dt);
|
||||||
|
} else {
|
||||||
|
this._updateWithOverlap(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============= 私有方法 | Private Methods =============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用圆形重叠检测
|
||||||
|
* Update using circle overlap detection
|
||||||
|
*/
|
||||||
|
private _updateWithOverlap(p: Particle): void {
|
||||||
|
if (!this._physicsQuery) return;
|
||||||
|
|
||||||
|
const result = this._physicsQuery.overlapCircle(
|
||||||
|
{ x: p.x, y: p.y },
|
||||||
|
this.particleRadius,
|
||||||
|
this.collisionMask
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.entityIds.length > 0) {
|
||||||
|
// 发生碰撞 | Collision occurred
|
||||||
|
const entityId = result.entityIds[0];
|
||||||
|
const colliderHandle = result.colliderHandles[0];
|
||||||
|
|
||||||
|
// 估算法线(从粒子速度反向)| Estimate normal (from particle velocity)
|
||||||
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
||||||
|
const normal = speed > 0.001
|
||||||
|
? { x: -p.vx / speed, y: -p.vy / speed }
|
||||||
|
: { x: 0, y: 1 };
|
||||||
|
|
||||||
|
this._handleCollision(p, { x: p.x, y: p.y }, normal, entityId, colliderHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用射线检测
|
||||||
|
* Update using raycast detection
|
||||||
|
*/
|
||||||
|
private _updateWithRaycast(p: Particle, dt: number): void {
|
||||||
|
if (!this._physicsQuery) return;
|
||||||
|
|
||||||
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
||||||
|
if (speed < 0.001) return;
|
||||||
|
|
||||||
|
// 归一化方向 | Normalize direction
|
||||||
|
const direction = { x: p.vx / speed, y: p.vy / speed };
|
||||||
|
const distance = speed * dt + this.particleRadius;
|
||||||
|
|
||||||
|
const hit = this._physicsQuery.raycast(
|
||||||
|
{ x: p.x, y: p.y },
|
||||||
|
direction,
|
||||||
|
distance,
|
||||||
|
this.collisionMask
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
this._handleCollision(p, hit.point, hit.normal, hit.entityId, hit.colliderHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理碰撞
|
||||||
|
* Handle collision
|
||||||
|
*/
|
||||||
|
private _handleCollision(
|
||||||
|
p: Particle,
|
||||||
|
point: { x: number; y: number },
|
||||||
|
normal: { x: number; y: number },
|
||||||
|
entityId: number,
|
||||||
|
colliderHandle: number
|
||||||
|
): void {
|
||||||
|
// 触发回调 | Trigger callback
|
||||||
|
if (this.onCollision) {
|
||||||
|
this.onCollision({
|
||||||
|
particle: p,
|
||||||
|
point,
|
||||||
|
normal,
|
||||||
|
entityId,
|
||||||
|
colliderHandle
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.behavior) {
|
||||||
|
case Physics2DCollisionBehavior.Kill:
|
||||||
|
this._particlesToKill.add(p);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Physics2DCollisionBehavior.Bounce:
|
||||||
|
this._applyBounce(p, normal);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Physics2DCollisionBehavior.Stop:
|
||||||
|
p.vx = 0;
|
||||||
|
p.vy = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用反弹
|
||||||
|
* Apply bounce effect
|
||||||
|
*/
|
||||||
|
private _applyBounce(p: Particle, normal: { x: number; y: number }): void {
|
||||||
|
// 反射公式: v' = v - 2 * (v · n) * n
|
||||||
|
// Reflection formula: v' = v - 2 * (v · n) * n
|
||||||
|
const dot = p.vx * normal.x + p.vy * normal.y;
|
||||||
|
|
||||||
|
// 只在粒子朝向表面时反弹 | Only bounce if particle is moving towards surface
|
||||||
|
if (dot < 0) {
|
||||||
|
p.vx = (p.vx - 2 * dot * normal.x) * this.bounceFactor;
|
||||||
|
p.vy = (p.vy - 2 * dot * normal.y) * this.bounceFactor;
|
||||||
|
|
||||||
|
// 稍微移开粒子防止重复碰撞 | Move particle slightly away to prevent repeated collision
|
||||||
|
p.x += normal.x * this.particleRadius * 0.1;
|
||||||
|
p.y += normal.y * this.particleRadius * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用生命损失 | Apply life loss
|
||||||
|
if (this.lifeLossOnBounce > 0) {
|
||||||
|
p.lifetime *= (1 - this.lifeLossOnBounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查最小速度 | Check minimum velocity
|
||||||
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
||||||
|
if (speed < this.minVelocityThreshold) {
|
||||||
|
this._particlesToKill.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,3 +20,9 @@ export {
|
|||||||
createDefaultForceField,
|
createDefaultForceField,
|
||||||
type ForceField
|
type ForceField
|
||||||
} from './ForceFieldModule';
|
} from './ForceFieldModule';
|
||||||
|
export {
|
||||||
|
Physics2DCollisionModule,
|
||||||
|
Physics2DCollisionBehavior,
|
||||||
|
type IPhysics2DQuery,
|
||||||
|
type ParticleCollisionInfo
|
||||||
|
} from './Physics2DCollisionModule';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EntitySystem, Matcher, ECSSystem, Time, Entity } from '@esengine/ecs-fr
|
|||||||
import { ParticleSystemComponent } from '../ParticleSystemComponent';
|
import { ParticleSystemComponent } from '../ParticleSystemComponent';
|
||||||
import { ParticleRenderDataProvider } from '../rendering/ParticleRenderDataProvider';
|
import { ParticleRenderDataProvider } from '../rendering/ParticleRenderDataProvider';
|
||||||
import type { IEngineIntegration, IEngineBridge } from '../ParticleRuntimeModule';
|
import type { IEngineIntegration, IEngineBridge } from '../ParticleRuntimeModule';
|
||||||
|
import { Physics2DCollisionModule, type IPhysics2DQuery } from '../modules/Physics2DCollisionModule';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认粒子纹理 ID
|
* 默认粒子纹理 ID
|
||||||
@@ -69,12 +70,15 @@ export class ParticleUpdateSystem extends EntitySystem {
|
|||||||
private _renderDataProvider: ParticleRenderDataProvider;
|
private _renderDataProvider: ParticleRenderDataProvider;
|
||||||
private _engineIntegration: IEngineIntegration | null = null;
|
private _engineIntegration: IEngineIntegration | null = null;
|
||||||
private _engineBridge: IEngineBridge | null = null;
|
private _engineBridge: IEngineBridge | null = null;
|
||||||
|
private _physics2DQuery: IPhysics2DQuery | null = null;
|
||||||
private _defaultTextureLoaded: boolean = false;
|
private _defaultTextureLoaded: boolean = false;
|
||||||
private _defaultTextureLoading: boolean = false;
|
private _defaultTextureLoading: boolean = false;
|
||||||
/** 追踪每个粒子组件上次加载的资产 GUID | Track last loaded asset GUID for each particle component */
|
/** 追踪每个粒子组件上次加载的资产 GUID | Track last loaded asset GUID for each particle component */
|
||||||
private _lastLoadedGuids: WeakMap<ParticleSystemComponent, string> = new WeakMap();
|
private _lastLoadedGuids: WeakMap<ParticleSystemComponent, string> = new WeakMap();
|
||||||
/** 正在加载资产的粒子组件 | Particle components currently loading assets */
|
/** 正在加载资产的粒子组件 | Particle components currently loading assets */
|
||||||
private _loadingComponents: WeakSet<ParticleSystemComponent> = new WeakSet();
|
private _loadingComponents: WeakSet<ParticleSystemComponent> = new WeakSet();
|
||||||
|
/** 已注入物理查询的粒子组件 | Particle components with physics query injected */
|
||||||
|
private _physicsInjectedComponents: WeakSet<ParticleSystemComponent> = new WeakSet();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Matcher.empty().all(ParticleSystemComponent));
|
super(Matcher.empty().all(ParticleSystemComponent));
|
||||||
@@ -107,6 +111,19 @@ export class ParticleUpdateSystem extends EntitySystem {
|
|||||||
this._engineBridge = bridge;
|
this._engineBridge = bridge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 2D 物理查询接口
|
||||||
|
* Set 2D physics query interface
|
||||||
|
*
|
||||||
|
* 如果设置,将自动注入到所有使用 Physics2DCollisionModule 的粒子系统。
|
||||||
|
* If set, will be auto-injected into all particle systems using Physics2DCollisionModule.
|
||||||
|
*
|
||||||
|
* @param query - 物理查询接口(通常是 Physics2DService)| Physics query (usually Physics2DService)
|
||||||
|
*/
|
||||||
|
setPhysics2DQuery(query: IPhysics2DQuery | null): void {
|
||||||
|
this._physics2DQuery = query;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取渲染数据提供者
|
* 获取渲染数据提供者
|
||||||
* Get render data provider
|
* Get render data provider
|
||||||
@@ -164,6 +181,11 @@ export class ParticleUpdateSystem extends EntitySystem {
|
|||||||
// This allows property changes to take effect immediately in the editor
|
// This allows property changes to take effect immediately in the editor
|
||||||
particle.ensureBuilt();
|
particle.ensureBuilt();
|
||||||
|
|
||||||
|
// 自动注入物理查询到 Physics2DCollisionModule | Auto-inject physics query to Physics2DCollisionModule
|
||||||
|
if (this._physics2DQuery && !this._physicsInjectedComponents.has(particle)) {
|
||||||
|
this._injectPhysics2DQuery(particle);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新粒子系统 | Update particle system
|
// 更新粒子系统 | Update particle system
|
||||||
if (particle.isPlaying) {
|
if (particle.isPlaying) {
|
||||||
particle.update(deltaTime, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
|
particle.update(deltaTime, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
|
||||||
@@ -366,9 +388,27 @@ export class ParticleUpdateSystem extends EntitySystem {
|
|||||||
if (particle) {
|
if (particle) {
|
||||||
// 从渲染数据提供者注销 | Unregister from render data provider
|
// 从渲染数据提供者注销 | Unregister from render data provider
|
||||||
this._renderDataProvider.unregister(particle);
|
this._renderDataProvider.unregister(particle);
|
||||||
|
// 清除物理注入标记 | Clear physics injection mark
|
||||||
|
this._physicsInjectedComponents.delete(particle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入物理查询到粒子系统的 Physics2DCollisionModule
|
||||||
|
* Inject physics query into particle system's Physics2DCollisionModule
|
||||||
|
*/
|
||||||
|
private _injectPhysics2DQuery(particle: ParticleSystemComponent): void {
|
||||||
|
if (!this._physics2DQuery) return;
|
||||||
|
|
||||||
|
for (const module of particle.modules) {
|
||||||
|
if (module instanceof Physics2DCollisionModule) {
|
||||||
|
module.setPhysicsQuery(this._physics2DQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._physicsInjectedComponents.add(particle);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统销毁时清理
|
* 系统销毁时清理
|
||||||
* Cleanup on system destroy
|
* Cleanup on system destroy
|
||||||
|
|||||||
@@ -21,6 +21,34 @@ import { Physics2DService } from './services/Physics2DService';
|
|||||||
// 注册 Rapier2D 加载器
|
// 注册 Rapier2D 加载器
|
||||||
import './loaders';
|
import './loaders';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2D 物理查询接口
|
||||||
|
* 2D Physics query interface
|
||||||
|
*
|
||||||
|
* 用于粒子系统等模块查询物理世界
|
||||||
|
* Used by particle system and other modules to query physics world
|
||||||
|
*/
|
||||||
|
export interface IPhysics2DQuery {
|
||||||
|
overlapCircle(
|
||||||
|
center: { x: number; y: number },
|
||||||
|
radius: number,
|
||||||
|
collisionMask?: number
|
||||||
|
): { entityIds: number[]; colliderHandles: number[] };
|
||||||
|
|
||||||
|
raycast(
|
||||||
|
origin: { x: number; y: number },
|
||||||
|
direction: { x: number; y: number },
|
||||||
|
maxDistance: number,
|
||||||
|
collisionMask?: number
|
||||||
|
): {
|
||||||
|
entityId: number;
|
||||||
|
point: { x: number; y: number };
|
||||||
|
normal: { x: number; y: number };
|
||||||
|
distance: number;
|
||||||
|
colliderHandle: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 物理系统上下文扩展
|
* 物理系统上下文扩展
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +67,18 @@ export interface PhysicsSystemContext extends SystemContext {
|
|||||||
* 物理配置
|
* 物理配置
|
||||||
*/
|
*/
|
||||||
physicsConfig?: any;
|
physicsConfig?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2D 物理查询接口
|
||||||
|
* 2D Physics query interface
|
||||||
|
*
|
||||||
|
* 供粒子系统等模块使用,用于检测粒子与物理碰撞体的碰撞。
|
||||||
|
* 实际上是 Physics2DSystem 的引用,因为它实现了 IPhysics2DQuery 接口。
|
||||||
|
*
|
||||||
|
* For particle system and other modules to detect collision with physics colliders.
|
||||||
|
* Actually a reference to Physics2DSystem which implements IPhysics2DQuery interface.
|
||||||
|
*/
|
||||||
|
physics2DQuery?: IPhysics2DQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,6 +183,9 @@ class PhysicsRuntimeModule implements IRuntimeModule {
|
|||||||
|
|
||||||
physicsContext.physicsSystem = physicsSystem;
|
physicsContext.physicsSystem = physicsSystem;
|
||||||
physicsContext.physics2DWorld = physicsSystem.world;
|
physicsContext.physics2DWorld = physicsSystem.world;
|
||||||
|
// 同时暴露 physics2DQuery,供粒子系统等模块使用
|
||||||
|
// Also expose physics2DQuery for particle system and other modules
|
||||||
|
physicsContext.physics2DQuery = physicsSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ export { Physics2DPlugin } from './PhysicsEditorPlugin';
|
|||||||
|
|
||||||
// Runtime plugin (for game builds)
|
// Runtime plugin (for game builds)
|
||||||
export { PhysicsPlugin } from './PhysicsRuntimeModule';
|
export { PhysicsPlugin } from './PhysicsRuntimeModule';
|
||||||
|
|
||||||
|
// Physics query interface (for particle system integration)
|
||||||
|
export type { IPhysics2DQuery, PhysicsSystemContext } from './PhysicsRuntimeModule';
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1067,6 +1067,9 @@ importers:
|
|||||||
'@esengine/engine-core':
|
'@esengine/engine-core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../engine-core
|
version: link:../engine-core
|
||||||
|
'@esengine/physics-rapier2d':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../physics-rapier2d
|
||||||
rimraf:
|
rimraf:
|
||||||
specifier: ^5.0.5
|
specifier: ^5.0.5
|
||||||
version: 5.0.10
|
version: 5.0.10
|
||||||
|
|||||||
Reference in New Issue
Block a user