713 lines
27 KiB
C#
713 lines
27 KiB
C#
/*
|
||
* @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";
|
||
path["SFX/UI_message"] = "Audio/SFX/UI_message";
|
||
path["SFX/UI_playerenter"] = "Audio/SFX/UI_playerenter";
|
||
path["SFX/UI_beep"] = "Audio/SFX/UI_beep";
|
||
|
||
|
||
|
||
foreach (var kv in path)
|
||
{
|
||
_clips[kv.Key] = new List<AudioClip>();
|
||
_clips[kv.Key].Add(TH1Resource.ResourceLoader.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;
|
||
}
|
||
}
|
||
}
|