Files
2026-07-30 23:15:58 +08:00

133 lines
4.5 KiB
C#

using UnityEngine;
/// <summary>
/// Centralized judge sound player. Assign four AudioSources (one per judgement) in the Inspector.
/// Designed to be low-latency: each judgement has a dedicated AudioSource and PlayOneShot is used.
/// Place this component on a persistent gameobject in the scene (or your audio container).
/// </summary>
public class JudgeSoundManager : MonoBehaviour
{
public static JudgeSoundManager Instance { get; private set; }
[Header("AudioSources (assign dedicated AudioSource for each judgement)")]
public AudioSource perfectSource;
public AudioSource greatSource;
public AudioSource goodSource;
public AudioSource missSource;
[Header("Enable/Disable per-judgement playback")]
[Tooltip("Toggle whether Perfect sounds are played")]
public bool enablePerfect = true;
[Tooltip("Toggle whether Great sounds are played")]
public bool enableGreat = true;
[Tooltip("Toggle whether Good sounds are played")]
public bool enableGood = true;
[Tooltip("Toggle whether Miss sounds are played")]
public bool enableMiss = true;
[Header("Fallback clips (optional, used if the assigned AudioSource has no clip)")]
public AudioClip perfectClip;
public AudioClip greatClip;
public AudioClip goodClip;
public AudioClip missClip;
private void Awake()
{
if (Instance == null)
{
Instance = this;
// Do not automatically destroy; leave lifecycle to scene management
}
else
{
Destroy(gameObject);
return;
}
// Ensure AudioSources are configured for low latency
ConfigureSource(perfectSource);
ConfigureSource(greatSource);
ConfigureSource(goodSource);
ConfigureSource(missSource);
}
private void ConfigureSource(AudioSource src)
{
if (src == null) return;
src.playOnAwake = false;
// keep 3D off for UI/flat sounds
src.spatialBlend = 0f;
// ensure not looping
src.loop = false;
// Lowest-latency reactive playback for judge SFX:
// bypass any AudioMixer-group / AudioListener DSP effects (reverb, compressor, etc.)
// that would otherwise add buffer-sized processing latency on top of hardware output.
src.bypassEffects = true;
src.bypassListenerEffects = true;
src.bypassReverbZones = true;
// Highest priority so a judge hit is never delayed by voice-stealing under load.
src.priority = 0;
}
public enum JudgeResult { Perfect, Great, Good, Miss }
public void PlayJudgeSound(string result)
{
if (string.IsNullOrEmpty(result)) return;
switch (result)
{
case "Perfect": PlayJudgeSound(JudgeResult.Perfect); break;
case "Great": PlayJudgeSound(JudgeResult.Great); break;
case "Good": PlayJudgeSound(JudgeResult.Good); break;
case "Miss": PlayJudgeSound(JudgeResult.Miss); break;
default: break;
}
}
public void PlayJudgeSound(JudgeResult r)
{
// honor inspector toggles to allow disabling specific judgement sounds
switch (r)
{
case JudgeResult.Perfect:
if (!enablePerfect) return;
PlayOnSource(perfectSource, perfectClip);
break;
case JudgeResult.Great:
if (!enableGreat) return;
PlayOnSource(greatSource, greatClip);
break;
case JudgeResult.Good:
if (!enableGood) return;
PlayOnSource(goodSource, goodClip);
break;
case JudgeResult.Miss:
if (!enableMiss) return;
PlayOnSource(missSource, missClip);
break;
}
}
private void PlayOnSource(AudioSource src, AudioClip fallbackClip)
{
if (src != null)
{
// Prefer PlayOneShot so sources can overlap and we don't need to manage clip state
if (src.clip != null)
{
src.PlayOneShot(src.clip);
}
else if (fallbackClip != null)
{
src.PlayOneShot(fallbackClip);
}
}
else if (fallbackClip != null)
{
// no dedicated source provided: play via a temporary one-shot using AudioSource.PlayClipAtPoint at camera position
AudioSource.PlayClipAtPoint(fallbackClip, Camera.main != null ? Camera.main.transform.position : Vector3.zero);
}
}
}