145 lines
6.2 KiB
C#
145 lines
6.2 KiB
C#
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<ParticleSystem>();
|
|
_renderer = GetComponent<ParticleSystemRenderer>();
|
|
}
|
|
|
|
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<AudioSource>();
|
|
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>();
|
|
_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;
|
|
}
|
|
}
|
|
}
|
|
}
|