조개 사운드

This commit is contained in:
2026-06-24 14:40:39 +09:00
parent 59bf6f0555
commit 513fa94ab9
28 changed files with 626 additions and 4 deletions

View File

@@ -38,6 +38,36 @@ public class ClamOpenClose : MonoBehaviour
[Tooltip("쾅 하고 닫힌 뒤 원래 닫힌 위치로 돌아오는 시간입니다.")]
[SerializeField] private float snapCloseOvershootDuration = 0.02f;
[Header("Sound")]
[SerializeField] private AudioSource audioSource;
[Tooltip("조개가 닫히며 입이 부딪힐 때 재생할 사운드입니다. 여러 개를 넣으면 랜덤 재생됩니다.")]
[SerializeField] private AudioClip[] closeImpactSounds;
[Tooltip("조개가 닫히기 시작할 때 재생할 예고/마찰 사운드입니다. 필요 없으면 비워두세요.")]
[SerializeField] private AudioClip closeStartSound;
[Range(0f, 1f)]
[SerializeField] private float closeImpactVolume = 1f;
[Range(0f, 1f)]
[SerializeField] private float closeStartVolume = 0.5f;
[Tooltip("닫힘 충격음의 최소 피치입니다.")]
[SerializeField] private float minImpactPitch = 0.9f;
[Tooltip("닫힘 충격음의 최대 피치입니다.")]
[SerializeField] private float maxImpactPitch = 1.1f;
[Tooltip("닫힘 충격음의 볼륨을 살짝 랜덤하게 줄지 여부입니다.")]
[SerializeField] private bool randomizeImpactVolume = true;
[Tooltip("볼륨 랜덤 적용 시 최소 배율입니다.")]
[SerializeField] private float minImpactVolumeMultiplier = 0.85f;
[Tooltip("볼륨 랜덤 적용 시 최대 배율입니다.")]
[SerializeField] private float maxImpactVolumeMultiplier = 1.0f;
[Header("Start Option")]
[SerializeField] private bool startAutomatically = true;
[SerializeField] private bool startOpened = false;
@@ -74,6 +104,17 @@ private void Awake()
if (found != null) downShell = found;
}
if (audioSource == null)
{
audioSource = GetComponent<AudioSource>();
}
if (audioSource != null)
{
audioSource.playOnAwake = false;
audioSource.loop = false;
}
if (upShell == null)
{
Debug.LogWarning("[ClamOpenClose] UpShell이 연결되지 않았습니다.", this);
@@ -145,6 +186,7 @@ private IEnumerator OpenCloseRoutine()
yield return new WaitForSeconds(openWait);
isClosing = true;
PlayCloseStartSound();
onCloseStarted?.Invoke();
yield return MoveShells(
@@ -158,6 +200,10 @@ private IEnumerator OpenCloseRoutine()
isOpen = false;
// 실제 입이 맞닿는 타이밍.
// 랜덤으로 닫혀도 항상 닫힘 동작이 끝난 직후 실행됨.
PlayCloseImpactSound();
if (useSnapClose)
{
yield return SnapCloseEffect();
@@ -225,6 +271,47 @@ private IEnumerator SnapCloseEffect()
downShell.localRotation = downClosedRot;
}
private void PlayCloseStartSound()
{
if (audioSource == null || closeStartSound == null)
return;
audioSource.pitch = 1f;
audioSource.PlayOneShot(closeStartSound, closeStartVolume);
}
private void PlayCloseImpactSound()
{
if (audioSource == null)
return;
if (closeImpactSounds == null || closeImpactSounds.Length == 0)
return;
AudioClip clip = closeImpactSounds[Random.Range(0, closeImpactSounds.Length)];
if (clip == null)
return;
float originalPitch = audioSource.pitch;
audioSource.pitch = Random.Range(minImpactPitch, maxImpactPitch);
float volume = closeImpactVolume;
if (randomizeImpactVolume)
{
float volumeMultiplier = Random.Range(minImpactVolumeMultiplier, maxImpactVolumeMultiplier);
volume *= volumeMultiplier;
}
audioSource.PlayOneShot(clip, volume);
// PlayOneShot은 재생 직후 피치를 바꿔도 이미 재생 중인 소리에 영향이 적지만,
// 다음 사운드 재생을 위해 기본 피치로 복구.
audioSource.pitch = originalPitch;
}
private void SetClosedImmediately()
{
if (upShell != null)

View File

@@ -23,6 +23,24 @@ public class FallingStalactite : MonoBehaviour
[Tooltip("떨어지기 전에는 데미지를 끄고, 떨어질 때 켭니다.")]
[SerializeField] private bool damageOnlyWhileFalling = true;
[Header("Sound")]
[SerializeField] private AudioSource audioSource;
[Tooltip("종유석이 떨어지기 시작할 때 재생할 소리입니다.")]
[SerializeField] private AudioClip fallSound;
[Range(0f, 1f)]
[SerializeField] private float fallSoundVolume = 1f;
[Tooltip("리셋 후 다시 떨어질 때도 소리를 재생할지 여부입니다.")]
[SerializeField] private bool playSoundEveryFall = true;
[Tooltip("Play through the shared 2D SFX pool when available so the fall cue is always audible.")]
[SerializeField] private bool playThroughSoundManager = true;
[Tooltip("Force the fallback AudioSource to 2D playback.")]
[SerializeField] private bool playFallSoundAs2D = true;
[Header("Reset Option")]
[SerializeField] private bool resetAfterFall = true;
@@ -38,6 +56,7 @@ public class FallingStalactite : MonoBehaviour
private Vector3 startPosition;
private Quaternion startRotation;
private bool hasFallen;
private bool hasPlayedSound;
private Coroutine fallRoutine;
public bool HasFallen => hasFallen;
@@ -62,6 +81,9 @@ private void ResolveReferences()
if (damageObstacle == null)
damageObstacle = GetComponent<DamageObstacle>();
if (audioSource == null)
audioSource = GetComponent<AudioSource>();
}
private void PrepareStalactite()
@@ -85,7 +107,21 @@ private void PrepareStalactite()
damageCollider.enabled = true;
}
if (audioSource != null)
{
audioSource.playOnAwake = false;
audioSource.mute = false;
if (playFallSoundAs2D)
audioSource.spatialBlend = 0f;
}
PreloadFallSound();
hasFallen = false;
if (playSoundEveryFall)
hasPlayedSound = false;
}
public void TriggerFall()
@@ -106,6 +142,8 @@ private IEnumerator FallRoutine()
if (fallDelay > 0f)
yield return new WaitForSeconds(fallDelay);
PlayFallSound();
if (damageObstacle != null)
{
damageObstacle.SetDamage(damage);
@@ -137,6 +175,71 @@ private IEnumerator FallRoutine()
fallRoutine = null;
}
private AudioClip GetFallClip()
{
if (fallSound != null)
return fallSound;
if (audioSource != null)
return audioSource.clip;
return null;
}
private void PreloadFallSound()
{
AudioClip clipToLoad = GetFallClip();
if (clipToLoad != null && clipToLoad.loadState == AudioDataLoadState.Unloaded)
clipToLoad.LoadAudioData();
}
private void PlayFallSound()
{
if (hasPlayedSound && !playSoundEveryFall)
return;
AudioClip clipToPlay = GetFallClip();
if (clipToPlay == null)
{
if (showDebugLog)
Debug.LogWarning($"[FallingStalactite] {name} Fall Sound가 연결되지 않았습니다.", this);
return;
}
if (clipToPlay.loadState == AudioDataLoadState.Unloaded)
clipToPlay.LoadAudioData();
if (playThroughSoundManager && SoundManager.Instance != null)
{
SoundManager.Instance.PlaySFX(clipToPlay, fallSoundVolume);
}
else if (audioSource != null)
{
audioSource.enabled = true;
audioSource.mute = false;
audioSource.playOnAwake = false;
if (audioSource.volume <= 0f)
audioSource.volume = 1f;
if (playFallSoundAs2D)
audioSource.spatialBlend = 0f;
audioSource.PlayOneShot(clipToPlay, fallSoundVolume);
}
else
{
AudioSource.PlayClipAtPoint(clipToPlay, transform.position, fallSoundVolume);
}
hasPlayedSound = true;
if (showDebugLog)
Debug.Log($"[FallingStalactite] {name} 낙하 사운드 재생: {clipToPlay.name}", this);
}
public void ResetStalactite()
{
if (rb != null)
@@ -161,6 +264,11 @@ public void ResetStalactite()
hasFallen = false;
}
if (playSoundEveryFall)
{
hasPlayedSound = false;
}
if (showDebugLog)
Debug.Log($"[FallingStalactite] {name} 원위치 리셋", this);
}

