불꽃놀이
This commit is contained in:
8
Assets/02_Scripts/FX.meta
Normal file
8
Assets/02_Scripts/FX.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 698b48d69d1768842a39115684f18c32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
144
Assets/02_Scripts/FX/ParticleSfx.cs
Normal file
144
Assets/02_Scripts/FX/ParticleSfx.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/FX/ParticleSfx.cs.meta
Normal file
2
Assets/02_Scripts/FX/ParticleSfx.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56d4d955f9d3bb443845adee6a2f84d5
|
||||
@@ -162,13 +162,19 @@ public void PlaySFX(AudioClip clip, float volume = 1f)
|
||||
}
|
||||
|
||||
//3D SFX 재생 (VR 공간음향 - 특정 위치에서 들림)
|
||||
public void PlaySFXAt(AudioClip clip, Vector3 position, float volume = 1f)
|
||||
//minDistance: 이 거리 안에선 풀 볼륨으로 들림. 하늘 높이 터지는 폭죽처럼 멀리서 나는 큰 소리는
|
||||
//크게(예: 30~50) 잡아야 거리 감쇠로 사라지지 않는다. maxDistance: 감쇠 계산 상한.
|
||||
public void PlaySFXAt(AudioClip clip, Vector3 position, float volume = 1f,
|
||||
float minDistance = 1f, float maxDistance = 500f)
|
||||
{
|
||||
if (clip == null) return;
|
||||
|
||||
AudioSource source = GetSfxSource();
|
||||
source.transform.position = position;
|
||||
source.spatialBlend = 1f; //3D
|
||||
source.rolloffMode = AudioRolloffMode.Logarithmic;
|
||||
source.minDistance = minDistance;
|
||||
source.maxDistance = maxDistance;
|
||||
Play(source, clip, volume);
|
||||
_ = ReturnAfterPlay(source, clip.length); //재생이 끝나면 풀에 반납
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user