TH1/Unity/Assets/Scripts/TH1_Audio/AudioManager.cs
2026-05-21 02:00:00 +08:00

710 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: 白哉
* @Description:
* @Date: 2025年05月22日 星期四 14:05:32
* @Modify:
*/
using System.Collections.Generic;
using System.Linq;
using Logic.CrashSight;
using RuntimeData;
using TH1_Logic.Config;
using TH1_Logic.Core;
using UnityEngine;
namespace Logic.Audio
{
public class AudioManager
{
public static AudioManager Instance = new AudioManager();
private AudioPlayer _musicPlayer;
private List<AudioPlayer> _allPlayer;
private Dictionary<string, List<AudioClip>> _clips;
private GameObject AudioRoot;
private Dictionary<string, float> _musicRecord;
private readonly HashSet<uint> _ambientTerritoryGridSet = new HashSet<uint>();
/// <summary>
/// 获取当前正在播放的BGM名(用于UI侧暂存,无在播则返回null)
/// </summary>
public string GetCurrentMusicName()
{
if (_musicPlayer == null) return null;
if (_musicPlayer.State == PlayerState.Finished || _musicPlayer.State == PlayerState.Prepare) return null;
return _musicPlayer.MusicName;
}
// BGM 轮播相关
private bool _isPlayingRotation = false;
private float _gapTimer = 0f;
private List<string> _activePlayerMusics = new List<string>();
private int _currentMusicIndex = 0;
private string _lastPlayedMusicName = null;
// 等待播放相关方案A
private string _pendingMusicName = null;
private float _fadeOutWaitTimer = 0f;
private bool _pendingIsContinue = false;
private float _pendingFadeIn = 2f;
private float _pendingFadeOut = 2f;
private bool _pendingIsLoop = false;
private float _extraInterval = 5f; // 轮播两首歌之间的额外间隔
public void Init()
{
_allPlayer = new List<AudioPlayer>();
_clips = new Dictionary<string, List<AudioClip>>();
_musicRecord = new Dictionary<string, float>();
var path = new Dictionary<string, string>();
path["Main"] = "Audio/Main";
path["RemiliaEgyptian"] = "Audio/RemiliaEgyptian";
path["SatoriIndian"] = "Audio/SatoriIndian";
path["KanakoGermany"] = "Audio/KanakoGermany";
path["KaguyaFrench"] = "Audio/KaguyaFrench";
path["ReimuNorway"] = "Audio/ReimuNorway";
path["ByakurenBritish"] = "Audio/ByakurenBritish";
path["MikoPersian"] = "Audio/MikoPersian";
path["ZanmuByzantine"] = "Audio/ZanmuByzantine";
path["LapisMayan"] = "Audio/LapisMayan";
path["IizunamaruMalian"] = "Audio/IizunamaruMalian";
path["CirnoGreek"] = "Audio/CirnoGreek";
path["HinanawiAztec"] = "Audio/HinanawiAztec";
path["SaigyoujiSumerian"] = "Audio/SaigyoujiSumerian";
path["ChirizukaIncan"] = "Audio/ChirizukaIncan";
path["KijinMogolian"] = "Audio/KijinMogolian";
path["WatatsukiKhmer"] = "Audio/WatatsukiKhmer";
path["Story"] = "Audio/Game";
path["SFX/UI_buttonHover"] = "Audio/SFX/UI_buttonHover";
path["SFX/UI_buttonClick"] = "Audio/SFX/UI_buttonClick";
path["SFX/UNIT_click"] = "Audio/SFX/UNIT_click";
path["SFX/UNIT_bomb"] = "Audio/SFX/UNIT_bomb";
path["SFX/UNIT_archer"] = "Audio/SFX/UNIT_archer";
path["SFX/UNIT_hurt"] = "Audio/SFX/UNIT_hurt";
path["SFX/UNIT_move"] = "Audio/SFX/UNIT_move";
path["SFX/ENV_sea"] = "Audio/SFX/ENV_sea";
path["SFX/ENV_forest"] = "Audio/SFX/ENV_forest";
path["SFX/CITY_exp"] = "Audio/SFX/CITY_exp";
path["SFX/CITY_levelup"] = "Audio/SFX/CITY_levelup";
path["SFX/UNIT_attack"] = "Audio/SFX/UNIT_attack1";
path["SFX/PLAYER_coin"] = "Audio/SFX/PLAYER_coin";
path["SFX/GRID_build"] = "Audio/SFX/GRID_build";
path["SFX/UNIT_attack_arrow"] = "Audio/SFX/UNIT_attack_arrow";
path["SFX/UNIT_attack_bomb"] = "Audio/SFX/UNIT_attack_bomb";
path["SFX/UNIT_levelup"] = "Audio/SFX/UNIT_levelup";
path["SFX/UNIT_capture"] = "Audio/SFX/UNIT_capture";
path["SFX/UNIT_heal"] = "Audio/SFX/UNIT_heal";
path["SFX/UNIT_die"] = "Audio/SFX/UNIT_die";
path["SFX/UNIT_treasure"] = "Audio/SFX/UNIT_treasure";
path["SFX/MATCH_win"] = "Audio/SFX/MATCH_win";
path["SFX/MATCH_lose"] = "Audio/SFX/MATCH_lose";
path["SFX/UNIT_born"] = "Audio/SFX/UNIT_born";
path["SFX/start"] = "Audio/SFX/start";
foreach (var kv in path)
{
_clips[kv.Key] = new List<AudioClip>();
_clips[kv.Key].Add(Resources.Load<AudioClip>(kv.Value));
}
if (!AudioRoot) AudioRoot = new GameObject();
AudioRoot.name = "AudioRoot";
var cfg = Object.FindObjectOfType<AudioClipConfig>();
if (cfg != null)
{
foreach (var clip in cfg.Clips)
{
_clips[clip.name] = new List<AudioClip>();
_clips[clip.name].Add(clip);
}
}
}
public void Update()
{
foreach (var player in _allPlayer)
{
if (player.IsMusic) player.Update(ConfigManager.Instance.Config.MusicVolume);
else player.Update(ConfigManager.Instance.Config.AudioVolume);
if (player.State != PlayerState.Finished) continue;
if (!player.Clip) continue;
if (!_clips.ContainsKey(player.MusicName)) _clips[player.MusicName] = new List<AudioClip>();
_clips[player.MusicName].Add(player.Clip);
player.Clip = null;
}
// 处理待播放音乐的等待方案A
UpdatePendingMusic();
// 更新 BGM 轮播
UpdateBgmRotation();
}
public void PlayMusic(string musicName, float fadeIn, float fadeOut, bool isLoop, bool isContinue=true)
{
if (!_clips.ContainsKey(musicName) || _clips[musicName].Count == 0) return;
if (_musicPlayer != null)
{
if (_musicPlayer.MusicName == musicName)
{
if (_musicPlayer.State == PlayerState.Playing) return;
if (_musicPlayer.State == PlayerState.FadeIn) return;
if (_musicPlayer.State == PlayerState.FadeOut)
{
_musicPlayer.StartTime = Time.time - (_musicPlayer.FadeOutDuration - (Time.time - _musicPlayer.EndTime));
_musicPlayer.State = PlayerState.FadeIn;
return;
}
}
// 异曲切换: 让旧 player 自然走完 FadeOut, 新曲用独立的新 player crossfade,
// 避免复用同一个 _musicPlayer 导致 Source.clip 被立刻覆盖而戛然而止。
StopMusic();
_musicPlayer = null;
}
_musicPlayer = GetPlayer();
_musicPlayer.IsMusic = true;
// 清除待播放和间隔状态,因为正在显式切换到新音乐
_pendingMusicName = null;
_gapTimer = 0;
_musicPlayer.Clip = _clips[musicName][0];
if (!_musicPlayer.Clip)
{
LogSystem.LogError($"音乐资源 {musicName} 未找到或加载失败!");
return;
}
_musicPlayer.MusicName = musicName;
_musicPlayer.IsLoop = isLoop;
_musicPlayer.Length = _musicPlayer.Clip.length;
_musicPlayer.FadeInDuration = fadeIn;
_musicPlayer.FadeOutDuration = fadeOut;
if (isContinue && _musicRecord.ContainsKey(musicName)) _musicPlayer.Play(_musicRecord[musicName]);
else _musicPlayer.Play();
}
public void StopMusic()
{
if (_musicPlayer == null) return;
if (_musicPlayer.Stop())
{
// 确保记录的时间不超过音频长度
float currentTime = _musicPlayer.Source.time + _musicPlayer.FadeOutDuration;
float maxTime = _musicPlayer.Length - 0.5f;
_musicRecord[_musicPlayer.MusicName] = Mathf.Clamp(currentTime, 0, Mathf.Max(0, maxTime));
}
}
#region A
/// <summary>
/// 延迟播放音乐等待上一首FadeOut完成后播放
/// </summary>
public void PlayMusicDelayed(string musicName, float fadeIn, float fadeOut, bool isLoop, bool isContinue = false, float extraWait = 0f)
{
// 如果有待播放的音乐,立即播放它(避免堆积)
if (!string.IsNullOrEmpty(_pendingMusicName))
{
PlayMusic(_pendingMusicName, _pendingFadeIn, _pendingFadeOut, _pendingIsLoop, _pendingIsContinue);
}
// 如果当前没有在播放的音乐,直接播放
if (_musicPlayer == null || _musicPlayer.State == PlayerState.Finished || _musicPlayer.State == PlayerState.Prepare)
{
PlayMusic(musicName, fadeIn, fadeOut, isLoop, isContinue);
return;
}
// 当前有音乐在播放开始FadeOut并等待
_pendingMusicName = musicName;
_pendingFadeIn = fadeIn;
_pendingFadeOut = fadeOut;
_pendingIsLoop = isLoop;
_pendingIsContinue = isContinue;
// 计算等待时间FadeOut时间 + 额外等待时间
_fadeOutWaitTimer = _musicPlayer.FadeOutDuration + extraWait;
// 开始停止当前音乐触发FadeOut
StopMusic();
}
/// <summary>
/// 更新待播放音乐的状态在Update中调用
/// </summary>
private void UpdatePendingMusic()
{
if (string.IsNullOrEmpty(_pendingMusicName)) return;
_fadeOutWaitTimer -= Time.deltaTime;
// 检查是否等待完成FadeOut结束 + 额外等待时间)
if (_fadeOutWaitTimer <= 0)
{
// 播放待播放的音乐
string musicToPlay = _pendingMusicName;
PlayMusic(_pendingMusicName, _pendingFadeIn, _pendingFadeOut, _pendingIsLoop, _pendingIsContinue);
// 清除待播放状态
_pendingMusicName = null;
_fadeOutWaitTimer = 0f;
}
}
#endregion
#region BGM
/// <summary>
/// 开始 BGM 轮播
/// </summary>
public void StartBgmRotation()
{
if (!ConfigManager.Instance.Config.BgmContinuousPlay) return;
_isPlayingRotation = true;
_gapTimer = 0;
UpdateActivePlayerMusics();
// 如果已经在播放轮播列表中的音乐
if (_musicPlayer != null &&
(_musicPlayer.State == PlayerState.Playing || _musicPlayer.State == PlayerState.FadeIn) &&
_activePlayerMusics.Contains(_musicPlayer.MusicName))
{
_lastPlayedMusicName = _musicPlayer.MusicName;
// 取消状态机的循环, 让 UpdateBgmRotation 在到达 FadeOut 触发点时进 FadeOut。
// 不动 Source.loop: BGM 的 Source.loop 始终为 true (在 Play() 中强制),
// 防止 AudioSource 自然终止, FadeOut 完全由状态机控制。
if (_musicPlayer.IsLoop)
{
_musicPlayer.IsLoop = false;
float currentPlayTime = _musicPlayer.Source?.time ?? 0;
_musicPlayer.StartTime = Time.time - currentPlayTime;
}
return;
}
// 开始轮播时随机选择第一首
_lastPlayedMusicName = null;
PlayNextRotationMusic();
}
/// <summary>
/// 停止 BGM 轮播
/// </summary>
public void StopBgmRotation()
{
_isPlayingRotation = false;
}
/// <summary>
/// 如果配置开启,恢复 BGM 轮播UI 关闭时调用)
/// </summary>
public void ResumeBgmRotationIfEnabled()
{
if (!ConfigManager.Instance.Config.BgmContinuousPlay) return;
_isPlayingRotation = true;
_gapTimer = 0;
UpdateActivePlayerMusics();
// 如果没有播放或已结束,开始播放轮播
if (_musicPlayer == null || _musicPlayer.State == PlayerState.Finished)
{
if (_activePlayerMusics.Count > 0) PlayNextRotationMusic();
return;
}
// 如果当前播放的音乐在轮播列表中
if (_activePlayerMusics.Contains(_musicPlayer.MusicName))
{
// 如果音乐正在FadeOut被StopMusic触发重新播放以接续
if (_musicPlayer.State == PlayerState.FadeOut)
{
string musicName = _musicPlayer.MusicName;
PlayMusic(musicName, 0.5f, 2f, false, true);
return;
}
// 取消状态机的循环, 让 UpdateBgmRotation 在到达 FadeOut 触发点时进 FadeOut。
// 不动 Source.loop: BGM 的 Source.loop 始终为 true (在 Play() 中强制),
// 防止 AudioSource 自然终止, FadeOut 完全由状态机控制。
if (_musicPlayer.IsLoop)
{
_musicPlayer.IsLoop = false;
float currentPlayTime = _musicPlayer.Source?.time ?? 0;
_musicPlayer.StartTime = Time.time - currentPlayTime;
}
return;
}
// 如果播放的是非轮播音乐,停止它并开始轮播
StopMusic();
if (_activePlayerMusics.Count > 0) PlayNextRotationMusic();
}
/// <summary>
/// 更新当前场上所有已遇见玩家的音乐列表
/// </summary>
public void UpdateActivePlayerMusics()
{
if (Main.MapData == null || Main.MapData.PlayerMap == null) return;
_activePlayerMusics.Clear();
var uniqueMusicNames = new HashSet<string>();
var selfPlayer = Main.MapData.PlayerMap.SelfPlayerData;
// 遍历所有已遇见的玩家包括自己自己在MeetPlayers列表中
foreach (var playerId in selfPlayer.MeetPlayers)
{
if (!Main.MapData.PlayerMap.GetPlayerDataByPlayerID(playerId, out var player)) continue;
if (Table.Instance.PlayerDataAssets.GetPlayerInfo(player, out var playerInfo))
{
if (!string.IsNullOrEmpty(playerInfo.MusicName) && _clips.ContainsKey(playerInfo.MusicName))
{
uniqueMusicNames.Add(playerInfo.MusicName);
}
}
}
_activePlayerMusics.AddRange(uniqueMusicNames);
// 随机打乱顺序
ShuffleList(_activePlayerMusics);
}
/// <summary>
/// BGM 轮播更新(在 Update 中调用)
/// </summary>
private void UpdateBgmRotation()
{
if (!_isPlayingRotation) return;
if (!ConfigManager.Instance.Config.BgmContinuousPlay)
{
StopBgmRotation();
return;
}
// 如果有待播放的音乐,等待 UpdatePendingMusic 处理
if (!string.IsNullOrEmpty(_pendingMusicName)) return;
// 如果正在等待两首歌之间的间隔
if (_gapTimer > 0)
{
_gapTimer -= Time.deltaTime;
if (_gapTimer <= 0)
{
_gapTimer = 0;
if (_activePlayerMusics.Count > 0)
PlayNextRotationMusic();
}
return;
}
// 如果当前没有播放或播放完毕,开始间隔计时
if (_musicPlayer == null || _musicPlayer.State == PlayerState.Finished)
{
if (_activePlayerMusics.Count > 0)
_gapTimer = _extraInterval;
return;
}
// 取消状态机的循环, 让 UpdateSourceVolume 在到达 Length-FadeOutDuration 时进 FadeOut。
// 不动 Source.loop: BGM 的 Source.loop 在 Play() 里被强制为 true,
// 防止 AudioSource 到达 Clip 末尾自然终止导致 FadeOut 还没跑完就丢声音。
if (_musicPlayer.State == PlayerState.Playing && _musicPlayer.IsLoop)
{
_musicPlayer.IsLoop = false;
float currentPlayTime = _musicPlayer.Source?.time ?? 0;
_musicPlayer.StartTime = Time.time - currentPlayTime;
}
// 其他状态FadeIn/Playing/FadeOut让音乐自然播放AudioPlayer 状态机会自动处理
}
/// <summary>
/// 播放轮播列表中的下一首音乐
/// </summary>
private void PlayNextRotationMusic()
{
if (_activePlayerMusics.Count == 0)
{
UpdateActivePlayerMusics();
if (_activePlayerMusics.Count == 0) return;
}
// 随机选择下一首音乐
int randomIndex;
if (_activePlayerMusics.Count == 1)
{
randomIndex = 0;
}
else
{
// 避免重复播放同一首
do
{
randomIndex = UnityEngine.Random.Range(0, _activePlayerMusics.Count);
} while (_activePlayerMusics[randomIndex] == _lastPlayedMusicName);
}
_currentMusicIndex = randomIndex;
var musicName = _activePlayerMusics[_currentMusicIndex];
_lastPlayedMusicName = musicName;
// 使用延迟播放如果当前有音乐在FadeOut等待其完成后播放
// extraWait=0因为间隔已由 _gapTimer 管理
PlayMusicDelayed(musicName, 2f, 2f, false, false, 0f);
}
/// <summary>
/// 打乱列表顺序
/// </summary>
private void ShuffleList(List<string> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = UnityEngine.Random.Range(0, i + 1);
var temp = list[i];
list[i] = list[j];
list[j] = temp;
}
}
#endregion
public void PlayAudio(string musicName, float fadeIn = 0f, float fadeOut = 0f, bool isLoop = false)
{
if (!_clips.ContainsKey(musicName)) return;
var player = GetPlayer();
player.IsMusic = false;
player.Clip = _clips[musicName][0];
if (!player.Clip)
{
LogSystem.LogError($"音频资源 {musicName} 未找到或加载失败!");
return;
}
player.MusicName = musicName;
player.IsLoop = isLoop;
player.Length = player.Clip.length;
player.FadeInDuration = fadeIn;
player.FadeOutDuration = fadeOut;
player.Play();
}
private AudioPlayer GetPlayer()
{
// 检查是否有可重用的播放器
foreach (var player in _allPlayer)
{
if (player == _musicPlayer) continue;
if (player.State == PlayerState.Finished || player.State == PlayerState.Prepare)
{
return player;
}
}
_allPlayer.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
// 限制最大同时播放数量
if (_allPlayer.Count >= 16) // FMOD默认最大通道数通常是32这里取一半作为安全值
{
LogSystem.LogInfo("Too many audio players active, trying to reuse oldest one");
var oldestPlayer = _allPlayer[0];
oldestPlayer.Stop();
if (oldestPlayer.Source != null)
{
oldestPlayer.Source.Stop();
oldestPlayer.State = PlayerState.Prepare;
return oldestPlayer;
}
}
var sourceObj = new GameObject();
sourceObj.transform.SetParent(AudioRoot.transform);
var source = sourceObj.AddComponent<AudioSource>();
var newPlayer = new AudioPlayer(source);
_allPlayer.Add(newPlayer);
return newPlayer;
}
//----- InGame部分的音频 -------
public void InGameAudioInit(Main main, MapData map)
{
}
public void CalculateAndPlayAmbient()
{
if (Main.MapData == null) return;
if (Random.Range(0, 100) < 40) return;
//播放环境音效。如果领土内有海洋则60%概率播放海洋。否则如果有forest 播放forest
bool isForest = false;
bool isSea = false;
var player = Main.MapData.CurPlayer;;
if (player == null) return;
_ambientTerritoryGridSet.Clear();
Main.MapData.GetPlayerTerritoryGridIdSet(player.Id, _ambientTerritoryGridSet);
foreach (var g in _ambientTerritoryGridSet)
{
if (!Main.MapData.GridMap.GetGridDataByGid(g, out var grid)) continue;
if (grid.Terrain != TerrainType.Land) isSea = true;
if (grid.Vegetation == Vegetation.Trees) isForest = true;
if (isSea && isForest) break;
}
if(isSea && Random.Range(0, 100) < 60)
PlayAudio("SFX/ENV_sea",1f);
else if(isForest)
PlayAudio("SFX/ENV_forest",1f);
}
public void InGameOnTurnStart()
{
if(Main.MapData.CurPlayer == Main.MapData.PlayerMap.SelfPlayerData)
CalculateAndPlayAmbient();
}
}
public enum PlayerState
{
Prepare,
FadeIn,
FadeOut,
Playing,
Finished,
}
public class AudioPlayer
{
public string MusicName;
public AudioSource Source;
public AudioClip Clip;
public bool IsLoop;
public float Length;
public float FadeInDuration;
public float FadeOutDuration;
public float StartTime;
public float EndTime;
public PlayerState State;
// 播放当帧不能停, 能停标记
public bool FirstFrame;
// 是否是BGM(true=用MusicVolume控制, false=用AudioVolume控制)
// 切歌crossfade期间, 旧BGM player已不是_musicPlayer但仍应受MusicVolume控制
public bool IsMusic;
public AudioPlayer(AudioSource source)
{
Source = source;
State = PlayerState.Prepare;
}
public void Play(float recordTime = 0f)
{
if (Clip == null)
{
State = PlayerState.Finished;
return;
}
State = PlayerState.FadeIn;
FirstFrame = true;
Source.time = 0;
Source.clip = Clip;
Source.volume = 0;
// BGM 强制 Source.loop = true: 防止 AudioSource 在 Source.time 到达 Clip.length
// 时自动停止, 导致状态机的 FadeOut 还没机会跑就丢声音(听感=戛然而止)。
// 状态机的 IsLoop 字段独立控制 FadeOut 触发时机, FadeOut 完成后状态机会调 Source.Stop()。
// 短音效 (IsMusic=false) 走原逻辑, 由 IsLoop 决定是否循环。
Source.loop = IsMusic || IsLoop;
Source.Play();
// 设置播放位置(在 Play 之后)
if (recordTime > 0 && Clip.length > 0)
{
float maxSeekTime = Clip.length - 0.5f;
if (maxSeekTime > 0 && recordTime < maxSeekTime)
{
try
{
Source.time = recordTime;
}
catch (System.Exception e)
{
LogSystem.LogWarning($"AudioPlayer: 设置播放位置失败 - {e.Message}");
}
}
}
}
public void Update(float volumeRatio)
{
if (Source == null) return;
if (FirstFrame) StartTime = Time.time;
volumeRatio = Mathf.Clamp(volumeRatio, 0, 1);
UpdateSourceVolume(volumeRatio);
FirstFrame = false;
}
public bool Stop()
{
if (State == PlayerState.Finished || State == PlayerState.Prepare || State == PlayerState.FadeOut) return false;
State = PlayerState.FadeOut;
EndTime = Time.time;
return true;
}
private void UpdateSourceVolume(float volumeRatio)
{
if (State == PlayerState.Finished || State == PlayerState.Prepare) return;
if (State == PlayerState.FadeIn)
{
if (Time.time - StartTime < FadeInDuration)
{
Source.volume = (Time.time - StartTime) / FadeInDuration * volumeRatio;
}
else
{
Source.volume = 1 * volumeRatio;
State = PlayerState.Playing;
}
}
if (State == PlayerState.Playing && !IsLoop)
{
if (Time.time - StartTime >= Length - FadeOutDuration)
{
EndTime = Time.time;
State = PlayerState.FadeOut;
}
}
if (State == PlayerState.FadeOut && !FirstFrame)
{
if (Time.time - EndTime >= FadeOutDuration)
{
Source.time = 0;
Source.volume = 0;
Source.Stop();
State = PlayerState.Finished;
}
else
{
Source.volume = (FadeOutDuration - Time.time + EndTime) / FadeOutDuration * volumeRatio;
}
}
if (State == PlayerState.Playing) Source.volume = volumeRatio;
}
}
}