feat(particle): 添加完整粒子系统和粒子编辑器 (#284)
* feat(editor-core): 添加用户系统自动注册功能 - IUserCodeService 新增 registerSystems/unregisterSystems/getRegisteredSystems 方法 - UserCodeService 实现系统检测、实例化和场景注册逻辑 - ServiceRegistry 在预览开始时注册用户系统,停止时移除 - 热更新时自动重新加载用户系统 - 更新 System 脚本模板添加 @ECSSystem 装饰器 * feat(editor-core): 添加编辑器脚本支持(Inspector/Gizmo) - registerEditorExtensions 实际注册用户 Inspector 和 Gizmo - 添加 unregisterEditorExtensions 方法 - ServiceRegistry 在项目加载时编译并加载编辑器脚本 - 项目关闭时自动清理编辑器扩展 - 添加 Inspector 和 Gizmo 脚本创建模板 * feat(particle): 添加粒子系统和粒子编辑器 新增两个包: - @esengine/particle: 粒子系统核心库 - @esengine/particle-editor: 粒子编辑器 UI 粒子系统功能: - ECS 组件架构,支持播放/暂停/重置控制 - 7种发射形状:点、圆、环、矩形、边缘、线、锥形 - 5个动画模块:颜色渐变、缩放曲线、速度控制、旋转、噪声 - 纹理动画模块支持精灵表动画 - 3种混合模式:Normal、Additive、Multiply - 11个内置预设:火焰、烟雾、爆炸、雨、雪等 - 对象池优化,支持粒子复用 编辑器功能: - 实时 Canvas 预览,支持全屏和鼠标跟随 - 点击触发爆发效果(用于测试爆炸类特效) - 渐变编辑器:可视化颜色关键帧编辑 - 曲线编辑器:支持缩放曲线和缓动函数 - 预设浏览器:快速应用内置预设 - 模块开关:独立启用/禁用各个模块 - Vector2 样式输入(重力 X/Y) * feat(particle): 完善粒子系统核心功能 1. Burst 定时爆发系统 - BurstConfig 接口支持时间、数量、循环次数、间隔 - 运行时自动处理定时爆发 - 支持无限循环爆发 2. 速度曲线模块 (VelocityOverLifetimeModule) - 6种曲线类型:Constant、Linear、EaseIn、EaseOut、EaseInOut、Custom - 自定义关键帧曲线支持 - 附加速度 X/Y - 轨道速度和径向速度 3. 碰撞边界模块 (CollisionModule) - 矩形和圆形边界类型 - 3种碰撞行为:Kill、Bounce、Wrap - 反弹系数和最小速度阈值 - 反弹时生命损失 * feat(particle): 添加力场模块、碰撞模块和世界/本地空间支持 - 新增 ForceFieldModule 支持风力、吸引点、漩涡、湍流四种力场类型 - 新增 SimulationSpace 枚举支持世界空间和本地空间切换 - ParticleSystemComponent 集成力场模块和空间模式 - 粒子编辑器添加 Collision 和 ForceField 模块的 UI 编辑支持 - 新增 Vortex、Leaves、Bouncing 三个预设展示新功能 - 编辑器预览实现完整的碰撞和力场效果 * fix(particle): 移除未使用的 transform 循环变量
This commit is contained in:
600
packages/particle/src/ParticleSystemComponent.ts
Normal file
600
packages/particle/src/ParticleSystemComponent.ts
Normal file
@@ -0,0 +1,600 @@
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { ParticlePool } from './Particle';
|
||||
import { ParticleEmitter, EmissionShape, createDefaultEmitterConfig, type EmitterConfig, type ValueRange, type ColorValue } from './ParticleEmitter';
|
||||
import type { IParticleModule } from './modules/IParticleModule';
|
||||
import { ColorOverLifetimeModule } from './modules/ColorOverLifetimeModule';
|
||||
import { SizeOverLifetimeModule } from './modules/SizeOverLifetimeModule';
|
||||
import { CollisionModule } from './modules/CollisionModule';
|
||||
import { ForceFieldModule } from './modules/ForceFieldModule';
|
||||
|
||||
/**
|
||||
* 爆发配置
|
||||
* Burst configuration
|
||||
*/
|
||||
export interface BurstConfig {
|
||||
/** 触发时间(秒)| Trigger time (seconds) */
|
||||
time: number;
|
||||
/** 发射数量 | Particle count */
|
||||
count: number;
|
||||
/** 循环次数(0=无限)| Number of cycles (0=infinite) */
|
||||
cycles: number;
|
||||
/** 循环间隔(秒)| Interval between cycles (seconds) */
|
||||
interval: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 粒子混合模式
|
||||
* Particle blend mode
|
||||
*/
|
||||
export enum ParticleBlendMode {
|
||||
/** 正常混合 | Normal blend */
|
||||
Normal = 'normal',
|
||||
/** 叠加 | Additive */
|
||||
Additive = 'additive',
|
||||
/** 正片叠底 | Multiply */
|
||||
Multiply = 'multiply'
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟空间
|
||||
* Simulation space
|
||||
*/
|
||||
export enum SimulationSpace {
|
||||
/** 本地空间(粒子跟随发射器)| Local space (particles follow emitter) */
|
||||
Local = 'local',
|
||||
/** 世界空间(粒子不跟随发射器)| World space (particles don't follow emitter) */
|
||||
World = 'world'
|
||||
}
|
||||
|
||||
/**
|
||||
* 粒子系统组件
|
||||
* Particle system component
|
||||
*
|
||||
* Manages particle emission, simulation, and provides data for rendering.
|
||||
* 管理粒子发射、模拟,并为渲染提供数据。
|
||||
*/
|
||||
@ECSComponent('ParticleSystem')
|
||||
@Serializable({ version: 1, typeId: 'ParticleSystem' })
|
||||
export class ParticleSystemComponent extends Component {
|
||||
// ============= 基础属性 | Basic Properties =============
|
||||
|
||||
/** 最大粒子数量 | Maximum particle count */
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Max Particles', min: 1, max: 10000 })
|
||||
public maxParticles: number = 1000;
|
||||
|
||||
/** 是否循环播放 | Whether to loop */
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Looping' })
|
||||
public looping: boolean = true;
|
||||
|
||||
/** 预热时间(秒)| Prewarm time (seconds) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Prewarm Time', min: 0 })
|
||||
public prewarmTime: number = 0;
|
||||
|
||||
/** 持续时间(秒,非循环时使用)| Duration (seconds, for non-looping) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Duration', min: 0.1 })
|
||||
public duration: number = 5;
|
||||
|
||||
/** 播放速度倍率 | Playback speed multiplier */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Playback Speed', min: 0.01, max: 10 })
|
||||
public playbackSpeed: number = 1;
|
||||
|
||||
/** 模拟空间 | Simulation space */
|
||||
@Serialize()
|
||||
@Property({ type: 'enum', label: 'Simulation Space', options: [
|
||||
{ value: 'world', label: 'World' },
|
||||
{ value: 'local', label: 'Local' }
|
||||
]})
|
||||
public simulationSpace: SimulationSpace = SimulationSpace.World;
|
||||
|
||||
// ============= 发射器属性 | Emitter Properties =============
|
||||
|
||||
/** 每秒发射数量 | Emission rate (particles per second) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Emission Rate', min: 0 })
|
||||
public emissionRate: number = 10;
|
||||
|
||||
/** 发射形状 | Emission shape */
|
||||
@Serialize()
|
||||
@Property({ type: 'enum', label: 'Shape', options: [
|
||||
{ value: 'point', label: 'Point' },
|
||||
{ value: 'circle', label: 'Circle' },
|
||||
{ value: 'rectangle', label: 'Rectangle' },
|
||||
{ value: 'line', label: 'Line' },
|
||||
{ value: 'cone', label: 'Cone' }
|
||||
]})
|
||||
public emissionShape: EmissionShape = EmissionShape.Point;
|
||||
|
||||
/** 形状半径 | Shape radius */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Shape Radius', min: 0 })
|
||||
public shapeRadius: number = 0;
|
||||
|
||||
/** 形状宽度 | Shape width */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Shape Width', min: 0 })
|
||||
public shapeWidth: number = 0;
|
||||
|
||||
/** 形状高度 | Shape height */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Shape Height', min: 0 })
|
||||
public shapeHeight: number = 0;
|
||||
|
||||
// ============= 粒子属性 | Particle Properties =============
|
||||
|
||||
/** 粒子生命时间最小值(秒)| Particle lifetime min (seconds) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Lifetime Min', min: 0.01 })
|
||||
public lifetimeMin: number = 1;
|
||||
|
||||
/** 粒子生命时间最大值(秒)| Particle lifetime max (seconds) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Lifetime Max', min: 0.01 })
|
||||
public lifetimeMax: number = 2;
|
||||
|
||||
/** 初始速度最小值 | Initial speed min */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Speed Min', min: 0 })
|
||||
public speedMin: number = 50;
|
||||
|
||||
/** 初始速度最大值 | Initial speed max */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Speed Max', min: 0 })
|
||||
public speedMax: number = 100;
|
||||
|
||||
/** 发射方向(角度)| Emission direction (degrees) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Direction', min: -180, max: 180 })
|
||||
public direction: number = -90;
|
||||
|
||||
/** 发射方向扩散(角度)| Direction spread (degrees) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Direction Spread', min: 0, max: 360 })
|
||||
public directionSpread: number = 0;
|
||||
|
||||
/** 初始缩放最小值 | Initial scale min */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scale Min', min: 0.01 })
|
||||
public scaleMin: number = 1;
|
||||
|
||||
/** 初始缩放最大值 | Initial scale max */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Scale Max', min: 0.01 })
|
||||
public scaleMax: number = 1;
|
||||
|
||||
/** 重力X | Gravity X */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gravity X' })
|
||||
public gravityX: number = 0;
|
||||
|
||||
/** 重力Y | Gravity Y */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gravity Y' })
|
||||
public gravityY: number = 0;
|
||||
|
||||
// ============= 颜色属性 | Color Properties =============
|
||||
|
||||
/** 起始颜色 | Start color */
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Start Color' })
|
||||
public startColor: string = '#ffffff';
|
||||
|
||||
/** 起始透明度 | Start alpha */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Start Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public startAlpha: number = 1;
|
||||
|
||||
/** 结束透明度(淡出)| End alpha (fade out) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'End Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public endAlpha: number = 0;
|
||||
|
||||
/** 结束缩放乘数 | End scale multiplier */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'End Scale', min: 0 })
|
||||
public endScale: number = 0;
|
||||
|
||||
// ============= 渲染属性 | Rendering Properties =============
|
||||
|
||||
/** 粒子纹理 | Particle texture */
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public texture: string = '';
|
||||
|
||||
/** 粒子尺寸(像素)| Particle size (pixels) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Particle Size', min: 1 })
|
||||
public particleSize: number = 8;
|
||||
|
||||
/** 混合模式 | Blend mode */
|
||||
@Serialize()
|
||||
@Property({ type: 'enum', label: 'Blend Mode', options: [
|
||||
{ value: 'normal', label: 'Normal' },
|
||||
{ value: 'additive', label: 'Additive' },
|
||||
{ value: 'multiply', label: 'Multiply' }
|
||||
]})
|
||||
public blendMode: ParticleBlendMode = ParticleBlendMode.Additive;
|
||||
|
||||
/** 排序顺序 | Sorting order */
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Sorting Order' })
|
||||
public sortingOrder: number = 0;
|
||||
|
||||
// ============= 爆发配置 | Burst Configuration =============
|
||||
|
||||
/** 爆发列表 | Burst list */
|
||||
@Serialize()
|
||||
public bursts: BurstConfig[] = [];
|
||||
|
||||
// ============= 运行时状态 | Runtime State =============
|
||||
|
||||
private _pool: ParticlePool | null = null;
|
||||
private _emitter: ParticleEmitter | null = null;
|
||||
private _modules: IParticleModule[] = [];
|
||||
private _isPlaying: boolean = false;
|
||||
private _elapsedTime: number = 0;
|
||||
private _needsRebuild: boolean = true;
|
||||
/** 爆发状态追踪 | Burst state tracking */
|
||||
private _burstStates: { firedCount: number; lastFireTime: number }[] = [];
|
||||
/** 上一帧发射器位置(本地空间用)| Last frame emitter position (for local space) */
|
||||
private _lastEmitterX: number = 0;
|
||||
private _lastEmitterY: number = 0;
|
||||
|
||||
/** 纹理ID(运行时)| Texture ID (runtime) */
|
||||
public textureId: number = 0;
|
||||
|
||||
/** 是否正在播放 | Whether playing */
|
||||
get isPlaying(): boolean {
|
||||
return this._isPlaying;
|
||||
}
|
||||
|
||||
/** 已播放时间 | Elapsed time */
|
||||
get elapsedTime(): number {
|
||||
return this._elapsedTime;
|
||||
}
|
||||
|
||||
/** 活跃粒子数 | Active particle count */
|
||||
get activeParticleCount(): number {
|
||||
return this._pool?.activeCount ?? 0;
|
||||
}
|
||||
|
||||
/** 粒子池 | Particle pool */
|
||||
get pool(): ParticlePool | null {
|
||||
return this._pool;
|
||||
}
|
||||
|
||||
/** 粒子模块列表 | Particle modules */
|
||||
get modules(): IParticleModule[] {
|
||||
return this._modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化粒子系统
|
||||
* Initialize particle system
|
||||
*/
|
||||
initialize(): void {
|
||||
this._rebuildIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放粒子系统
|
||||
* Play particle system
|
||||
*
|
||||
* @param worldX - Initial world position X for prewarm | 预热时的初始世界坐标X
|
||||
* @param worldY - Initial world position Y for prewarm | 预热时的初始世界坐标Y
|
||||
*/
|
||||
play(worldX: number = 0, worldY: number = 0): void {
|
||||
this._rebuildIfNeeded();
|
||||
this._isPlaying = true;
|
||||
this._emitter!.isEmitting = true;
|
||||
this._elapsedTime = 0;
|
||||
// 初始化爆发状态 | Initialize burst states
|
||||
this._burstStates = this.bursts.map(() => ({ firedCount: 0, lastFireTime: -Infinity }));
|
||||
// 初始化发射器位置 | Initialize emitter position
|
||||
this._lastEmitterX = worldX;
|
||||
this._lastEmitterY = worldY;
|
||||
|
||||
if (this.prewarmTime > 0) {
|
||||
this._simulate(this.prewarmTime, worldX, worldY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止粒子系统
|
||||
* Stop particle system
|
||||
*/
|
||||
stop(clearParticles: boolean = false): void {
|
||||
this._isPlaying = false;
|
||||
this._emitter!.isEmitting = false;
|
||||
this._elapsedTime = 0;
|
||||
// 重置爆发状态 | Reset burst states
|
||||
this._burstStates = this.bursts.map(() => ({ firedCount: 0, lastFireTime: -Infinity }));
|
||||
|
||||
if (clearParticles) {
|
||||
this._pool?.recycleAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停粒子系统
|
||||
* Pause particle system
|
||||
*/
|
||||
pause(): void {
|
||||
this._isPlaying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即爆发发射
|
||||
* Burst emit
|
||||
*
|
||||
* @param count - Number of particles to emit | 发射的粒子数量
|
||||
* @param worldX - World position X | 世界坐标X
|
||||
* @param worldY - World position Y | 世界坐标Y
|
||||
*/
|
||||
burst(count: number, worldX: number = 0, worldY: number = 0): void {
|
||||
this._rebuildIfNeeded();
|
||||
if (!this._emitter || !this._pool) return;
|
||||
|
||||
this._emitter.burst(this._pool, count, worldX, worldY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新粒子系统
|
||||
* Update particle system
|
||||
*
|
||||
* @param dt - Delta time in seconds | 时间增量(秒)
|
||||
* @param worldX - World position X for emission | 发射位置世界坐标X
|
||||
* @param worldY - World position Y for emission | 发射位置世界坐标Y
|
||||
*/
|
||||
update(dt: number, worldX: number = 0, worldY: number = 0): void {
|
||||
if (!this._isPlaying || !this._pool || !this._emitter) return;
|
||||
|
||||
const scaledDt = dt * this.playbackSpeed;
|
||||
this._simulate(scaledDt, worldX, worldY);
|
||||
this._elapsedTime += scaledDt;
|
||||
|
||||
// 检查持续时间 | Check duration
|
||||
if (!this.looping && this._elapsedTime >= this.duration) {
|
||||
this._emitter.isEmitting = false;
|
||||
if (this._pool.activeCount === 0) {
|
||||
this._isPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模块
|
||||
* Add module
|
||||
*/
|
||||
addModule<T extends IParticleModule>(module: T): T {
|
||||
this._modules.push(module);
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块
|
||||
* Get module by type
|
||||
*/
|
||||
getModule<T extends IParticleModule>(name: string): T | undefined {
|
||||
return this._modules.find(m => m.name === name) as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除模块
|
||||
* Remove module
|
||||
*/
|
||||
removeModule(module: IParticleModule): boolean {
|
||||
const index = this._modules.indexOf(module);
|
||||
if (index >= 0) {
|
||||
this._modules.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记需要重建
|
||||
* Mark for rebuild
|
||||
*/
|
||||
markDirty(): void {
|
||||
this._needsRebuild = true;
|
||||
}
|
||||
|
||||
private _rebuildIfNeeded(): void {
|
||||
if (!this._needsRebuild && this._pool && this._emitter) return;
|
||||
|
||||
// 创建/调整粒子池 | Create/resize particle pool
|
||||
if (!this._pool) {
|
||||
this._pool = new ParticlePool(this.maxParticles);
|
||||
} else if (this._pool.capacity !== this.maxParticles) {
|
||||
this._pool.resize(this.maxParticles);
|
||||
}
|
||||
|
||||
// 解析颜色 | Parse color
|
||||
const color = this._parseColor(this.startColor);
|
||||
|
||||
// 创建发射器配置 | Create emitter config
|
||||
const config: EmitterConfig = {
|
||||
...createDefaultEmitterConfig(),
|
||||
emissionRate: this.emissionRate,
|
||||
burstCount: 0,
|
||||
lifetime: { min: this.lifetimeMin, max: this.lifetimeMax },
|
||||
shape: this.emissionShape,
|
||||
shapeRadius: this.shapeRadius,
|
||||
shapeWidth: this.shapeWidth,
|
||||
shapeHeight: this.shapeHeight,
|
||||
coneAngle: Math.PI / 6,
|
||||
direction: this.direction * Math.PI / 180,
|
||||
directionSpread: this.directionSpread * Math.PI / 180,
|
||||
speed: { min: this.speedMin, max: this.speedMax },
|
||||
angularVelocity: { min: 0, max: 0 },
|
||||
startScale: { min: this.scaleMin, max: this.scaleMax },
|
||||
startRotation: { min: 0, max: 0 },
|
||||
startColor: { ...color, a: this.startAlpha },
|
||||
startColorVariance: { r: 0, g: 0, b: 0, a: 0 },
|
||||
gravityX: this.gravityX,
|
||||
gravityY: this.gravityY
|
||||
};
|
||||
|
||||
if (!this._emitter) {
|
||||
this._emitter = new ParticleEmitter(config);
|
||||
} else {
|
||||
this._emitter.config = config;
|
||||
}
|
||||
|
||||
// 设置默认模块 | Setup default modules
|
||||
if (this._modules.length === 0) {
|
||||
// 颜色模块(淡出)| Color module (fade out)
|
||||
const colorModule = new ColorOverLifetimeModule();
|
||||
colorModule.gradient = [
|
||||
{ time: 0, r: 1, g: 1, b: 1, a: 1 },
|
||||
{ time: 1, r: 1, g: 1, b: 1, a: this.endAlpha }
|
||||
];
|
||||
this._modules.push(colorModule);
|
||||
|
||||
// 缩放模块 | Size module
|
||||
const sizeModule = new SizeOverLifetimeModule();
|
||||
sizeModule.startMultiplier = 1;
|
||||
sizeModule.endMultiplier = this.endScale;
|
||||
this._modules.push(sizeModule);
|
||||
}
|
||||
|
||||
this._needsRebuild = false;
|
||||
}
|
||||
|
||||
private _simulate(dt: number, worldX: number, worldY: number): void {
|
||||
if (!this._pool || !this._emitter) return;
|
||||
|
||||
// 本地空间:计算发射器移动量 | Local space: calculate emitter movement
|
||||
const isLocalSpace = this.simulationSpace === SimulationSpace.Local;
|
||||
const emitterDeltaX = worldX - this._lastEmitterX;
|
||||
const emitterDeltaY = worldY - this._lastEmitterY;
|
||||
|
||||
// 发射新粒子 | Emit new particles
|
||||
this._emitter.emit(this._pool, dt, worldX, worldY);
|
||||
|
||||
// 处理定时爆发 | Process timed bursts
|
||||
this._processBursts(worldX, worldY);
|
||||
|
||||
// 查找碰撞模块并更新发射器位置 | Find collision module and update emitter position
|
||||
const collisionModule = this._modules.find(m => m instanceof CollisionModule) as CollisionModule | undefined;
|
||||
if (collisionModule) {
|
||||
collisionModule.emitterX = worldX;
|
||||
collisionModule.emitterY = worldY;
|
||||
collisionModule.clearDeathFlags();
|
||||
}
|
||||
|
||||
// 查找力场模块并更新发射器位置 | Find force field module and update emitter position
|
||||
const forceFieldModule = this._modules.find(m => m instanceof ForceFieldModule) as ForceFieldModule | undefined;
|
||||
if (forceFieldModule) {
|
||||
forceFieldModule.emitterX = worldX;
|
||||
forceFieldModule.emitterY = worldY;
|
||||
}
|
||||
|
||||
// 更新粒子 | Update particles
|
||||
this._pool.forEachActive((p) => {
|
||||
// 本地空间:粒子跟随发射器移动 | Local space: particles follow emitter
|
||||
if (isLocalSpace) {
|
||||
p.x += emitterDeltaX;
|
||||
p.y += emitterDeltaY;
|
||||
}
|
||||
|
||||
// 物理更新 | Physics update
|
||||
p.vx += p.ax * dt;
|
||||
p.vy += p.ay * dt;
|
||||
p.x += p.vx * dt;
|
||||
p.y += p.vy * dt;
|
||||
p.age += dt;
|
||||
|
||||
// 应用模块 | Apply modules
|
||||
const normalizedAge = p.age / p.lifetime;
|
||||
for (const module of this._modules) {
|
||||
if (module.enabled) {
|
||||
module.update(p, dt, normalizedAge);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查生命周期 | Check lifetime
|
||||
if (p.age >= p.lifetime) {
|
||||
this._pool!.recycle(p);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理碰撞模块标记的死亡粒子 | Process particles marked for death by collision module
|
||||
if (collisionModule) {
|
||||
const particlesToKill = collisionModule.getParticlesToKill();
|
||||
for (const p of particlesToKill) {
|
||||
this._pool.recycle(p);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录发射器位置供下一帧使用 | Record emitter position for next frame
|
||||
this._lastEmitterX = worldX;
|
||||
this._lastEmitterY = worldY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理定时爆发
|
||||
* Process timed bursts
|
||||
*/
|
||||
private _processBursts(worldX: number, worldY: number): void {
|
||||
if (!this._pool || !this._emitter || this.bursts.length === 0) return;
|
||||
|
||||
// 确保爆发状态数组与配置同步 | Ensure burst states array is synced with config
|
||||
while (this._burstStates.length < this.bursts.length) {
|
||||
this._burstStates.push({ firedCount: 0, lastFireTime: -Infinity });
|
||||
}
|
||||
|
||||
const currentTime = this._elapsedTime;
|
||||
|
||||
for (let i = 0; i < this.bursts.length; i++) {
|
||||
const burst = this.bursts[i];
|
||||
const state = this._burstStates[i];
|
||||
|
||||
// 检查是否已达到循环次数上限 | Check if reached cycle limit
|
||||
if (burst.cycles > 0 && state.firedCount >= burst.cycles) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算下次触发时间 | Calculate next fire time
|
||||
let nextFireTime: number;
|
||||
if (state.firedCount === 0) {
|
||||
// 首次触发 | First fire
|
||||
nextFireTime = burst.time;
|
||||
} else {
|
||||
// 循环触发 | Cycle fire
|
||||
nextFireTime = state.lastFireTime + burst.interval;
|
||||
}
|
||||
|
||||
// 检查是否应该触发 | Check if should fire
|
||||
if (currentTime >= nextFireTime) {
|
||||
this._emitter.burst(this._pool, burst.count, worldX, worldY);
|
||||
state.firedCount++;
|
||||
state.lastFireTime = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _parseColor(hex: string): { r: number; g: number; b: number } {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (result) {
|
||||
return {
|
||||
r: parseInt(result[1], 16) / 255,
|
||||
g: parseInt(result[2], 16) / 255,
|
||||
b: parseInt(result[3], 16) / 255
|
||||
};
|
||||
}
|
||||
return { r: 1, g: 1, b: 1 };
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
this._pool?.recycleAll();
|
||||
this._pool = null;
|
||||
this._emitter = null;
|
||||
this._modules = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user