diff --git a/src/engine/SpriteEffectInstance.ts b/src/engine/SpriteEffectInstance.ts index 6a0f2e8..0fbf1fa 100644 --- a/src/engine/SpriteEffectInstance.ts +++ b/src/engine/SpriteEffectInstance.ts @@ -15,6 +15,21 @@ const randomRange = (min: number, max: number): number => const lerp = (a: number, b: number, t: number): number => a + (b - a) * t; +/** + * 키프레임 보간 (2개: 선형, 3개: 전반/후반 분할) + */ +const lerpKeyframes = (values: number[], t: number): number => { + if (values.length === 2) { + return lerp(values[0], values[1], t); + } + // 3개 이상: 균등 구간 분할 + const segments = values.length - 1; + const segT = t * segments; + const idx = Math.min(Math.floor(segT), segments - 1); + const localT = segT - idx; + return lerp(values[idx], values[idx + 1], localT); +}; + /** * 단일 SpriteEffectConfig에 대응하는 런타임 이펙트 인스턴스 * 텍스처 로딩, 메쉬 풀링, 방출/업데이트 로직을 관리 @@ -67,6 +82,11 @@ export class SpriteEffectInstance { this.loadTexture(config.spriteUrl); } + /** 런타임 설정 업데이트 (maxParticles 제외) */ + updateConfig(config: SpriteEffectConfig): void { + this.config = config; + } + /** 텍스처 로드 */ private loadTexture(url: string): void { const loader = new THREE.TextureLoader(); @@ -139,7 +159,8 @@ export class SpriteEffectInstance { particle.velocity.y = Math.sin(angleRad) * speed; // 초기 속성 - particle.scale = randomRange(config.initialScale[0], config.initialScale[1]); + particle.initialScale = randomRange(config.initialScale[0], config.initialScale[1]); + particle.scale = particle.initialScale; particle.rotation = 0; particle.opacity = 1; particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]); @@ -212,10 +233,11 @@ export class SpriteEffectInstance { // overLifetime 보간 적용 if (overLifetime) { if (overLifetime.scale) { - particle.scale = lerp(overLifetime.scale[0], overLifetime.scale[1], lifeRatio); + // overLifetime.scale은 initialScale에 대한 배율로 적용 + particle.scale = particle.initialScale * lerpKeyframes(overLifetime.scale, lifeRatio); } if (overLifetime.opacity) { - particle.opacity = lerp(overLifetime.opacity[0], overLifetime.opacity[1], lifeRatio); + particle.opacity = lerpKeyframes(overLifetime.opacity, lifeRatio); } if (overLifetime.rotationSpeed) { particle.rotation += overLifetime.rotationSpeed * deltaTime; diff --git a/src/engine/SpriteEffectManager.ts b/src/engine/SpriteEffectManager.ts index a7a7fc2..17469bc 100644 --- a/src/engine/SpriteEffectManager.ts +++ b/src/engine/SpriteEffectManager.ts @@ -59,8 +59,12 @@ export class SpriteEffectManager { const key = `${area.id}::${effectConfig.id}`; activeKeys.add(key); - // 이미 존재하면 스킵 - if (this.instances.has(key)) continue; + // 이미 존재하면 설정만 업데이트 + const existing = this.instances.get(key); + if (existing) { + existing.updateConfig(effectConfig); + continue; + } // 새 인스턴스 생성 console.log('[SpriteEffectManager] 인스턴스 생성:', key, effectConfig.spriteUrl); diff --git a/src/engine/SpriteParticlePool.ts b/src/engine/SpriteParticlePool.ts index b62fde5..32149a9 100644 --- a/src/engine/SpriteParticlePool.ts +++ b/src/engine/SpriteParticlePool.ts @@ -14,6 +14,8 @@ export interface SpriteParticle { velocity: Point; /** 현재 스케일 */ scale: number; + /** 방출 시 초기 스케일 (overLifetime 배율 기준값) */ + initialScale: number; /** 현재 회전 (라디안) */ rotation: number; /** 현재 투명도 */ @@ -48,6 +50,7 @@ export class SpriteParticlePool { position: { x: 0, y: 0 }, velocity: { x: 0, y: 0 }, scale: 1, + initialScale: 1, rotation: 0, opacity: 1, age: 0, diff --git a/src/types/spriteEffect.ts b/src/types/spriteEffect.ts index 9989be8..cf27afa 100644 --- a/src/types/spriteEffect.ts +++ b/src/types/spriteEffect.ts @@ -10,10 +10,10 @@ export type SpriteBlendMode = 'normal' | 'additive'; * 수명 기반 파티클 속성 보간 설정 */ export interface SpriteParticleOverLifetime { - /** [시작, 끝] 스케일 */ - scale?: [number, number]; - /** [시작, 끝] 투명도 */ - opacity?: [number, number]; + /** 스케일 보간: [시작, 끝] 또는 [시작, 중간, 끝] */ + scale?: number[]; + /** 투명도 보간: [시작, 끝] 또는 [시작, 중간, 끝] */ + opacity?: number[]; /** 회전 속도 (라디안/초) */ rotationSpeed?: number; /** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */