Files
esengine/packages/rendering/particle/src/ParticleEmitter.ts
YHH 155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00

389 lines
12 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 type { Particle, ParticlePool } from './Particle';
/**
* 发射形状类型
* Emission shape type
*/
export enum EmissionShape {
/** 点发射 | Point emission */
Point = 'point',
/** 圆形发射(填充)| Circle emission (filled) */
Circle = 'circle',
/** 矩形发射 | Rectangle emission */
Rectangle = 'rectangle',
/** 线段发射 | Line emission */
Line = 'line',
/** 圆锥/扇形发射 | Cone/fan emission */
Cone = 'cone',
/** 圆环发射(边缘)| Ring emission (edge only) */
Ring = 'ring',
/** 矩形边缘发射 | Rectangle edge emission */
Edge = 'edge'
}
/**
* 数值范围
* Value range for randomization
*/
export interface ValueRange {
min: number;
max: number;
}
/**
* 颜色值
* Color value (RGBA)
*/
export interface ColorValue {
r: number;
g: number;
b: number;
a: number;
}
/**
* 发射器配置
* Emitter configuration
*/
export interface EmitterConfig {
/** 每秒发射数量 | Particles per second */
emissionRate: number;
/** 单次爆发数量0表示持续发射| Burst count (0 for continuous) */
burstCount: number;
/** 粒子生命时间范围(秒)| Particle lifetime range (seconds) */
lifetime: ValueRange;
/** 发射形状 | Emission shape */
shape: EmissionShape;
/** 形状半径(用于圆形/圆锥)| Shape radius (for circle/cone) */
shapeRadius: number;
/** 形状宽度(用于矩形/线段)| Shape width (for rectangle/line) */
shapeWidth: number;
/** 形状高度(用于矩形)| Shape height (for rectangle) */
shapeHeight: number;
/** 圆锥角度(弧度,用于圆锥发射)| Cone angle (radians, for cone shape) */
coneAngle: number;
/** 发射方向弧度0=右)| Emission direction (radians, 0=right) */
direction: number;
/** 发射方向随机范围(弧度)| Direction random spread (radians) */
directionSpread: number;
/** 初始速度范围 | Initial speed range */
speed: ValueRange;
/** 初始角速度范围 | Initial angular velocity range */
angularVelocity: ValueRange;
/** 初始缩放范围 | Initial scale range */
startScale: ValueRange;
/** 初始旋转范围(弧度)| Initial rotation range (radians) */
startRotation: ValueRange;
/** 初始颜色 | Initial color */
startColor: ColorValue;
/** 初始颜色变化范围 | Initial color variance */
startColorVariance: ColorValue;
/** 重力X | Gravity X */
gravityX: number;
/** 重力Y | Gravity Y */
gravityY: number;
}
/**
* 创建默认发射器配置
* Create default emitter configuration
*/
export function createDefaultEmitterConfig(): EmitterConfig {
return {
emissionRate: 10,
burstCount: 0,
lifetime: { min: 1, max: 2 },
shape: EmissionShape.Point,
shapeRadius: 0,
shapeWidth: 0,
shapeHeight: 0,
coneAngle: Math.PI / 6,
direction: -Math.PI / 2,
directionSpread: 0,
speed: { min: 50, max: 100 },
angularVelocity: { min: 0, max: 0 },
startScale: { min: 1, max: 1 },
startRotation: { min: 0, max: 0 },
startColor: { r: 1, g: 1, b: 1, a: 1 },
startColorVariance: { r: 0, g: 0, b: 0, a: 0 },
gravityX: 0,
gravityY: 0
};
}
/**
* 粒子发射器
* Particle emitter - handles particle spawning
*/
export class ParticleEmitter {
public config: EmitterConfig;
private _emissionAccumulator: number = 0;
private _isEmitting: boolean = true;
constructor(config?: Partial<EmitterConfig>) {
this.config = { ...createDefaultEmitterConfig(), ...config };
}
/** 是否正在发射 | Whether emitter is active */
get isEmitting(): boolean {
return this._isEmitting;
}
set isEmitting(value: boolean) {
this._isEmitting = value;
}
/**
* 发射粒子
* Emit particles
*
* @param pool - Particle pool
* @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,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): number {
if (!this._isEmitting) return 0;
let emitted = 0;
if (this.config.burstCount > 0) {
// 爆发模式 | Burst mode
for (let i = 0; i < this.config.burstCount; i++) {
const p = pool.spawn();
if (p) {
this._initParticle(p, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
emitted++;
}
}
this._isEmitting = false;
} else {
// 持续发射 | Continuous emission
this._emissionAccumulator += this.config.emissionRate * dt;
while (this._emissionAccumulator >= 1) {
const p = pool.spawn();
if (p) {
this._initParticle(p, worldX, worldY, worldRotation, worldScaleX, worldScaleY);
emitted++;
}
this._emissionAccumulator -= 1;
}
}
return emitted;
}
/**
* 立即爆发发射
* 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,
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, worldRotation, worldScaleX, worldScaleY);
emitted++;
}
}
return emitted;
}
/**
* 重置发射器
* Reset emitter
*/
reset(): void {
this._emissionAccumulator = 0;
this._isEmitting = true;
}
private _initParticle(
p: Particle,
worldX: number,
worldY: number,
worldRotation: number = 0,
worldScaleX: number = 1,
worldScaleY: number = 1
): void {
const config = this.config;
// 获取形状偏移 | Get shape offset
const [ox, oy] = this._getShapeOffset();
// 应用旋转和缩放到发射偏移 | 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 (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);
// 速度也应用缩放(使用平均缩放)| 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;
p.ay = config.gravityY;
// 旋转 | Rotation
p.rotation = randomRange(config.startRotation.min, config.startRotation.max);
p.angularVelocity = randomRange(config.angularVelocity.min, config.angularVelocity.max);
// 缩放(应用世界缩放)| 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);
p.g = clamp(config.startColor.g + randomRange(-config.startColorVariance.g, config.startColorVariance.g), 0, 1);
p.b = clamp(config.startColor.b + randomRange(-config.startColorVariance.b, config.startColorVariance.b), 0, 1);
p.alpha = clamp(config.startColor.a + randomRange(-config.startColorVariance.a, config.startColorVariance.a), 0, 1);
p.startR = p.r;
p.startG = p.g;
p.startB = p.b;
p.startAlpha = p.alpha;
}
private _getShapeOffset(): [number, number] {
const config = this.config;
switch (config.shape) {
case EmissionShape.Point:
return [0, 0];
case EmissionShape.Circle: {
// 填充圆形 | Filled circle
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * config.shapeRadius;
return [Math.cos(angle) * radius, Math.sin(angle) * radius];
}
case EmissionShape.Ring: {
// 圆环边缘 | Ring edge only
const angle = Math.random() * Math.PI * 2;
return [Math.cos(angle) * config.shapeRadius, Math.sin(angle) * config.shapeRadius];
}
case EmissionShape.Rectangle: {
// 填充矩形 | Filled rectangle
const x = randomRange(-config.shapeWidth / 2, config.shapeWidth / 2);
const y = randomRange(-config.shapeHeight / 2, config.shapeHeight / 2);
return [x, y];
}
case EmissionShape.Edge: {
// 矩形边缘 | Rectangle edge only
const perimeter = 2 * (config.shapeWidth + config.shapeHeight);
const t = Math.random() * perimeter;
const w = config.shapeWidth;
const h = config.shapeHeight;
if (t < w) {
// 上边 | Top edge
return [t - w / 2, h / 2];
} else if (t < w + h) {
// 右边 | Right edge
return [w / 2, h / 2 - (t - w)];
} else if (t < 2 * w + h) {
// 下边 | Bottom edge
return [w / 2 - (t - w - h), -h / 2];
} else {
// 左边 | Left edge
return [-w / 2, -h / 2 + (t - 2 * w - h)];
}
}
case EmissionShape.Line: {
const t = Math.random() - 0.5;
const cos = Math.cos(config.direction + Math.PI / 2);
const sin = Math.sin(config.direction + Math.PI / 2);
return [cos * config.shapeWidth * t, sin * config.shapeWidth * t];
}
case EmissionShape.Cone: {
const angle = config.direction + randomRange(-config.coneAngle / 2, config.coneAngle / 2);
const radius = Math.random() * config.shapeRadius;
return [Math.cos(angle) * radius, Math.sin(angle) * radius];
}
default:
return [0, 0];
}
}
}
function randomRange(min: number, max: number): number {
return min + Math.random() * (max - min);
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}