using System; using UnityEngine; // 파티클이 새로 태어나는 순간(발사 / 폭발)에 사운드 하나, (옵션) 라이트 하나를 재생하는 최소 컴포넌트. // - 발사 파티클(쏘아올리는 것)에는 사운드만 // - dead 이벤트 서브 이미터로 터지는 파티클에는 사운드 + 라이트 // Unity엔 "파티클 생성 콜백"이 없어서, 살아있는 파티클 수가 늘어나는 순간을 감지한다. [RequireComponent(typeof(ParticleSystem))] public class ParticleSfx : MonoBehaviour { [Header("사운드")] [SerializeField] private AudioClip[] _sfx; // 여러 개면 무작위 (반복감 완화) [SerializeField, Range(0f, 1f)] private float _volume = 0.8f; [Tooltip("폭발마다 사운드가 들릴 확률 (1=항상). 폭죽이 많아 소리가 겹칠 때 낮춰서 솎아냄")] [SerializeField, Range(0f, 1f)] private float _sfxChance = 1f; [Tooltip("이 거리 안에선 풀 볼륨. 하늘 높이 터지는 폭발음은 크게(예: 30~50) 잡아야 잘 들림")] [SerializeField] private float _minDistance = 30f; [SerializeField] private float _maxDistance = 500f; [Header("라이트 (폭발용, 옵션)")] [SerializeField] private bool _flashLight = false; [Tooltip("지정하면 라이트를 폭발 위치가 아니라 이 콜라이더 범위 안의 무작위 지점에 띄운다. 폭죽은 위·옆으로 " + "제멋대로 퍼지므로, NPC를 비추려면 관객 구역에 콜라이더(예: Box, IsTrigger)를 두고 여기 연결. 비우면 폭발 위치.")] [SerializeField] private Collider _lightArea; [SerializeField] private Color _lightColor = new(1f, 0.9f, 0.7f); // 따뜻한 백색 (여러 색 폭발의 캐스트광은 섞여서 이게 자연스러움) [SerializeField] private float _lightIntensity = 30f; [SerializeField] private float _lightRange = 25f; [SerializeField] private float _flashDuration = 0.35f; [SerializeField] private LightShadows _shadows = LightShadows.None; // VR 성능상 그림자 off private ParticleSystem _ps; private ParticleSystemRenderer _renderer; private Light _light; private int _lastAlive; private void Awake() { _ps = GetComponent(); _renderer = GetComponent(); } private void OnEnable() => _lastAlive = _ps.particleCount; // 재활성화 시 첫 프레임 오발동 방지 private void LateUpdate() { int alive = _ps.particleCount; if (alive > _lastAlive) // 새 파티클 등장 = 발사/폭발 순간 { Vector3 pos = GetEventPosition(); if (UnityEngine.Random.value <= _sfxChance) PlaySfx(pos); // 사운드는 폭발 위치에서 if (_flashLight) _ = FlashLight(GetLightPosition(pos)); // 라이트는 (옵션) 지상 높이로 내려서 } _lastAlive = alive; } // 실제로 그려지는 파티클의 월드 바운즈 중심 = 눈에 보이는 폭발 위치. // 파티클 좌표를 직접 변환하면 시뮬레이션 스페이스·스케일링 모드(여기선 Local + 큰 스케일 10/30)와 // 안 맞아 위치가 크게 어긋난다. 렌더러 바운즈는 항상 월드 공간이라 스케일과 무관하게 정확하다. private Vector3 GetEventPosition() { if (_renderer != null && _ps.particleCount > 0) return _renderer.bounds.center; return transform.position; } // 라이트를 놓을 위치: 범위 콜라이더가 있으면 그 안 무작위 지점, 없으면 폭발 지점. // 폭죽은 수직으로 안 올라가고 옆으로 퍼지므로, NPC 조명은 폭발 XZ가 아니라 관객 구역 기준이어야 함. private Vector3 GetLightPosition(Vector3 burstPos) { if (_lightArea == null) return burstPos; Bounds b = _lightArea.bounds; // 월드 AABB 안에서 무작위 return new Vector3( UnityEngine.Random.Range(b.min.x, b.max.x), UnityEngine.Random.Range(b.min.y, b.max.y), UnityEngine.Random.Range(b.min.z, b.max.z)); } private void PlaySfx(Vector3 pos) { if (_sfx == null || _sfx.Length == 0) return; AudioClip clip = _sfx[UnityEngine.Random.Range(0, _sfx.Length)]; if (clip == null) return; if (SoundManager.Instance != null) { SoundManager.Instance.PlaySFXAt(clip, pos, _volume, _minDistance, _maxDistance); // 3D SFX 풀 재사용 (VR 공간음) return; } // 매니저 없을 때 폴백: 거리 설정을 반영한 임시 3D 소스 (PlayClipAtPoint는 minDistance 조절 불가) var go = new GameObject("FireworkSfx"); go.transform.position = pos; var src = go.AddComponent(); src.clip = clip; src.volume = _volume; src.spatialBlend = 1f; src.rolloffMode = AudioRolloffMode.Logarithmic; src.minDistance = _minDistance; src.maxDistance = _maxDistance; src.Play(); Destroy(go, clip.length); } // 라이트 하나를 만들어(재사용) 번쩍였다 끈다 private async Awaitable FlashLight(Vector3 pos) { if (_light == null) { var go = new GameObject("FlashLight"); go.transform.SetParent(transform); _light = go.AddComponent(); _light.type = LightType.Point; _light.shadows = _shadows; _light.renderMode = LightRenderMode.ForcePixel; } _light.transform.position = pos; _light.color = _lightColor; _light.range = _lightRange; _light.enabled = true; try { float t = 0f; while (t < _flashDuration) { _light.intensity = _lightIntensity * (1f - t / _flashDuration); t += Time.deltaTime; await Awaitable.NextFrameAsync(destroyCancellationToken); } } catch (OperationCanceledException) { /* 파괴됨 */ } finally { if (_light != null) { _light.intensity = 0f; _light.enabled = false; } } } }