Files
esengine/packages/particle/src/systems/ParticleSystem.ts
YHH 32d35ef2ee 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 循环变量
2025-12-05 23:03:31 +08:00

119 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { EntitySystem, Matcher, ECSSystem, Time, Entity } from '@esengine/ecs-framework';
import { ParticleSystemComponent } from '../ParticleSystemComponent';
import { ParticleRenderDataProvider } from '../rendering/ParticleRenderDataProvider';
/**
* Transform 组件接口(避免直接依赖 engine-core
* Transform component interface (avoid direct dependency on engine-core)
*/
interface ITransformComponent {
worldPosition?: { x: number; y: number; z: number };
position: { x: number; y: number; z: number };
}
/**
* 粒子更新系统
* Particle update system
*
* Updates all ParticleSystemComponents with their entity's world position.
* 使用实体的世界坐标更新所有粒子系统组件。
*/
@ECSSystem('ParticleUpdate', { updateOrder: 100 })
export class ParticleUpdateSystem extends EntitySystem {
private _transformType: (new (...args: any[]) => ITransformComponent) | null = null;
private _renderDataProvider: ParticleRenderDataProvider;
constructor() {
super(Matcher.empty().all(ParticleSystemComponent));
this._renderDataProvider = new ParticleRenderDataProvider();
}
/**
* 设置 Transform 组件类型
* Set Transform component type
*
* @param transformType - Transform component class | Transform 组件类
*/
setTransformType(transformType: new (...args: any[]) => ITransformComponent): void {
this._transformType = transformType;
}
/**
* 获取渲染数据提供者
* Get render data provider
*/
getRenderDataProvider(): ParticleRenderDataProvider {
return this._renderDataProvider;
}
protected override process(entities: readonly Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
if (!entity.enabled) continue;
const particle = entity.getComponent(ParticleSystemComponent) as ParticleSystemComponent | null;
if (!particle) continue;
let worldX = 0;
let worldY = 0;
let transform: ITransformComponent | null = null;
// 获取 Transform 位置 | Get Transform position
if (this._transformType) {
transform = entity.getComponent(this._transformType as any) as ITransformComponent | null;
if (transform) {
const pos = transform.worldPosition ?? transform.position;
worldX = pos.x;
worldY = pos.y;
}
}
// 更新粒子系统 | Update particle system
if (particle.isPlaying) {
particle.update(deltaTime, worldX, worldY);
}
// 更新渲染数据提供者的 Transform 引用 | Update render data provider's Transform reference
if (transform) {
this._renderDataProvider.register(particle, transform);
}
}
// 标记渲染数据需要更新 | Mark render data as dirty
this._renderDataProvider.markDirty();
}
protected override onAdded(entity: Entity): void {
const particle = entity.getComponent(ParticleSystemComponent) as ParticleSystemComponent | null;
if (particle) {
particle.initialize();
// 注册到渲染数据提供者 | Register to render data provider
if (this._transformType) {
const transform = entity.getComponent(this._transformType as any) as ITransformComponent | null;
if (transform) {
this._renderDataProvider.register(particle, transform);
}
}
}
}
protected override onRemoved(entity: Entity): void {
const particle = entity.getComponent(ParticleSystemComponent) as ParticleSystemComponent | null;
if (particle) {
// 从渲染数据提供者注销 | Unregister from render data provider
this._renderDataProvider.unregister(particle);
}
}
/**
* 系统销毁时清理
* Cleanup on system destroy
*/
public override destroy(): void {
super.destroy();
this._renderDataProvider.dispose();
}
}