新增 FlowScope Git 包雏形
This commit is contained in:
25
Packages/com.flowscope.gamecore/Runtime/Audio/AudioHandle.cs
Normal file
25
Packages/com.flowscope.gamecore/Runtime/Audio/AudioHandle.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public sealed class AudioHandle : IAudioHandle
|
||||
{
|
||||
private readonly Func<bool> _isPlaying;
|
||||
private readonly Action<float> _stop;
|
||||
|
||||
internal AudioHandle(Func<bool> isPlaying, Action<float> stop)
|
||||
{
|
||||
_isPlaying = isPlaying ?? throw new ArgumentNullException(nameof(isPlaying));
|
||||
_stop = stop ?? throw new ArgumentNullException(nameof(stop));
|
||||
}
|
||||
|
||||
public bool IsPlaying => _isPlaying();
|
||||
|
||||
public void Stop(float fadeOut = 0f)
|
||||
{
|
||||
_stop(fadeOut);
|
||||
}
|
||||
|
||||
internal static AudioHandle Stopped { get; } = new AudioHandle(() => false, _ => { });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e7da42c17284465ab9a68bec5387f19
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
392
Packages/com.flowscope.gamecore/Runtime/Audio/AudioService.cs
Normal file
392
Packages/com.flowscope.gamecore/Runtime/Audio/AudioService.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public sealed class AudioService : MonoBehaviour, IAudioService
|
||||
{
|
||||
private readonly List<SfxSlot> _sfxSlots = new();
|
||||
private IResourceService _resources;
|
||||
private AudioServiceConfig _config;
|
||||
private AudioSource _bgmSource;
|
||||
private IResourceHandle<AudioClip> _bgmResource;
|
||||
private Coroutine _bgmFade;
|
||||
private float _bgmVolume = 1f;
|
||||
private float _sfxVolume = 1f;
|
||||
private bool _mute;
|
||||
private int _playSequence;
|
||||
|
||||
public AudioService Initialize(IResourceService resources, AudioServiceConfig config = null)
|
||||
{
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
_config = config ?? new AudioServiceConfig();
|
||||
if (_config.SfxPoolSize < 0)
|
||||
{
|
||||
_config.SfxPoolSize = 0;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = LoadClipIfCompleted(key);
|
||||
return PlayBgm(clipHandle, loop, fadeIn);
|
||||
}
|
||||
|
||||
public async Task<IAudioHandle> PlayBgmAsync(
|
||||
string key,
|
||||
bool loop = true,
|
||||
float fadeIn = 0f,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||
return PlayBgm(clipHandle, loop, fadeIn);
|
||||
}
|
||||
|
||||
public IAudioHandle PlaySfx(string key)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = LoadClipIfCompleted(key);
|
||||
var slot = GetSfxSlot(key);
|
||||
if (slot == null)
|
||||
{
|
||||
clipHandle.Dispose();
|
||||
return AudioHandle.Stopped;
|
||||
}
|
||||
|
||||
return PlaySfx(slot, clipHandle);
|
||||
}
|
||||
|
||||
public async Task<IAudioHandle> PlaySfxAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureInitialized();
|
||||
var clipHandle = await LoadClipAsync(key, cancellationToken);
|
||||
var slot = GetSfxSlot(key);
|
||||
if (slot == null)
|
||||
{
|
||||
clipHandle.Dispose();
|
||||
return AudioHandle.Stopped;
|
||||
}
|
||||
|
||||
return PlaySfx(slot, clipHandle);
|
||||
}
|
||||
|
||||
public void StopBgm(float fadeOut = 0f)
|
||||
{
|
||||
StopBgmSource(_bgmSource, fadeOut);
|
||||
}
|
||||
|
||||
public void SetBgmVolume(float volume)
|
||||
{
|
||||
_bgmVolume = Mathf.Clamp01(volume);
|
||||
if (_bgmSource != null)
|
||||
{
|
||||
_bgmSource.volume = EffectiveBgmVolume;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSfxVolume(float volume)
|
||||
{
|
||||
_sfxVolume = Mathf.Clamp01(volume);
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null)
|
||||
{
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMute(bool mute)
|
||||
{
|
||||
_mute = mute;
|
||||
if (_bgmSource != null)
|
||||
{
|
||||
_bgmSource.volume = EffectiveBgmVolume;
|
||||
}
|
||||
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null)
|
||||
{
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IAudioHandle PlayBgm(IResourceHandle<AudioClip> clipHandle, bool loop, float fadeIn)
|
||||
{
|
||||
StopBgm();
|
||||
_bgmSource = gameObject.AddComponent<AudioSource>();
|
||||
_bgmResource = clipHandle;
|
||||
_bgmSource.clip = clipHandle.Asset;
|
||||
_bgmSource.loop = loop;
|
||||
_bgmSource.volume = fadeIn > 0f ? 0f : EffectiveBgmVolume;
|
||||
_bgmSource.Play();
|
||||
|
||||
if (fadeIn > 0f)
|
||||
{
|
||||
_bgmFade = StartCoroutine(FadeVolume(_bgmSource, 0f, EffectiveBgmVolume, fadeIn, null));
|
||||
}
|
||||
|
||||
var source = _bgmSource;
|
||||
return new AudioHandle(
|
||||
() => source != null && source.isPlaying,
|
||||
fadeOut => StopBgmSource(source, fadeOut));
|
||||
}
|
||||
|
||||
private IAudioHandle PlaySfx(SfxSlot slot, IResourceHandle<AudioClip> clipHandle)
|
||||
{
|
||||
slot.Release();
|
||||
slot.Resource = clipHandle;
|
||||
slot.Source.clip = clipHandle.Asset;
|
||||
slot.Source.loop = false;
|
||||
slot.Source.volume = EffectiveSfxVolume;
|
||||
slot.Sequence = ++_playSequence;
|
||||
slot.Source.Play();
|
||||
|
||||
return new AudioHandle(
|
||||
() => slot.Source != null && slot.Source.isPlaying,
|
||||
fadeOut => StopSfxSlot(slot, fadeOut));
|
||||
}
|
||||
|
||||
private float EffectiveBgmVolume => _mute ? 0f : _bgmVolume;
|
||||
private float EffectiveSfxVolume => _mute ? 0f : _sfxVolume;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null && !slot.Source.isPlaying)
|
||||
{
|
||||
slot.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopBgm();
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
slot.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private IResourceHandle<AudioClip> LoadClipIfCompleted(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(_resources is ICompletedResourceService completedResources) ||
|
||||
!completedResources.TryLoadCompleted(key, out IResourceHandle<AudioClip> handle))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Audio clip is not loaded synchronously: {key}. Use PlayBgmAsync or PlaySfxAsync.");
|
||||
}
|
||||
|
||||
return ValidateLoadedClip(key, handle);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IResourceHandle<AudioClip>> LoadClipAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = await _resources.LoadAsync<AudioClip>(key, cancellationToken);
|
||||
return ValidateLoadedClip(key, handle);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static IResourceHandle<AudioClip> ValidateLoadedClip(
|
||||
string key,
|
||||
IResourceHandle<AudioClip> handle)
|
||||
{
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"Audio clip is missing: {key}");
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
private SfxSlot GetSfxSlot(string key)
|
||||
{
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Source != null && !slot.Source.isPlaying)
|
||||
{
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
if (_sfxSlots.Count < _config.SfxPoolSize)
|
||||
{
|
||||
var source = gameObject.AddComponent<AudioSource>();
|
||||
var slot = new SfxSlot(source);
|
||||
_sfxSlots.Add(slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
if (_config.ReuseOldestWhenExhausted && _sfxSlots.Count > 0)
|
||||
{
|
||||
var oldest = _sfxSlots[0];
|
||||
foreach (var slot in _sfxSlots)
|
||||
{
|
||||
if (slot.Sequence < oldest.Sequence)
|
||||
{
|
||||
oldest = slot;
|
||||
}
|
||||
}
|
||||
|
||||
oldest.Source.Stop();
|
||||
return oldest;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"SFX pool exhausted, skipped key: {key}");
|
||||
return null;
|
||||
}
|
||||
|
||||
private void StopBgmSource(AudioSource source, float fadeOut)
|
||||
{
|
||||
if (source == null || source != _bgmSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_bgmFade != null)
|
||||
{
|
||||
StopCoroutine(_bgmFade);
|
||||
_bgmFade = null;
|
||||
}
|
||||
|
||||
if (fadeOut > 0f && source.isPlaying)
|
||||
{
|
||||
_bgmFade = StartCoroutine(FadeVolume(
|
||||
source,
|
||||
source.volume,
|
||||
0f,
|
||||
fadeOut,
|
||||
() => CompleteBgmStop(source)));
|
||||
return;
|
||||
}
|
||||
|
||||
CompleteBgmStop(source);
|
||||
}
|
||||
|
||||
private void CompleteBgmStop(AudioSource source)
|
||||
{
|
||||
if (source == null || source != _bgmSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
source.Stop();
|
||||
source.volume = 0f;
|
||||
Destroy(source);
|
||||
_bgmSource = null;
|
||||
_bgmFade = null;
|
||||
_bgmResource?.Dispose();
|
||||
_bgmResource = null;
|
||||
}
|
||||
|
||||
private void StopSfxSlot(SfxSlot slot, float fadeOut)
|
||||
{
|
||||
if (slot?.Source == null || !slot.Source.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (fadeOut > 0f)
|
||||
{
|
||||
StartCoroutine(FadeVolume(
|
||||
slot.Source,
|
||||
slot.Source.volume,
|
||||
0f,
|
||||
fadeOut,
|
||||
() =>
|
||||
{
|
||||
slot.Source.Stop();
|
||||
slot.Release();
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
slot.Source.Stop();
|
||||
slot.Release();
|
||||
}
|
||||
|
||||
private IEnumerator FadeVolume(
|
||||
AudioSource source,
|
||||
float from,
|
||||
float to,
|
||||
float duration,
|
||||
Action complete)
|
||||
{
|
||||
var elapsed = 0f;
|
||||
while (elapsed < duration && source != null)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
source.volume = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (source != null)
|
||||
{
|
||||
source.volume = to;
|
||||
}
|
||||
|
||||
complete?.Invoke();
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_resources == null)
|
||||
{
|
||||
throw new InvalidOperationException("AudioService must be initialized before use.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SfxSlot
|
||||
{
|
||||
public SfxSlot(AudioSource source)
|
||||
{
|
||||
Source = source;
|
||||
}
|
||||
|
||||
public AudioSource Source { get; }
|
||||
public IResourceHandle<AudioClip> Resource { get; set; }
|
||||
public int Sequence { get; set; }
|
||||
|
||||
public void Release()
|
||||
{
|
||||
Resource?.Dispose();
|
||||
Resource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e81e8f0ce1a42e6a3b09bf74893b6b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public sealed class AudioServiceConfig
|
||||
{
|
||||
public int SfxPoolSize = 10;
|
||||
public bool ReuseOldestWhenExhausted = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 276b2d62e26a4edcaaeba1a96a3c2917
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public interface IAudioHandle
|
||||
{
|
||||
void Stop(float fadeOut = 0f);
|
||||
bool IsPlaying { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 780afa085183438eaa9b372219113d62
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.Audio
|
||||
{
|
||||
public interface IAudioService
|
||||
{
|
||||
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
|
||||
Task<IAudioHandle> PlayBgmAsync(
|
||||
string key,
|
||||
bool loop = true,
|
||||
float fadeIn = 0f,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
IAudioHandle PlaySfx(string key);
|
||||
Task<IAudioHandle> PlaySfxAsync(
|
||||
string key,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
void StopBgm(float fadeOut = 0f);
|
||||
void SetBgmVolume(float volume);
|
||||
void SetSfxVolume(float volume);
|
||||
void SetMute(bool mute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2763bff6b4ca4ac2ae5b9e5f764ad984
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user