211 lines
5.9 KiB
TypeScript
211 lines
5.9 KiB
TypeScript
|
|
/**
|
|||
|
|
* 点击特效组件 - 在点击位置播放粒子效果
|
|||
|
|
* Click FX Component - Play particle effects at click position
|
|||
|
|
*
|
|||
|
|
* @example
|
|||
|
|
* ```typescript
|
|||
|
|
* // 在编辑器中添加此组件到相机或空实体上
|
|||
|
|
* // Add this component to camera or empty entity in editor
|
|||
|
|
*
|
|||
|
|
* // 配置粒子资产列表,点击时会轮换播放
|
|||
|
|
* // Configure particle asset list, will cycle through on click
|
|||
|
|
* clickFx.particleAssets = ['guid1', 'guid2', 'guid3'];
|
|||
|
|
* ```
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 点击特效触发模式
|
|||
|
|
* Click FX trigger mode
|
|||
|
|
*/
|
|||
|
|
export enum ClickFxTriggerMode {
|
|||
|
|
/** 鼠标左键点击 | Left mouse button click */
|
|||
|
|
LeftClick = 'leftClick',
|
|||
|
|
/** 鼠标右键点击 | Right mouse button click */
|
|||
|
|
RightClick = 'rightClick',
|
|||
|
|
/** 任意鼠标按钮 | Any mouse button */
|
|||
|
|
AnyClick = 'anyClick',
|
|||
|
|
/** 触摸 | Touch */
|
|||
|
|
Touch = 'touch',
|
|||
|
|
/** 鼠标和触摸都响应 | Both mouse and touch */
|
|||
|
|
All = 'all'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 点击特效组件
|
|||
|
|
* Click FX Component
|
|||
|
|
*
|
|||
|
|
* 在用户点击/触摸屏幕时,在点击位置播放粒子效果。
|
|||
|
|
* Plays particle effects at the click/touch position when user interacts.
|
|||
|
|
*/
|
|||
|
|
@ECSComponent('ClickFx')
|
|||
|
|
@Serializable({ version: 1, typeId: 'ClickFx' })
|
|||
|
|
export class ClickFxComponent extends Component {
|
|||
|
|
/**
|
|||
|
|
* 粒子资产 GUID 列表
|
|||
|
|
* List of particle asset GUIDs
|
|||
|
|
*
|
|||
|
|
* 多个资产会轮换播放,实现多样化的点击效果。
|
|||
|
|
* Multiple assets will cycle through for varied click effects.
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({
|
|||
|
|
type: 'array',
|
|||
|
|
label: 'Particle Assets',
|
|||
|
|
itemType: { type: 'asset', extensions: ['.particle', '.particle.json'] },
|
|||
|
|
reorderable: true
|
|||
|
|
})
|
|||
|
|
public particleAssets: string[] = [];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 触发模式
|
|||
|
|
* Trigger mode
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({
|
|||
|
|
type: 'enum',
|
|||
|
|
label: 'Trigger Mode',
|
|||
|
|
options: [
|
|||
|
|
{ label: 'Left Click', value: ClickFxTriggerMode.LeftClick },
|
|||
|
|
{ label: 'Right Click', value: ClickFxTriggerMode.RightClick },
|
|||
|
|
{ label: 'Any Click', value: ClickFxTriggerMode.AnyClick },
|
|||
|
|
{ label: 'Touch', value: ClickFxTriggerMode.Touch },
|
|||
|
|
{ label: 'All', value: ClickFxTriggerMode.All }
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
public triggerMode: ClickFxTriggerMode = ClickFxTriggerMode.All;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否启用
|
|||
|
|
* Whether enabled
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'boolean', label: 'Enabled' })
|
|||
|
|
public fxEnabled: boolean = true;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 最大同时播放数量
|
|||
|
|
* Maximum concurrent effects
|
|||
|
|
*
|
|||
|
|
* 限制同时播放的粒子效果数量,防止性能问题。
|
|||
|
|
* Limits concurrent particle effects to prevent performance issues.
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'integer', label: 'Max Concurrent', min: 1, max: 50 })
|
|||
|
|
public maxConcurrent: number = 10;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 粒子效果生命周期(秒)
|
|||
|
|
* Particle effect lifetime in seconds
|
|||
|
|
*
|
|||
|
|
* 效果播放多长时间后自动销毁。
|
|||
|
|
* How long before the effect is automatically destroyed.
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'number', label: 'Effect Lifetime', min: 0.1, max: 30, step: 0.1 })
|
|||
|
|
public effectLifetime: number = 3;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 位置偏移(相对于点击位置)
|
|||
|
|
* Position offset (relative to click position)
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'vector2', label: 'Position Offset' })
|
|||
|
|
public positionOffset: { x: number; y: number } = { x: 0, y: 0 };
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 缩放
|
|||
|
|
* Scale
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'number', label: 'Scale', min: 0.1, max: 10, step: 0.1 })
|
|||
|
|
public scale: number = 1;
|
|||
|
|
|
|||
|
|
// ============= 运行时状态(不序列化)| Runtime state (not serialized) =============
|
|||
|
|
|
|||
|
|
/** 当前粒子索引 | Current particle index */
|
|||
|
|
private _currentIndex: number = 0;
|
|||
|
|
|
|||
|
|
/** 活跃的特效实体 ID 列表 | Active effect entity IDs */
|
|||
|
|
private _activeEffects: { entityId: number; startTime: number }[] = [];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取下一个要播放的粒子资产 GUID
|
|||
|
|
* Get next particle asset GUID to play
|
|||
|
|
*/
|
|||
|
|
public getNextParticleAsset(): string | null {
|
|||
|
|
if (this.particleAssets.length === 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const guid = this.particleAssets[this._currentIndex];
|
|||
|
|
this._currentIndex = (this._currentIndex + 1) % this.particleAssets.length;
|
|||
|
|
return guid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加活跃特效
|
|||
|
|
* Add active effect
|
|||
|
|
*/
|
|||
|
|
public addActiveEffect(entityId: number): void {
|
|||
|
|
this._activeEffects.push({ entityId, startTime: Date.now() });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取活跃特效列表
|
|||
|
|
* Get active effects list
|
|||
|
|
*/
|
|||
|
|
public getActiveEffects(): ReadonlyArray<{ entityId: number; startTime: number }> {
|
|||
|
|
return this._activeEffects;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 移除活跃特效
|
|||
|
|
* Remove active effect
|
|||
|
|
*/
|
|||
|
|
public removeActiveEffect(entityId: number): void {
|
|||
|
|
const index = this._activeEffects.findIndex(e => e.entityId === entityId);
|
|||
|
|
if (index !== -1) {
|
|||
|
|
this._activeEffects.splice(index, 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清除所有活跃特效记录
|
|||
|
|
* Clear all active effect records
|
|||
|
|
*/
|
|||
|
|
public clearActiveEffects(): void {
|
|||
|
|
this._activeEffects = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取活跃特效数量
|
|||
|
|
* Get active effect count
|
|||
|
|
*/
|
|||
|
|
public get activeEffectCount(): number {
|
|||
|
|
return this._activeEffects.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否可以添加新特效
|
|||
|
|
* Whether can add new effect
|
|||
|
|
*/
|
|||
|
|
public canAddEffect(): boolean {
|
|||
|
|
return this._activeEffects.length < this.maxConcurrent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置状态
|
|||
|
|
* Reset state
|
|||
|
|
*/
|
|||
|
|
public reset(): void {
|
|||
|
|
this._currentIndex = 0;
|
|||
|
|
this._activeEffects = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override onRemovedFromEntity(): void {
|
|||
|
|
this.reset();
|
|||
|
|
}
|
|||
|
|
}
|