Files
esengine/packages/particle/src/ParticleSystemComponent.ts
YHH ed8f6e283b feat: 纹理路径稳定 ID 与架构改进 (#305)
* feat(asset-system): 实现路径稳定 ID 生成器

使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。

* fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用

使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效

* fix(editor-core): 修复场景切换时的资源泄漏

在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性

* fix(runtime-core): 修复 PluginManager 组件注册类型错误

将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例

* refactor(core): 提取 IComponentRegistry 接口

将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表

* refactor(engine-core): 改进插件服务注册机制

- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理

* refactor(modules): 适配新的组件注册接口

更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming

* fix(physics-rapier2d): 修复物理插件组件注册

- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题

* feat(editor-core): 添加 UserCodeService 就绪信号机制

- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题

* fix(editor-app): 在编译完成后调用 signalReady()

确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载

* feat(editor-core): 改进编辑器核心服务

- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口

* feat(engine): 改进 Rust 纹理管理器

- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定

* feat(ui): 添加场景切换和文本闪烁组件

新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化

* feat(editor-app): 添加外部文件修改检测

- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户

* feat(editor-app): 添加渲染调试面板

- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进

* refactor(editor-app): 编辑器服务和组件优化

- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进

* feat(i18n): 更新国际化翻译

- 添加新功能相关翻译
- 更新中文、英文、西班牙文

* feat(tauri): 添加文件修改时间查询命令

- 新增 get_file_mtime 命令
- 支持检测文件外部修改

* refactor(particle): 粒子系统改进

- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试

* refactor(platform): 平台适配层优化

- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化

* refactor(asset-system-editor): 资产元数据改进

- AssetMetaFile 优化
- 导出调整

* fix(asset-system): 移除未使用的 TextureLoader 导入

* fix(tests): 更新测试以使用 GlobalComponentRegistry 实例

修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
2025-12-16 12:46:14 +08:00

1049 lines
38 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 { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
import type { IAssetManager } from '@esengine/asset-system';
import { SortingLayers, type ISortable } from '@esengine/engine-core';
import { ParticlePool, type Particle } from './Particle';
import { ParticleEmitter, EmissionShape, createDefaultEmitterConfig, type EmitterConfig, type ColorValue } from './ParticleEmitter';
import type { IParticleModule } from './modules/IParticleModule';
import { ColorOverLifetimeModule } from './modules/ColorOverLifetimeModule';
import { SizeOverLifetimeModule } from './modules/SizeOverLifetimeModule';
import { CollisionModule, BoundaryType, CollisionBehavior } from './modules/CollisionModule';
import { ForceFieldModule, ForceFieldType, type ForceField } from './modules/ForceFieldModule';
import { Physics2DCollisionModule } from './modules/Physics2DCollisionModule';
import { TextureSheetAnimationModule, AnimationPlayMode, AnimationLoopMode } from './modules/TextureSheetAnimationModule';
import type { IParticleAsset, IBurstConfig } from './loaders/ParticleLoader';
// Re-export for backward compatibility
// 为了向后兼容重新导出
export type { IBurstConfig };
/** @deprecated Use IBurstConfig instead */
export type BurstConfig = IBurstConfig;
/**
* 粒子混合模式
* 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'
}
/**
* 渲染空间
* Render space
*
* 决定粒子在哪个坐标空间渲染。
* Determines which coordinate space the particles are rendered in.
*/
export enum RenderSpace {
/**
* 世界空间 - 受世界相机影响
* World space - affected by world camera
*/
World = 'world',
/**
* 屏幕空间 - 使用固定正交投影,不受世界相机影响
* Screen space - uses fixed orthographic projection, not affected by world camera
*
* 适用于点击特效、UI 粒子等需要固定在屏幕上的效果。
* Suitable for click effects, UI particles, and other effects that need to stay fixed on screen.
*/
Screen = 'screen'
}
/**
* 运行时覆盖配置
* Runtime override configuration
*
* 用于在游戏运行时动态修改粒子系统参数,而不影响原始资产配置。
* Used to dynamically modify particle system parameters at runtime without affecting the original asset configuration.
*/
export interface ParticleRuntimeOverrides {
/** 发射速率覆盖 | Emission rate override */
emissionRate?: number;
/** 播放速度覆盖 | Playback speed override */
playbackSpeed?: number;
/** 是否循环覆盖 | Looping override */
looping?: boolean;
/** 重力X覆盖 | Gravity X override */
gravityX?: number;
/** 重力Y覆盖 | Gravity Y override */
gravityY?: number;
/** 起始颜色覆盖 | Start color override */
startColor?: ColorValue;
/** 缩放乘数(应用于所有尺寸)| Scale multiplier (applied to all sizes) */
scaleMultiplier?: number;
/** 速度乘数 | Speed multiplier */
speedMultiplier?: number;
}
/**
* 粒子系统组件
* Particle system component
*
* 基于资产的粒子系统组件。所有粒子配置从 .particle 文件读取,
* 运行时可通过 runtimeOverrides 动态修改部分参数。
*
* Asset-based particle system component. All particle configuration is read from .particle files.
* Runtime modifications can be made through runtimeOverrides.
*
* @example
* ```typescript
* // 在编辑器中设置 particleAssetGuid运行时自动加载
* // Set particleAssetGuid in editor, loads automatically at runtime
*
* // 运行时修改发射速率
* // Modify emission rate at runtime
* particle.setOverride('emissionRate', 50);
*
* // 或批量设置
* // Or set multiple overrides
* particle.setOverrides({ emissionRate: 50, playbackSpeed: 2 });
*
* // 清除覆盖,恢复原始值
* // Clear overrides, restore original values
* particle.clearOverrides();
* ```
*/
@ECSComponent('ParticleSystem')
@Serializable({ version: 4, typeId: 'ParticleSystem' })
export class ParticleSystemComponent extends Component implements ISortable {
// ============= 资产引用 | Asset Reference =============
/**
* 粒子效果资产 GUID
* Particle effect asset GUID
*
* 必须设置此属性才能使用粒子系统。所有配置从 .particle 文件读取。
* Must be set to use the particle system. All configuration is read from .particle file.
*/
@Serialize()
@Property({ type: 'asset', label: 'Particle Asset', extensions: ['.particle', '.particle.json'] })
public particleAssetGuid: string = '';
// ============= 播放控制 | Playback Control =============
/**
* 是否自动播放
* Whether to auto-play on start
*
* 默认为 false在编辑器中需要手动点击播放按钮。
* 运行时场景中如需自动播放,请设置为 true。
*
* Default is false, manual play button click is required in editor.
* Set to true for auto-play in runtime scenes.
*/
@Serialize()
@Property({ type: 'boolean', label: 'Auto Play' })
public autoPlay: boolean = false;
/**
* 模拟空间
* Simulation space
*
* Local: 粒子跟随发射器移动
* World: 粒子在世界空间独立运动
*/
@Serialize()
@Property({
type: 'enum',
label: 'Simulation Space',
options: [
{ label: 'Local', value: SimulationSpace.Local },
{ label: 'World', value: SimulationSpace.World }
]
})
public simulationSpace: SimulationSpace = SimulationSpace.World;
// ============= 排序 | Sorting =============
/**
* 排序层
* Sorting layer
*
* 决定渲染的大类顺序。
* Determines the major render order category.
*/
@Serialize()
@Property({
type: 'enum',
label: 'Sorting Layer',
options: ['Background', 'Default', 'Foreground', 'WorldOverlay', 'UI', 'ScreenOverlay', 'Modal']
})
public sortingLayer: string = SortingLayers.Default;
/**
* 层内顺序(越高越在上面)
* Order within layer (higher = rendered on top)
*/
@Serialize()
@Property({ type: 'integer', label: 'Order in Layer' })
public orderInLayer: number = 0;
/**
* 渲染空间
* Render space
*
* World: 世界空间,受相机影响(默认)
* Screen: 屏幕空间,固定在屏幕上,适用于点击特效等
*/
@Serialize()
@Property({
type: 'enum',
label: 'Render Space',
options: [
{ label: 'World', value: RenderSpace.World },
{ label: 'Screen', value: RenderSpace.Screen }
]
})
public renderSpace: RenderSpace = RenderSpace.World;
// ============= 运行时覆盖 | Runtime Overrides =============
/**
* 运行时参数覆盖
* Runtime parameter overrides
*
* 这些值会覆盖资产中的对应配置。不会被序列化保存。
* These values override corresponding asset configuration. Not serialized.
*/
private _runtimeOverrides: ParticleRuntimeOverrides = {};
// ============= 运行时状态 | 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;
/** 当前世界旋转(弧度)| Current world rotation (radians) */
private _worldRotation: number = 0;
/** 当前世界缩放X | Current world scale X */
private _worldScaleX: number = 1;
/** 当前世界缩放Y | Current world scale Y */
private _worldScaleY: number = 1;
/** 已加载的粒子资产数据 | Loaded particle asset data */
private _loadedAsset: IParticleAsset | null = null;
/** 上次加载的资产 GUID用于检测变化| Last loaded asset GUID (for change detection) */
private _lastLoadedGuid: string = '';
/** 纹理ID运行时| Texture ID (runtime) */
public textureId: number = 0;
// ============= 公开属性访问器 | Public Property Accessors =============
/** 是否正在播放 | 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;
}
/** 已加载的资产数据 | Loaded asset data */
get loadedAsset(): IParticleAsset | null {
return this._loadedAsset;
}
/**
* 获取当前运行时覆盖配置
* Get current runtime overrides
*/
get runtimeOverrides(): Readonly<ParticleRuntimeOverrides> {
return this._runtimeOverrides;
}
/** 当前世界旋转(弧度)| Current world rotation (radians) */
get worldRotation(): number {
return this._worldRotation;
}
/** 当前世界缩放X | Current world scale X */
get worldScaleX(): number {
return this._worldScaleX;
}
/** 当前世界缩放Y | Current world scale Y */
get worldScaleY(): number {
return this._worldScaleY;
}
// ============= 从资产或覆盖读取的属性 | Properties from Asset or Overrides =============
/** 最大粒子数(从资产读取)| Maximum particles (from asset) */
get maxParticles(): number {
return this._loadedAsset?.maxParticles ?? 1000;
}
/** 是否循环 | Whether looping */
get looping(): boolean {
return this._runtimeOverrides.looping ?? this._loadedAsset?.looping ?? true;
}
/** 持续时间(秒)| Duration in seconds */
get duration(): number {
return this._loadedAsset?.duration ?? 5;
}
/** 播放速度 | Playback speed */
get playbackSpeed(): number {
return this._runtimeOverrides.playbackSpeed ?? this._loadedAsset?.playbackSpeed ?? 1;
}
/** 发射速率 | Emission rate */
get emissionRate(): number {
return this._runtimeOverrides.emissionRate ?? this._loadedAsset?.emissionRate ?? 10;
}
/** 混合模式(从资产读取)| Blend mode (from asset) */
get blendMode(): ParticleBlendMode {
return this._loadedAsset?.blendMode ?? ParticleBlendMode.Additive;
}
/** 粒子尺寸(从资产读取)| Particle size (from asset) */
get particleSize(): number {
return this._loadedAsset?.particleSize ?? 8;
}
/** 纹理 GUID从资产读取| Texture GUID (from asset) */
get textureGuid(): string {
return this._loadedAsset?.textureGuid ?? '';
}
/** 爆发列表(从资产读取)| Burst list (from asset) */
get bursts(): IBurstConfig[] {
return this._loadedAsset?.bursts ?? [];
}
// ============= 运行时覆盖方法 | Runtime Override Methods =============
/**
* 设置单个运行时覆盖参数
* Set a single runtime override parameter
*
* @param key 参数名 | Parameter name
* @param value 参数值 | Parameter value
*/
setOverride<K extends keyof ParticleRuntimeOverrides>(key: K, value: ParticleRuntimeOverrides[K]): void {
this._runtimeOverrides[key] = value;
this._needsRebuild = true;
}
/**
* 批量设置运行时覆盖参数
* Set multiple runtime override parameters
*
* @param overrides 覆盖配置 | Override configuration
*/
setOverrides(overrides: Partial<ParticleRuntimeOverrides>): void {
Object.assign(this._runtimeOverrides, overrides);
this._needsRebuild = true;
}
/**
* 清除所有运行时覆盖,恢复资产原始值
* Clear all runtime overrides, restore asset original values
*/
clearOverrides(): void {
this._runtimeOverrides = {};
this._needsRebuild = true;
}
/**
* 清除指定的运行时覆盖参数
* Clear specific runtime override parameter
*
* @param key 参数名 | Parameter name
*/
clearOverride<K extends keyof ParticleRuntimeOverrides>(key: K): void {
delete this._runtimeOverrides[key];
this._needsRebuild = true;
}
// ============= 生命周期方法 | Lifecycle Methods =============
/**
* 初始化粒子系统
* Initialize particle system
*/
initialize(): void {
this._rebuildIfNeeded();
// 自动播放 | Auto play
if (this.autoPlay && !this._isPlaying) {
this.play();
}
}
/**
* 加载粒子资产
* Load particle asset
*
* @deprecated 建议通过 ParticleUpdateSystem 加载资产,系统会自动处理资产加载和初始化。
* Prefer loading assets through ParticleUpdateSystem, which handles asset loading and initialization automatically.
*
* @param guid - Asset GUID to load | 要加载的资产 GUID
* @param bForceReload - 是否强制重新加载 | Whether to force reload
* @param assetManager - 资产管理器实例(必需)| Asset manager instance (required)
* @returns Promise that resolves when asset is loaded | 资产加载完成时解析的 Promise
*/
async loadAsset(guid: string, bForceReload: boolean = false, assetManager?: IAssetManager): Promise<boolean> {
if (!guid) {
this._loadedAsset = null;
this._lastLoadedGuid = '';
this._needsRebuild = true;
return true;
}
// 如果是同一个资产且不强制重新加载,不需要重新加载
// If same asset and not force reload, no need to reload
if (guid === this._lastLoadedGuid && this._loadedAsset && !bForceReload) {
return true;
}
if (!assetManager) {
console.error('[ParticleSystem] assetManager is required for loadAsset. Use ParticleUpdateSystem for automatic asset loading.');
return false;
}
try {
const result = await assetManager.loadAsset<IParticleAsset>(guid, { forceReload: bForceReload });
const asset = result?.asset;
if (asset) {
this._loadedAsset = asset;
this._lastLoadedGuid = guid;
this._needsRebuild = true;
// 应用资产的排序属性 | Apply sorting properties from asset
if (asset.sortingLayer) {
this.sortingLayer = asset.sortingLayer;
}
if (asset.orderInLayer !== undefined) {
this.orderInLayer = asset.orderInLayer;
}
return true;
} else {
console.warn(`[ParticleSystem] Failed to load asset: ${guid}`);
return false;
}
} catch (error) {
console.error(`[ParticleSystem] Error loading asset ${guid}:`, error);
return false;
}
}
/**
* 强制重新加载资产
* Force reload the asset
*
* @deprecated 建议通过 ParticleUpdateSystem 加载资产。
* Prefer loading assets through ParticleUpdateSystem.
*
* 当资产文件内容变化时调用此方法,强制从文件系统重新加载。
* Call this method when asset file content changes, forcing a reload from filesystem.
*
* @param assetManager - 资产管理器实例(必需)| Asset manager instance (required)
*/
async reloadAsset(assetManager?: IAssetManager): Promise<boolean> {
if (!this.particleAssetGuid) return false;
return this.loadAsset(this.particleAssetGuid, true, assetManager);
}
/**
* 设置资产数据(由加载器调用)
* Set asset data (called by loader)
*
* @param asset 粒子资产数据 | Particle asset data
*/
setAssetData(asset: IParticleAsset | null): void {
this._loadedAsset = asset;
this._needsRebuild = true;
}
// ============= 播放控制 | Playback Control =============
/**
* 播放粒子系统
* Play the particle system
*/
play(): void {
this._rebuildIfNeeded();
if (this._emitter) {
this._emitter.isEmitting = true;
}
this._isPlaying = true;
this._elapsedTime = 0;
// 重置爆发状态 | Reset burst states
this._burstStates = this.bursts.map(() => ({ firedCount: 0, lastFireTime: -Infinity }));
}
/**
* 暂停粒子系统
* Pause the particle system
*/
pause(): void {
if (this._emitter) {
this._emitter.isEmitting = false;
}
this._isPlaying = false;
}
/**
* 停止粒子系统
* Stop the particle system
*
* @param clear 是否立即清除所有粒子 | Whether to immediately clear all particles
*/
stop(clear: boolean = false): void {
if (this._emitter) {
this._emitter.isEmitting = false;
}
this._isPlaying = false;
this._elapsedTime = 0;
if (clear && this._pool) {
this._pool.recycleAll();
}
// 重置爆发状态 | Reset burst states
this._burstStates = [];
}
/**
* 清除所有粒子
* Clear all particles
*/
clear(): void {
this._pool?.recycleAll();
}
/**
* 触发一次爆发
* Trigger a burst emission
*
* @param count 发射数量 | Number of particles to emit
*/
emit(count: number): void {
if (this._pool && this._emitter) {
this._emitter.burst(this._pool, count, this._lastEmitterX, this._lastEmitterY);
}
}
/**
* 更新粒子系统
* 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
* @param worldRotation - World rotation in radians | 世界旋转(弧度)
* @param worldScaleX - World scale X | 世界缩放X
* @param worldScaleY - World scale Y | 世界缩放Y
*/
update(
dt: number,
worldX: number = 0,
worldY: number = 0,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): void {
if (!this._isPlaying || !this._pool || !this._emitter) return;
const scaledDt = dt * this.playbackSpeed;
this._simulate(scaledDt, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
this._elapsedTime += scaledDt;
// 检查持续时间 | Check duration
if (!this.looping && this._elapsedTime >= this.duration) {
this._emitter.isEmitting = false;
if (this._pool.activeCount === 0) {
this._isPlaying = false;
}
}
}
// ============= 模块管理 | Module Management =============
/**
* 添加模块
* 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;
}
// ============= 重建与标记 | Rebuild and Marking =============
/**
* 标记需要重建
* Mark for rebuild
*/
markDirty(): void {
this._needsRebuild = true;
}
/**
* 检查并重建粒子系统(如果需要)
* Check and rebuild particle system if needed
*
* This method is called by ParticleUpdateSystem to ensure the particle system
* is built even when not playing. This allows property changes to take effect
* immediately in the editor.
*
* 此方法由 ParticleUpdateSystem 调用,确保即使未播放时也能重建粒子系统。
* 这使得编辑器中的属性更改能够立即生效。
*/
ensureBuilt(): void {
this._rebuildIfNeeded();
}
private _rebuildIfNeeded(): void {
if (!this._needsRebuild && this._pool && this._emitter) return;
// 必须有加载的资产才能构建
// Must have loaded asset to build
const asset = this._loadedAsset;
if (!asset) {
// 没有资产时使用默认值创建最小系统
// Create minimal system with defaults when no asset
if (!this._pool) {
this._pool = new ParticlePool(100);
}
if (!this._emitter) {
this._emitter = new ParticleEmitter(createDefaultEmitterConfig());
}
this._needsRebuild = false;
return;
}
// 应用运行时覆盖 | Apply runtime overrides
const overrides = this._runtimeOverrides;
const scaleMultiplier = overrides.scaleMultiplier ?? 1;
const speedMultiplier = overrides.speedMultiplier ?? 1;
const maxParticles = asset.maxParticles;
const emissionRate = overrides.emissionRate ?? asset.emissionRate;
const emissionShape = asset.emissionShape;
const shapeRadius = asset.shapeRadius;
const shapeWidth = asset.shapeWidth;
const shapeHeight = asset.shapeHeight;
const lifetimeMin = asset.lifetimeMin;
const lifetimeMax = asset.lifetimeMax;
const speedMin = (asset.speedMin ?? 50) * speedMultiplier;
const speedMax = (asset.speedMax ?? 100) * speedMultiplier;
const direction = asset.direction ?? 90;
const directionSpread = asset.directionSpread ?? 0;
const scaleMin = (asset.scaleMin ?? 1) * scaleMultiplier;
const scaleMax = (asset.scaleMax ?? 1) * scaleMultiplier;
const gravityX = overrides.gravityX ?? asset.gravityX ?? 0;
const gravityY = overrides.gravityY ?? asset.gravityY ?? 0;
const startAlpha = asset.startAlpha ?? 1;
const endAlpha = asset.endAlpha ?? 0;
const endScale = asset.endScale ?? 0;
// 解析颜色 | Parse color
let color: { r: number; g: number; b: number };
if (overrides.startColor) {
color = { r: overrides.startColor.r, g: overrides.startColor.g, b: overrides.startColor.b };
} else if (asset.startColor) {
color = { r: asset.startColor.r, g: asset.startColor.g, b: asset.startColor.b };
} else {
color = { r: 1, g: 1, b: 1 };
}
// 创建/调整粒子池 | Create/resize particle pool
if (!this._pool) {
this._pool = new ParticlePool(maxParticles);
} else if (this._pool.capacity !== maxParticles) {
this._pool.resize(maxParticles);
}
// 创建发射器配置 | Create emitter config
const directionRad = (direction - 90) * Math.PI / 180;
const config: EmitterConfig = {
...createDefaultEmitterConfig(),
emissionRate,
burstCount: 0,
lifetime: { min: lifetimeMin, max: lifetimeMax },
shape: emissionShape,
shapeRadius,
shapeWidth,
shapeHeight,
coneAngle: Math.PI / 6,
direction: directionRad,
directionSpread: directionSpread * Math.PI / 180,
speed: { min: speedMin, max: speedMax },
angularVelocity: { min: 0, max: 0 },
startScale: { min: scaleMin, max: scaleMax },
startRotation: { min: 0, max: 0 },
startColor: { ...color, a: startAlpha },
startColorVariance: { r: 0, g: 0, b: 0, a: 0 },
gravityX,
gravityY: -gravityY
};
if (!this._emitter) {
this._emitter = new ParticleEmitter(config);
} else {
this._emitter.config = config;
}
// 重建模块列表 | Rebuild modules list
// 每次重建时清空并重新创建,确保配置同步
// Clear and recreate on each rebuild to ensure config is in sync
this._modules = [];
// 颜色模块(淡出)| 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: endAlpha }
];
this._modules.push(colorModule);
// 缩放模块 | Size module
const sizeModule = new SizeOverLifetimeModule();
sizeModule.startMultiplier = 1;
sizeModule.endMultiplier = endScale;
this._modules.push(sizeModule);
// 从资产配置创建模块 | Create modules from asset configuration
this._createModulesFromAsset(asset);
this._needsRebuild = false;
}
/**
* 从资产配置创建模块实例
* Create module instances from asset configuration
*/
private _createModulesFromAsset(asset: IParticleAsset): void {
if (!asset.modules || asset.modules.length === 0) return;
for (const moduleConfig of asset.modules) {
if (!moduleConfig.enabled) continue;
switch (moduleConfig.type) {
case 'Collision': {
const collisionModule = new CollisionModule();
const params = moduleConfig.params;
if (params.boundaryType !== undefined) {
collisionModule.boundaryType = params.boundaryType as BoundaryType;
}
if (params.behavior !== undefined) {
collisionModule.behavior = params.behavior as CollisionBehavior;
}
if (params.left !== undefined) collisionModule.left = params.left as number;
if (params.right !== undefined) collisionModule.right = params.right as number;
if (params.top !== undefined) collisionModule.top = params.top as number;
if (params.bottom !== undefined) collisionModule.bottom = params.bottom as number;
if (params.radius !== undefined) collisionModule.radius = params.radius as number;
if (params.bounceFactor !== undefined) collisionModule.bounceFactor = params.bounceFactor as number;
if (params.lifeLossOnBounce !== undefined) collisionModule.lifeLossOnBounce = params.lifeLossOnBounce as number;
if (params.minVelocityThreshold !== undefined) collisionModule.minVelocityThreshold = params.minVelocityThreshold as number;
this._modules.push(collisionModule);
break;
}
case 'ForceField': {
const forceModule = new ForceFieldModule();
const params = moduleConfig.params;
// ForceFieldModule 使用 forceFields 数组 | ForceFieldModule uses forceFields array
const field: ForceField = {
type: (params.type as ForceFieldType) ?? ForceFieldType.Wind,
enabled: true,
strength: (params.strength as number) ?? 100,
directionX: params.directionX as number | undefined,
directionY: params.directionY as number | undefined,
centerX: params.centerX as number | undefined,
centerY: params.centerY as number | undefined,
inwardStrength: params.inwardStrength as number | undefined,
frequency: params.frequency as number | undefined,
amplitude: params.amplitude as number | undefined,
};
forceModule.forceFields.push(field);
this._modules.push(forceModule);
break;
}
case 'TextureSheetAnimation': {
// 纹理图集动画模块 | Texture sheet animation module
const textureModule = new TextureSheetAnimationModule();
// moduleConfig 直接包含属性(非 params 嵌套)
// moduleConfig contains properties directly (not nested in params)
const cfg = moduleConfig as unknown as Record<string, unknown>;
textureModule.enabled = true;
if (cfg.tilesX !== undefined) textureModule.tilesX = cfg.tilesX as number;
if (cfg.tilesY !== undefined) textureModule.tilesY = cfg.tilesY as number;
if (cfg.totalFrames !== undefined) textureModule.totalFrames = cfg.totalFrames as number;
if (cfg.startFrame !== undefined) textureModule.startFrame = cfg.startFrame as number;
if (cfg.frameRate !== undefined) textureModule.frameRate = cfg.frameRate as number;
if (cfg.speedMultiplier !== undefined) textureModule.speedMultiplier = cfg.speedMultiplier as number;
if (cfg.cycleCount !== undefined) textureModule.cycleCount = cfg.cycleCount as number;
// 播放模式 | Play mode
if (cfg.playMode !== undefined) {
const playModeMap: Record<string, AnimationPlayMode> = {
'lifetimeLoop': AnimationPlayMode.LifetimeLoop,
'fixedFps': AnimationPlayMode.FixedFPS,
'random': AnimationPlayMode.Random,
'speedBased': AnimationPlayMode.SpeedBased,
};
textureModule.playMode = playModeMap[cfg.playMode as string] ?? AnimationPlayMode.LifetimeLoop;
}
// 循环模式 | Loop mode
if (cfg.loopMode !== undefined) {
const loopModeMap: Record<string, AnimationLoopMode> = {
'once': AnimationLoopMode.Once,
'loop': AnimationLoopMode.Loop,
'pingPong': AnimationLoopMode.PingPong,
};
textureModule.loopMode = loopModeMap[cfg.loopMode as string] ?? AnimationLoopMode.Once;
}
this._modules.push(textureModule);
break;
}
// 可扩展其他模块类型 | Extensible for other module types
default:
console.warn(`[ParticleSystem] Unknown module type: ${moduleConfig.type}`);
}
}
}
private _simulate(
dt: number,
worldX: number,
worldY: number,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): 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;
this._lastEmitterX = worldX;
this._lastEmitterY = worldY;
// 保存当前的变换参数,供渲染使用 | Save current transform params for rendering
this._worldRotation = worldRotation;
this._worldScaleX = worldScaleX;
this._worldScaleY = worldScaleY;
// 发射新粒子(应用旋转到发射方向)| Emit new particles (apply rotation to emission direction)
this._emitter.emit(this._pool, dt, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
// 处理爆发 | Process bursts
this._processBursts(worldX, worldY, worldRotation, worldScaleX, worldScaleY);
// 更新现有粒子 | Update existing particles
const particles = this._pool.particles;
const particlesToRecycle: Particle[] = [];
for (const p of particles) {
if (!p.alive) continue;
p.age += dt;
if (p.age >= p.lifetime) {
particlesToRecycle.push(p);
continue;
}
// 应用重力 | Apply gravity
const config = this._emitter.config;
p.vx += config.gravityX * dt;
p.vy += config.gravityY * dt;
// 更新位置 | Update position
p.x += p.vx * dt;
p.y += p.vy * dt;
// 本地空间:粒子跟随发射器 | Local space: particles follow emitter
if (isLocalSpace) {
p.x += emitterDeltaX;
p.y += emitterDeltaY;
}
// 更新旋转 | Update rotation
p.rotation += p.angularVelocity * dt;
// 应用模块 | Apply modules
const normalizedAge = p.age / p.lifetime;
for (const module of this._modules) {
if (module.enabled) {
// 更新模块的发射器位置 | Update module emitter position
if (module instanceof CollisionModule) {
module.emitterX = worldX;
module.emitterY = worldY;
}
if (module instanceof ForceFieldModule) {
module.emitterX = worldX;
module.emitterY = worldY;
}
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();
}
}
}
// 回收已过期的粒子 | Recycle expired particles
for (const p of particlesToRecycle) {
this._pool.recycle(p);
}
}
private _processBursts(
worldX: number,
worldY: number,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): void {
const bursts = this.bursts;
if (!bursts || bursts.length === 0 || !this._pool || !this._emitter) return;
// 初始化爆发状态 | Initialize burst states
while (this._burstStates.length < bursts.length) {
this._burstStates.push({ firedCount: 0, lastFireTime: -Infinity });
}
for (let i = 0; i < bursts.length; i++) {
const burst = bursts[i];
const state = this._burstStates[i];
// 检查是否达到触发时间 | Check if trigger time reached
if (this._elapsedTime >= burst.time) {
// 检查循环次数 | Check cycle count
const maxCycles = burst.cycles === 0 ? Infinity : burst.cycles;
if (state.firedCount >= maxCycles) continue;
// 检查间隔 | Check interval
const timeSinceLastFire = this._elapsedTime - state.lastFireTime;
const interval = state.firedCount === 0 ? 0 : burst.interval;
if (timeSinceLastFire >= interval) {
this._emitter.burst(this._pool, burst.count, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
state.firedCount++;
state.lastFireTime = this._elapsedTime;
}
}
}
}
// ============= 清理 | Cleanup =============
/**
* 重置粒子系统到初始状态
* Reset particle system to initial state
*/
resetSystem(): void {
this.stop(true);
this._pool = null;
this._emitter = null;
this._modules = [];
this._needsRebuild = true;
this._loadedAsset = null;
this._lastLoadedGuid = '';
this._runtimeOverrides = {};
this.textureId = 0;
}
/**
* 组件从实体移除时的回调
* Called when component is removed from entity
*/
override onRemovedFromEntity(): void {
this.resetSystem();
}
}