fix(particle): 修复粒子系统在浏览器预览中的资产加载和渲染 (#290)

* fix(editor): 修复粒子实体创建和优化检视器

- 添加 effects 分类到右键菜单,修复粒子实体无法创建的问题
- 添加粒子效果的本地化标签
- 简化粒子组件检视器,优先显示资产文件选择
- 高级属性只在未选择资产时显示,且默认折叠
- 添加可折叠的属性分组提升用户体验

* fix(particle): 修复粒子系统在浏览器预览中的资产加载和渲染

- 添加粒子 Gizmo 支持,显示发射形状并响应 Transform 缩放/旋转
- 修复资产热重载:添加 reloadAsset() 方法和 assets:refresh 事件监听
- 修复 VectorFieldEditors 数值输入精度(step 改为 0.01)
- 修复浏览器预览中粒子资产加载失败的问题:
  - 将相对路径转换为绝对路径以正确复制资产文件
  - 使用原始 GUID 而非生成的 GUID 构建 asset catalog
  - 初始化全局 assetManager 单例的 catalog 和 loader
  - 在 GameRuntime 的 systemContext 中添加 engineIntegration
- 公开 AssetManager.initializeFromCatalog 方法供运行时使用
This commit is contained in:
YHH
2025-12-07 01:00:35 +08:00
committed by GitHub
parent 1fb702169e
commit 568b327425
22 changed files with 1628 additions and 782 deletions

View File

@@ -27,8 +27,10 @@
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/asset-system": "workspace:*"
},
"devDependencies": {
"@esengine/asset-system": "workspace:*",
"@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-framework-math": "workspace:*",
"@esengine/engine-core": "workspace:*",

View File

@@ -159,9 +159,20 @@ export class ParticleEmitter {
* @param dt - Delta time in seconds
* @param worldX - World position X
* @param worldY - World position Y
* @param worldRotation - World rotation in radians (applied to emission direction)
* @param worldScaleX - World scale X (applied to emission offset and speed)
* @param worldScaleY - World scale Y (applied to emission offset and speed)
* @returns Number of particles emitted
*/
emit(pool: ParticlePool, dt: number, worldX: number, worldY: number): number {
emit(
pool: ParticlePool,
dt: number,
worldX: number,
worldY: number,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): number {
if (!this._isEmitting) return 0;
let emitted = 0;
@@ -171,7 +182,7 @@ export class ParticleEmitter {
for (let i = 0; i < this.config.burstCount; i++) {
const p = pool.spawn();
if (p) {
this._initParticle(p, worldX, worldY);
this._initParticle(p, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
emitted++;
}
}
@@ -182,7 +193,7 @@ export class ParticleEmitter {
while (this._emissionAccumulator >= 1) {
const p = pool.spawn();
if (p) {
this._initParticle(p, worldX, worldY);
this._initParticle(p, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
emitted++;
}
this._emissionAccumulator -= 1;
@@ -195,13 +206,29 @@ export class ParticleEmitter {
/**
* 立即爆发发射
* Burst emit immediately
*
* @param pool - Particle pool
* @param count - Number of particles to emit
* @param worldX - World position X
* @param worldY - World position Y
* @param worldRotation - World rotation in radians
* @param worldScaleX - World scale X
* @param worldScaleY - World scale Y
*/
burst(pool: ParticlePool, count: number, worldX: number, worldY: number): number {
burst(
pool: ParticlePool,
count: number,
worldX: number,
worldY: number,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): number {
let emitted = 0;
for (let i = 0; i < count; i++) {
const p = pool.spawn();
if (p) {
this._initParticle(p, worldX, worldY);
this._initParticle(p, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
emitted++;
}
}
@@ -217,23 +244,45 @@ export class ParticleEmitter {
this._isEmitting = true;
}
private _initParticle(p: Particle, worldX: number, worldY: number): void {
private _initParticle(
p: Particle,
worldX: number,
worldY: number,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): void {
const config = this.config;
// 位置 | Position
// 获取形状偏移 | Get shape offset
const [ox, oy] = this._getShapeOffset();
p.x = worldX + ox;
p.y = worldY + oy;
// 应用旋转和缩放到发射偏移 | Apply rotation and scale to emission offset
// 先缩放,再旋转 | Scale first, then rotate
const scaledOx = ox * worldScaleX;
const scaledOy = oy * worldScaleY;
const cos = Math.cos(worldRotation);
const sin = Math.sin(worldRotation);
const rotatedOx = scaledOx * cos - scaledOy * sin;
const rotatedOy = scaledOx * sin + scaledOy * cos;
// 位置 | Position
p.x = worldX + rotatedOx;
p.y = worldY + rotatedOy;
// 生命时间 | Lifetime
p.lifetime = randomRange(config.lifetime.min, config.lifetime.max);
p.age = 0;
// 速度方向 | Velocity direction
const dir = config.direction + randomRange(-config.directionSpread / 2, config.directionSpread / 2);
// 速度方向(应用世界旋转)| Velocity direction (apply world rotation)
const baseDir = config.direction + randomRange(-config.directionSpread / 2, config.directionSpread / 2);
const dir = baseDir + worldRotation;
const speed = randomRange(config.speed.min, config.speed.max);
p.vx = Math.cos(dir) * speed;
p.vy = Math.sin(dir) * speed;
// 速度也应用缩放(使用平均缩放)| Speed also applies scale (use average scale)
const avgScale = (worldScaleX + worldScaleY) / 2;
p.vx = Math.cos(dir) * speed * avgScale;
p.vy = Math.sin(dir) * speed * avgScale;
// 加速度(重力)| Acceleration (gravity)
p.ax = config.gravityX;
@@ -243,12 +292,12 @@ export class ParticleEmitter {
p.rotation = randomRange(config.startRotation.min, config.startRotation.max);
p.angularVelocity = randomRange(config.angularVelocity.min, config.angularVelocity.max);
// 缩放 | Scale
const scale = randomRange(config.startScale.min, config.startScale.max);
p.scaleX = scale;
p.scaleY = scale;
p.startScaleX = scale;
p.startScaleY = scale;
// 缩放(应用世界缩放)| Scale (apply world scale)
const baseScale = randomRange(config.startScale.min, config.startScale.max);
p.scaleX = baseScale * worldScaleX;
p.scaleY = baseScale * worldScaleY;
p.startScaleX = p.scaleX;
p.startScaleY = p.scaleY;
// 颜色 | Color
p.r = clamp(config.startColor.r + randomRange(-config.startColorVariance.r, config.startColorVariance.r), 0, 1);

View File

@@ -1,10 +1,28 @@
import type { ComponentRegistry as ComponentRegistryType, IScene } from '@esengine/ecs-framework';
import type { IRuntimeModule, IPlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { assetManager as globalAssetManager, type AssetManager } from '@esengine/asset-system';
import { ParticleSystemComponent } from './ParticleSystemComponent';
import { ParticleUpdateSystem } from './systems/ParticleSystem';
import { ParticleLoader, ParticleAssetType } from './loaders/ParticleLoader';
export type { SystemContext, ModuleManifest, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader };
/**
* 引擎桥接接口(用于直接加载纹理)
* Engine bridge interface (for direct texture loading)
*/
export interface IEngineBridge {
loadTexture(id: number, url: string): Promise<void>;
}
/**
* 引擎集成接口(用于加载纹理)
* Engine integration interface (for loading textures)
*/
export interface IEngineIntegration {
loadTextureForComponent(texturePath: string): Promise<number>;
}
/**
* 粒子系统上下文
* Particle system context
@@ -18,10 +36,17 @@ export interface ParticleSystemContext extends SystemContext {
addRenderDataProvider(provider: any): void;
removeRenderDataProvider(provider: any): void;
};
/** 引擎集成(用于加载纹理)| Engine integration (for loading textures) */
engineIntegration?: IEngineIntegration;
/** 引擎桥接(用于直接加载纹理)| Engine bridge (for direct texture loading) */
engineBridge?: IEngineBridge;
/** 资产管理器(用于注册加载器)| Asset manager (for registering loaders) */
assetManager?: AssetManager;
}
class ParticleRuntimeModule implements IRuntimeModule {
private _updateSystem: ParticleUpdateSystem | null = null;
private _loaderRegistered = false;
registerComponents(registry: typeof ComponentRegistryType): void {
registry.register(ParticleSystemComponent);
@@ -30,6 +55,24 @@ class ParticleRuntimeModule implements IRuntimeModule {
createSystems(scene: IScene, context: SystemContext): void {
const particleContext = context as ParticleSystemContext;
// 注册粒子资产加载器到上下文的 assetManager 和全局单例
// Register particle asset loader to context assetManager AND global singleton
if (!this._loaderRegistered) {
const loader = new ParticleLoader();
// Register to context's assetManager (used by GameRuntime)
if (particleContext.assetManager) {
particleContext.assetManager.registerLoader(ParticleAssetType as any, loader);
}
// Also register to global singleton (used by ParticleSystemComponent.loadAsset)
// 同时注册到全局单例ParticleSystemComponent.loadAsset 使用的是全局单例)
globalAssetManager.registerLoader(ParticleAssetType as any, loader);
this._loaderRegistered = true;
console.log('[ParticleRuntimeModule] Registered ParticleLoader to both context and global assetManager');
}
this._updateSystem = new ParticleUpdateSystem();
// 设置 Transform 组件类型 | Set Transform component type
@@ -37,9 +80,14 @@ class ParticleRuntimeModule implements IRuntimeModule {
this._updateSystem.setTransformType(particleContext.transformType);
}
// 在编辑器中禁用系统(手动控制)| Disable in editor (manual control)
if (context.isEditor) {
this._updateSystem.enabled = false;
// 设置引擎集成(用于加载纹理)| Set engine integration (for loading textures)
if (particleContext.engineIntegration) {
this._updateSystem.setEngineIntegration(particleContext.engineIntegration);
}
// 设置引擎桥接(用于加载默认纹理)| Set engine bridge (for loading default texture)
if (particleContext.engineBridge) {
this._updateSystem.setEngineBridge(particleContext.engineBridge);
}
scene.addSystem(this._updateSystem);
@@ -70,7 +118,7 @@ const manifest: ModuleManifest = {
category: 'Rendering',
icon: 'Sparkles',
isCore: false,
defaultEnabled: false,
defaultEnabled: true,
isEngineModule: true,
canContainContent: true,
dependencies: ['core', 'math', 'sprite'],

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,21 @@ export interface IParticleModuleConfig {
params: Record<string, unknown>;
}
/**
* 爆发配置
* Burst configuration
*/
export interface IBurstConfig {
/** 触发时间(秒)| Trigger time (seconds) */
time: number;
/** 发射数量 | Particle count */
count: number;
/** 循环次数0=无限)| Number of cycles (0=infinite) */
cycles: number;
/** 循环间隔(秒)| Interval between cycles (seconds) */
interval: number;
}
/**
* 粒子效果资源数据接口
* Particle effect asset data interface
@@ -110,11 +125,17 @@ export interface IParticleAsset {
sortingOrder: number;
/** 纹理资产 GUID | Texture asset GUID */
textureGuid?: string;
/** 纹理路径(编辑器兼容)| Texture path (editor compatibility) */
texturePath?: string;
// 模块配置 | Module configurations
/** 模块列表 | Module list */
modules?: IParticleModuleConfig[];
// 爆发配置 | Burst configurations
/** 爆发列表 | Burst list */
bursts?: IBurstConfig[];
// 纹理动画(可选)| Texture animation (optional)
/** 纹理图集列数 | Texture sheet columns */
textureTilesX?: number;

View File

@@ -3,5 +3,6 @@ export {
ParticleAssetType,
createDefaultParticleAsset,
type IParticleAsset,
type IParticleModuleConfig
type IParticleModuleConfig,
type IBurstConfig
} from './ParticleLoader';

View File

@@ -106,6 +106,7 @@ export class ParticleRenderDataProvider implements IRenderDataProvider {
}
}
if (totalParticles === 0) return;
// 确保缓冲区足够大 | Ensure buffers are large enough
@@ -183,6 +184,11 @@ export class ParticleRenderDataProvider implements IRenderDataProvider {
}
if (particleIndex > 0) {
// 获取纹理路径(支持多种来源)| Get texture path (support multiple sources)
const firstComponent = systems[0]?.component;
const asset = firstComponent?.loadedAsset as { textureGuid?: string; texturePath?: string } | null;
const texPath = asset?.textureGuid || asset?.texturePath || firstComponent?.textureGuid || undefined;
// 创建当前组的渲染数据 | Create render data for current group
const renderData: ParticleProviderRenderData = {
transforms: this._transforms.subarray(0, particleIndex * 7),
@@ -191,7 +197,7 @@ export class ParticleRenderDataProvider implements IRenderDataProvider {
colors: this._colors.subarray(0, particleIndex),
tileCount: particleIndex,
sortingOrder,
texturePath: systems[0]?.component.textureGuid || undefined
texturePath: texPath
};
this._renderDataCache.push(renderData);

View File

@@ -1,6 +1,43 @@
import { EntitySystem, Matcher, ECSSystem, Time, Entity } from '@esengine/ecs-framework';
import { ParticleSystemComponent } from '../ParticleSystemComponent';
import { ParticleRenderDataProvider } from '../rendering/ParticleRenderDataProvider';
import type { IEngineIntegration, IEngineBridge } from '../ParticleRuntimeModule';
/**
* 默认粒子纹理 ID
* Default particle texture ID
*/
const DEFAULT_PARTICLE_TEXTURE_ID = 99999;
/**
* 生成默认粒子纹理的 Data URL渐变圆形
* Generate default particle texture Data URL (gradient circle)
*/
function generateDefaultParticleTextureDataURL(): string {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) return '';
// 创建径向渐变 | Create radial gradient
const gradient = ctx.createRadialGradient(
size / 2, size / 2, 0,
size / 2, size / 2, size / 2
);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.4, 'rgba(255, 255, 255, 0.8)');
gradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.3)');
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.fill();
return canvas.toDataURL('image/png');
}
/**
* Transform 组件接口(避免直接依赖 engine-core
@@ -9,6 +46,14 @@ import { ParticleRenderDataProvider } from '../rendering/ParticleRenderDataProvi
interface ITransformComponent {
worldPosition?: { x: number; y: number; z: number };
position: { x: number; y: number; z: number };
/** 世界旋转Vector3z 分量为 2D 旋转弧度)| World rotation (Vector3, z component is 2D rotation in radians) */
worldRotation?: { x: number; y: number; z: number };
/** 本地旋转Vector3| Local rotation (Vector3) */
rotation?: { x: number; y: number; z: number };
/** 世界缩放 | World scale */
worldScale?: { x: number; y: number; z: number };
/** 本地缩放 | Local scale */
scale?: { x: number; y: number; z: number };
}
/**
@@ -22,6 +67,14 @@ interface ITransformComponent {
export class ParticleUpdateSystem extends EntitySystem {
private _transformType: (new (...args: any[]) => ITransformComponent) | null = null;
private _renderDataProvider: ParticleRenderDataProvider;
private _engineIntegration: IEngineIntegration | null = null;
private _engineBridge: IEngineBridge | null = null;
private _defaultTextureLoaded: boolean = false;
private _defaultTextureLoading: boolean = false;
/** 追踪每个粒子组件上次加载的资产 GUID | Track last loaded asset GUID for each particle component */
private _lastLoadedGuids: WeakMap<ParticleSystemComponent, string> = new WeakMap();
/** 正在加载资产的粒子组件 | Particle components currently loading assets */
private _loadingComponents: WeakSet<ParticleSystemComponent> = new WeakSet();
constructor() {
super(Matcher.empty().all(ParticleSystemComponent));
@@ -38,6 +91,22 @@ export class ParticleUpdateSystem extends EntitySystem {
this._transformType = transformType;
}
/**
* 设置引擎集成(用于加载纹理)
* Set engine integration (for loading textures)
*/
setEngineIntegration(integration: IEngineIntegration): void {
this._engineIntegration = integration;
}
/**
* 设置引擎桥接(用于加载默认纹理)
* Set engine bridge (for loading default texture)
*/
setEngineBridge(bridge: IEngineBridge): void {
this._engineBridge = bridge;
}
/**
* 获取渲染数据提供者
* Get render data provider
@@ -57,26 +126,61 @@ export class ParticleUpdateSystem extends EntitySystem {
let worldX = 0;
let worldY = 0;
let worldRotation = 0;
let worldScaleX = 1;
let worldScaleY = 1;
let transform: ITransformComponent | null = null;
// 获取 Transform 位置 | Get Transform position
// 获取 Transform 位置、旋转、缩放 | Get Transform position, rotation, scale
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;
// 获取旋转2D 使用 z 分量)| Get rotation (2D uses z component)
const rot = transform.worldRotation ?? transform.rotation;
if (rot) {
worldRotation = rot.z;
}
// 获取缩放 | Get scale
const scale = transform.worldScale ?? transform.scale;
if (scale) {
worldScaleX = scale.x;
worldScaleY = scale.y;
}
}
}
// 检测资产 GUID 变化并重新加载 | Detect asset GUID change and reload
// 这使得编辑器中选择新的粒子资产时能够立即切换
// This allows immediate switching when selecting a new particle asset in the editor
this._checkAndReloadAsset(particle);
// 确保粒子系统已构建(即使未播放)| Ensure particle system is built (even when not playing)
// 这使得编辑器中的属性更改能够立即生效
// This allows property changes to take effect immediately in the editor
particle.ensureBuilt();
// 更新粒子系统 | Update particle system
if (particle.isPlaying) {
particle.update(deltaTime, worldX, worldY);
particle.update(deltaTime, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
}
// 尝试加载纹理(如果还没有加载)| Try to load texture if not loaded yet
if (particle.textureId === 0) {
this.loadParticleTexture(particle);
}
// 更新渲染数据提供者的 Transform 引用 | Update render data provider's Transform reference
// 确保粒子系统始终被注册 | Ensure particle system is always registered
if (transform) {
this._renderDataProvider.register(particle, transform);
} else {
// 使用默认 Transform | Use default transform
this._renderDataProvider.register(particle, { position: { x: worldX, y: worldY } });
}
}
@@ -87,18 +191,176 @@ export class ParticleUpdateSystem extends EntitySystem {
protected override onAdded(entity: Entity): void {
const particle = entity.getComponent(ParticleSystemComponent) as ParticleSystemComponent | null;
if (particle) {
particle.initialize();
// 异步初始化粒子系统 | Async initialize particle system
this._initializeParticle(entity, particle);
}
}
// 注册到渲染数据提供者 | 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);
}
/**
* 异步初始化粒子系统
* Async initialize particle system
*/
private async _initializeParticle(entity: Entity, particle: ParticleSystemComponent): Promise<void> {
// 如果有资产 GUID先加载资产 | Load asset first if GUID is set
if (particle.particleAssetGuid) {
await particle.loadAsset(particle.particleAssetGuid);
}
// 初始化粒子系统(不自动播放,由下面的逻辑控制)
// Initialize particle system (don't auto play, controlled by logic below)
particle.ensureBuilt();
// 加载纹理 | Load texture
await this.loadParticleTexture(particle);
// 注册到渲染数据提供者 | Register to render data provider
// 尝试获取 Transform如果没有则使用默认位置 | Try to get Transform, use default position if not available
let transform: ITransformComponent | null = null;
if (this._transformType) {
transform = entity.getComponent(this._transformType as any) as ITransformComponent | null;
}
// 即使没有 Transform也要注册粒子系统使用原点位置 | Register particle system even without Transform (use origin position)
if (transform) {
this._renderDataProvider.register(particle, transform);
} else {
this._renderDataProvider.register(particle, { position: { x: 0, y: 0 } });
}
// 记录已加载的资产 GUID | Record loaded asset GUID
this._lastLoadedGuids.set(particle, particle.particleAssetGuid);
// 决定是否自动播放 | Decide whether to auto play
// 编辑器模式:有资产时自动播放预览 | Editor mode: auto play preview if has asset
// 运行时模式:根据 autoPlay 设置 | Runtime mode: based on autoPlay setting
const isEditorMode = this.scene?.isEditorMode ?? false;
if (particle.particleAssetGuid && particle.loadedAsset) {
if (isEditorMode) {
// 编辑器模式:始终播放预览 | Editor mode: always play preview
particle.play();
} else if (particle.autoPlay) {
// 运行时模式:根据 autoPlay 设置 | Runtime mode: based on autoPlay
particle.play();
}
}
}
/**
* 检测资产 GUID 变化并重新加载
* Check for asset GUID change and reload if necessary
*
* 当编辑器中修改 particleAssetGuid 属性时,此方法会检测变化并触发重新加载。
* 加载完成后会自动开始播放预览,让用户立即看到效果。
*
* When particleAssetGuid property is modified in editor, this method detects the change and triggers reload.
* After loading, it automatically starts playback for preview so user can see the effect immediately.
*/
private _checkAndReloadAsset(particle: ParticleSystemComponent): void {
const currentGuid = particle.particleAssetGuid;
const lastGuid = this._lastLoadedGuids.get(particle);
// 如果 GUID 没有变化,或者正在加载中,跳过
// Skip if GUID hasn't changed or already loading
if (currentGuid === lastGuid || this._loadingComponents.has(particle)) {
return;
}
// 标记为正在加载 | Mark as loading
this._loadingComponents.add(particle);
this._lastLoadedGuids.set(particle, currentGuid);
// 停止当前播放并清除粒子 | Stop current playback and clear particles
particle.stop(true);
// 重置纹理 ID以便重新加载纹理 | Reset texture ID for texture reload
particle.textureId = 0;
// 异步加载新资产 | Async load new asset
(async () => {
try {
if (currentGuid) {
await particle.loadAsset(currentGuid);
// 加载纹理 | Load texture
await this.loadParticleTexture(particle);
// 标记需要重建 | Mark for rebuild
particle.markDirty();
// 在编辑器中自动播放预览,让用户立即看到效果
// Auto play preview in editor so user can see the effect immediately
particle.play();
console.log(`[ParticleUpdateSystem] Asset loaded and playing: ${currentGuid}`);
} else {
// 清空资产时,设置为 null | Clear asset when GUID is empty
particle.setAssetData(null);
particle.markDirty();
console.log(`[ParticleUpdateSystem] Asset cleared`);
}
} catch (error) {
console.error('[ParticleUpdateSystem] Failed to reload asset:', error);
} finally {
// 取消加载标记 | Remove loading mark
this._loadingComponents.delete(particle);
}
})();
}
/**
* 加载粒子纹理
* Load particle texture
*/
async loadParticleTexture(particle: ParticleSystemComponent): Promise<void> {
if (!this._engineIntegration) {
return;
}
// 已经加载过就跳过 | Skip if already loaded
if (particle.textureId > 0) return;
// 从已加载的资产获取纹理路径 | Get texture path from loaded asset
// 支持 textureGuid 和 texturePath编辑器可能使用后者
// Support both textureGuid and texturePath (editor may use the latter)
const asset = particle.loadedAsset;
const texturePath = asset?.textureGuid || asset?.texturePath || particle.textureGuid;
if (texturePath) {
try {
const textureId = await this._engineIntegration.loadTextureForComponent(texturePath);
particle.textureId = textureId;
} catch (error) {
console.error('[ParticleUpdateSystem] Failed to load texture:', texturePath, error);
// 加载失败时使用默认纹理 | Use default texture on load failure
await this._ensureDefaultTexture();
particle.textureId = DEFAULT_PARTICLE_TEXTURE_ID;
}
} else {
// 没有纹理路径时使用默认粒子纹理 | Use default particle texture when no path
await this._ensureDefaultTexture();
particle.textureId = DEFAULT_PARTICLE_TEXTURE_ID;
}
}
/**
* 确保默认粒子纹理已加载
* Ensure default particle texture is loaded
*/
private async _ensureDefaultTexture(): Promise<void> {
if (this._defaultTextureLoaded || this._defaultTextureLoading) return;
if (!this._engineBridge) return;
this._defaultTextureLoading = true;
try {
const dataUrl = generateDefaultParticleTextureDataURL();
if (dataUrl) {
await this._engineBridge.loadTexture(DEFAULT_PARTICLE_TEXTURE_ID, dataUrl);
this._defaultTextureLoaded = true;
}
} catch (error) {
console.error('[ParticleUpdateSystem] Failed to create default particle texture:', error);
}
this._defaultTextureLoading = false;
}
protected override onRemoved(entity: Entity): void {
const particle = entity.getComponent(ParticleSystemComponent) as ParticleSystemComponent | null;
if (particle) {