View File

@@ -0,0 +1,158 @@
using System.Collections;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
public class SteeringKeyGrabMusic : MonoBehaviour
{
[Header("References")]
[SerializeField] private XRGrabInteractable grabInteractable;
[SerializeField] private AudioSource audioSource;
[Header("Playback")]
[SerializeField] private bool playOnGrab = true;
[SerializeField] private bool stopOnRelease = true;
[Tooltip("키를 잡았을 때 음악을 처음부터 다시 재생할지 여부입니다.")]
[SerializeField] private bool restartFromBeginningOnGrab = false;
[Header("Fade")]
[SerializeField] private bool useFade = true;
[SerializeField] private float fadeInDuration = 0.4f;
[SerializeField] private float fadeOutDuration = 0.5f;
[Range(0f, 1f)]
[SerializeField] private float targetVolume = 1f;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private Coroutine fadeRoutine;
private bool isGrabbed;
private void Awake()
{
if (grabInteractable == null)
grabInteractable = GetComponentInChildren<XRGrabInteractable>(true);
if (audioSource == null)
audioSource = GetComponent<AudioSource>();
if (audioSource != null)
{
audioSource.playOnAwake = false;
audioSource.loop = true;
if (useFade)
audioSource.volume = 0f;
else
audioSource.volume = targetVolume;
}
}
private void OnEnable()
{
if (grabInteractable != null)
{
grabInteractable.selectEntered.AddListener(OnKeyGrabbed);
grabInteractable.selectExited.AddListener(OnKeyReleased);
}
}
private void OnDisable()
{
if (grabInteractable != null)
{
grabInteractable.selectEntered.RemoveListener(OnKeyGrabbed);
grabInteractable.selectExited.RemoveListener(OnKeyReleased);
}
}
private void OnKeyGrabbed(SelectEnterEventArgs args)
{
isGrabbed = true;
if (!playOnGrab)
return;
if (audioSource == null)
{
if (showDebugLog)
Debug.LogWarning("[SteeringKeyGrabMusic] AudioSource가 연결되지 않았습니다.", this);
return;
}
if (restartFromBeginningOnGrab)
audioSource.time = 0f;
if (!audioSource.isPlaying)
audioSource.Play();
if (useFade)
StartFade(targetVolume, fadeInDuration);
else
audioSource.volume = targetVolume;
if (showDebugLog)
Debug.Log("[SteeringKeyGrabMusic] 키 잡음. 음악 재생.", this);
}
private void OnKeyReleased(SelectExitEventArgs args)
{
isGrabbed = false;
if (!stopOnRelease)
return;
if (audioSource == null)
return;
if (useFade)
StartFade(0f, fadeOutDuration, stopAfterFade: true);
else
audioSource.Stop();
if (showDebugLog)
Debug.Log("[SteeringKeyGrabMusic] 키 놓음. 음악 정지.", this);
}
private void StartFade(float target, float duration, bool stopAfterFade = false)
{
if (fadeRoutine != null)
StopCoroutine(fadeRoutine);
fadeRoutine = StartCoroutine(FadeRoutine(target, duration, stopAfterFade));
}
private IEnumerator FadeRoutine(float target, float duration, bool stopAfterFade)
{
if (audioSource == null)
yield break;
float startVolume = audioSource.volume;
float timer = 0f;
float safeDuration = Mathf.Max(0.01f, duration);
while (timer < safeDuration)
{
timer += Time.deltaTime;
float t = Mathf.Clamp01(timer / safeDuration);
float smoothT = t * t * (3f - 2f * t);
audioSource.volume = Mathf.Lerp(startVolume, target, smoothT);
yield return null;
}
audioSource.volume = target;
if (stopAfterFade && !isGrabbed)
{
audioSource.Stop();
}
fadeRoutine = null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3a3e3221faafc724c82b5b177b84072e