yyl
2025-11-27 7c76f6a3b938adac52d0337259fa5b603da2ec66
125 战斗 新增音效
4个文件已修改
3个文件已添加
476 ■■■■■ 已修改文件
Main/Component/UI/Effect/BattleEffectPlayer.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleEffectMgr.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/BattleField.cs 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleManager.cs 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Sound.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Sound/BattleSoundManager.cs 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/Sound/BattleSoundManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Component/UI/Effect/BattleEffectPlayer.cs
@@ -102,6 +102,8 @@
    public GameObjectPoolManager.GameObjectPool pool;
    public BattleField battleField;
    public Action onComplete;
    private bool isPlaying = false;
@@ -343,6 +345,9 @@
        {
            PlayUnityEffect();
        }
        battleField?.soundManager.PlayEffectSound(effectConfig.audio);
        OnAlphaChanged();
    }
@@ -518,7 +523,7 @@
    //  创建后的特效会自动隐藏 需要手动调用Play才能播放
    public static BattleEffectPlayer Create(int effectId, Transform parent, bool isRedCamp)
    public static BattleEffectPlayer Create(int effectId, BattleField _battleField, Transform parent, bool isRedCamp)
    {
        // 直接创建特效播放器,不使用对象池
        BattleEffectPlayer battleEffectPlayer = null;
@@ -526,6 +531,7 @@
        GameObject newGo = new GameObject("BattleEffectPlayer_" + effectId);
        newGo.transform.SetParent(parent, false);
        battleEffectPlayer = newGo.AddComponent<BattleEffectPlayer>();
        battleEffectPlayer.battleField = _battleField;
        battleEffectPlayer.rectTrans = newGo.AddMissingComponent<RectTransform>();
        
        battleEffectPlayer.effectId = effectId;
Main/System/Battle/BattleEffectMgr.cs
@@ -84,7 +84,7 @@
        bool isRedCamp = camp == BattleCamp.Red;
        BattleEffectPlayer effectPlayer = BattleEffectPlayer.Create(effectId, battleField.battleRootNode.transform, isRedCamp);
        BattleEffectPlayer effectPlayer = BattleEffectPlayer.Create(effectId, battleField, battleField.battleRootNode.transform, isRedCamp);
        // 设置特效缩放和方向
        effectPlayer.transform.position = parent.position;
Main/System/Battle/BattleField/BattleField.cs
@@ -13,6 +13,8 @@
    public Action<float> OnSpeedRatioChange; // 添加战斗速度变化的回调
    public Action<bool> OnFocusChange;
    public Action OnBattleRun; // 添加战斗运行时的回调
    public BattleObjMgr battleObjMgr;
@@ -56,6 +58,8 @@
        }
    }
    protected bool isFocus = false;
    public BattleRootNode battleRootNode;
    private BattleMode battleMode;
    public event Action<BattleMode> ChangeBattleModeEvent;
@@ -67,6 +71,8 @@
    // 记录正在处理死亡的角色ID,防止重复处理
    private HashSet<uint> processingDeathObjIds = new HashSet<uint>();
    public BattleSoundManager soundManager;
#if UNITY_EDITOR
    public static Dictionary<string, string> battleHpRecorder = new Dictionary<string, string>();
@@ -85,6 +91,7 @@
        battleEffectMgr = new BattleEffectMgr();
        battleTweenMgr = new BattleTweenMgr();
        recordPlayer = new RecordPlayer();
        soundManager = new BattleSoundManager(this);
        processingDeathObjIds = new HashSet<uint>();
    }
@@ -519,6 +526,8 @@
        //         adjuster.SetParentCanvas(canvas);
        //     }
        // }
        BattleManager.Instance.FocusBattleField(this);
    }
    public void StartBattle(Action onMoveComplete)
@@ -730,4 +739,15 @@
    }
    public void SetFocus(bool v)
    {
        isFocus = v;
        OnFocusChange?.Invoke(isFocus);
    }
    public bool IsFocus()
    {
        return isFocus;
    }
}
Main/System/Battle/BattleManager.cs
@@ -681,4 +681,38 @@
        }
    }
    public void FocusBattleField(BattleField battleField)
    {
        foreach (var kvp in battleFields)
        {
            BattleField bf = kvp.Value;
            if (bf == null)
                continue;
            if (bf == battleField)
            {
                bf.SetFocus(true);
            }
            else
            {
                bf.SetFocus(false);
            }
        }
    }
    public BattleField GetFocusBattleField()
    {
        foreach (var kvp in battleFields)
        {
            BattleField bf = kvp.Value;
            if (bf == null)
                continue;
            if (bf.IsFocus())
            {
                return bf;
            }
        }
        return null;
    }
}
Main/System/Battle/Sound.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: afe4364444c3cfd4389d3921d2266dec
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/Sound/BattleSoundManager.cs
New file
@@ -0,0 +1,393 @@
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// 战斗音效管理器
/// 职责:播放战斗中的短促技能动作和特效音效
/// </summary>
public class BattleSoundManager
{
    private BattleField battleField;
    private GameObject audioSourceObject;
    // 音效池配置
    private const int INITIAL_AUDIO_POOL = 15; // 初始池大小
    private const int MAX_AUDIO_SOURCES = 30; // AudioSource 总数上限
    private Queue<AudioSource> audioSourcePool = new Queue<AudioSource>();
    private List<AudioSource> activeAudioSources = new List<AudioSource>();
    // 同一音效同时播放的最大数量
    private const int MAX_SAME_AUDIO_COUNT = 3;
    // 记录每个音效ID当前播放的AudioSource
    private Dictionary<int, List<AudioSource>> audioIdToSources = new Dictionary<int, List<AudioSource>>();
    // 音频剪辑缓存
    private Dictionary<int, AudioClip> audioClipCache = new Dictionary<int, AudioClip>();
    // 当前播放速度
    private float currentSpeedRatio = 1f;
    // 是否有焦点
    private bool hasFocus = true;
    public BattleSoundManager(BattleField _battleField)
    {
        battleField = _battleField;
        InitializeAudioSources();
        // 监听战场速度变化
        if (battleField != null)
        {
            battleField.OnSpeedRatioChange += OnSpeedRatioChanged;
            battleField.OnFocusChange += OnFocusChanged;
            currentSpeedRatio = battleField.speedRatio;
            hasFocus = battleField.IsFocus();
        }
    }
    /// <summary>
    /// 初始化音频源池
    /// </summary>
    private void InitializeAudioSources()
    {
        // 使用战场根节点作为 AudioSource 的载体
        audioSourceObject = battleField.battleRootNode.gameObject;
        // 创建初始音频源池
        for (int i = 0; i < INITIAL_AUDIO_POOL; i++)
        {
            var source = audioSourceObject.AddComponent<AudioSource>();
            source.playOnAwake = false;
            source.loop = false;
            source.spatialBlend = 0f; // 2D音效
            audioSourcePool.Enqueue(source);
        }
    }
    /// <summary>
    /// 每帧运行,清理已完成的音频源
    /// </summary>
    public void Run()
    {
        // 每帧清理,性能开销很小
        CleanupFinishedAudioSources();
    }
    /// <summary>
    /// 预热音频(暂时为空,留给移动端优化)
    /// </summary>
    public void PrewarmAudio(params int[] audioIds)
    {
        // TODO: 移动端优化时实现
    }
    /// <summary>
    /// 预热音频(暂时为空,留给移动端优化)
    /// </summary>
    public void PrewarmAudio(List<int> audioIds)
    {
        // TODO: 移动端优化时实现
    }
    /// <summary>
    /// 播放技能音效
    /// </summary>
    /// <param name="audioId">音效ID</param>
    public void PlaySkillSound(int audioId)
    {
        if (audioId <= 0)
        {
            return;
        }
        PlaySound(audioId);
    }
    /// <summary>
    /// 播放特效音效
    /// </summary>
    /// <param name="audioId">音效ID</param>
    public void PlayEffectSound(int audioId)
    {
        if (audioId <= 0)
        {
            return;
        }
        PlaySound(audioId);
    }
    /// <summary>
    /// 核心播放方法
    /// </summary>
    private void PlaySound(int audioId)
    {
        // 检查是否有焦点,无焦点时不播放
        if (!hasFocus)
        {
            return;
        }
        // 检查该音效是否已达到播放上限
        if (!CanPlayAudio(audioId))
        {
            return;
        }
        var audioClip = GetAudioClip(audioId);
        if (audioClip == null)
        {
            return;
        }
        // 从池中获取可用的音频源
        AudioSource source = GetAvailableAudioSource();
        if (source == null)
        {
            return;
        }
        // 设置音量(使用音效音量设置)
        source.volume = SystemSetting.Instance.GetSoundEffect();
        // 设置播放速度,使用pitch来控制
        // pitch范围建议在0.5-2.0之间以避免失真
        float pitch = Mathf.Clamp(currentSpeedRatio, 0.5f, 2.0f);
        source.pitch = pitch;
        // 播放音效
        source.PlayOneShot(audioClip);
        // 标记为活跃
        if (!activeAudioSources.Contains(source))
        {
            activeAudioSources.Add(source);
        }
        // 记录该音效ID与AudioSource的关联
        if (!audioIdToSources.ContainsKey(audioId))
        {
            audioIdToSources[audioId] = new List<AudioSource>();
        }
        audioIdToSources[audioId].Add(source);
    }
    /// <summary>
    /// 获取音频剪辑(优先从缓存获取)
    /// </summary>
    private AudioClip GetAudioClip(int audioId)
    {
        // 先从缓存中查找
        if (audioClipCache.TryGetValue(audioId, out AudioClip cachedClip))
        {
            return cachedClip;
        }
        // 缓存中没有,则加载并缓存
        var audioClip = LoadAudioClip(audioId);
        if (audioClip != null)
        {
            audioClipCache[audioId] = audioClip;
        }
        return audioClip;
    }
    /// <summary>
    /// 检查是否可以播放该音效
    /// </summary>
    private bool CanPlayAudio(int audioId)
    {
        if (!audioIdToSources.ContainsKey(audioId))
        {
            return true;
        }
        // 过滤掉已经停止播放的AudioSource
        var sources = audioIdToSources[audioId];
        sources.RemoveAll(s => s == null || !s.isPlaying);
        // 检查同时播放的数量
        return sources.Count < MAX_SAME_AUDIO_COUNT;
    }
    /// <summary>
    /// 加载音频剪辑
    /// </summary>
    private AudioClip LoadAudioClip(int audioId)
    {
        var config = AudioConfig.Get(audioId);
        if (config != null)
        {
            return AudioLoader.LoadAudio(config.Folder, config.Audio);
        }
        return null;
    }
    /// <summary>
    /// 获取可用的音频源
    /// </summary>
    private AudioSource GetAvailableAudioSource()
    {
        // 尝试从池中获取
        if (audioSourcePool.Count > 0)
        {
            return audioSourcePool.Dequeue();
        }
        // 如果池为空,检查是否可以动态创建
        if (audioSourceObject == null)
        {
            Debug.LogError("BattleSoundManager: audioSourceObject 为空,无法创建 AudioSource");
            return null;
        }
        // 计算当前总的 AudioSource 数量
        int totalAudioSources = audioSourceObject.GetComponents<AudioSource>().Length;
        if (totalAudioSources >= MAX_AUDIO_SOURCES)
        {
            // 达到上限,不再创建,丢弃这次播放请求
            Debug.LogWarning($"BattleSoundManager: AudioSource 数量已达上限 {MAX_AUDIO_SOURCES},无法播放新音效");
            return null;
        }
        // 在 battleRootNode 上动态创建新的 AudioSource
        var source = audioSourceObject.AddComponent<AudioSource>();
        source.playOnAwake = false;
        source.loop = false;
        source.spatialBlend = 0f; // 2D音效
        return source;
    }
    /// <summary>
    /// 清理已播放完成的音频源
    /// </summary>
    private void CleanupFinishedAudioSources()
    {
        // 清理 activeAudioSources 列表
        for (int i = activeAudioSources.Count - 1; i >= 0; i--)
        {
            var source = activeAudioSources[i];
            if (source == null || !source.isPlaying)
            {
                activeAudioSources.RemoveAt(i);
                if (source != null)
                {
                    audioSourcePool.Enqueue(source);
                }
            }
        }
        // 清理 audioIdToSources 中已停止播放的 AudioSource
        foreach (var kvp in audioIdToSources)
        {
            kvp.Value.RemoveAll(s => s == null || !s.isPlaying);
        }
    }
    /// <summary>
    /// 战场速度变化回调
    /// </summary>
    private void OnSpeedRatioChanged(float newSpeedRatio)
    {
        currentSpeedRatio = newSpeedRatio;
        // 更新所有正在播放的音效的速度
        foreach (var source in activeAudioSources)
        {
            if (source != null && source.isPlaying)
            {
                float pitch = Mathf.Clamp(newSpeedRatio, 0.5f, 2.0f);
                source.pitch = pitch;
            }
        }
    }
    /// <summary>
    /// 焦点变化回调
    /// </summary>
    private void OnFocusChanged(bool isFocus)
    {
        hasFocus = isFocus;
        // 失去焦点时,停止所有正在播放的音效
        if (!hasFocus)
        {
            StopAllSounds();
        }
    }
    /// <summary>
    /// 停止所有音效
    /// </summary>
    public void StopAllSounds()
    {
        foreach (var source in activeAudioSources)
        {
            if (source != null && source.isPlaying)
            {
                source.Stop();
            }
        }
        // 清理并回收所有音频源
        foreach (var source in activeAudioSources)
        {
            if (source != null)
            {
                audioSourcePool.Enqueue(source);
            }
        }
        activeAudioSources.Clear();
        audioIdToSources.Clear();
    }
    /// <summary>
    /// 设置音量
    /// </summary>
    public void SetVolume(float volume)
    {
        volume = Mathf.Clamp01(volume);
        foreach (var source in activeAudioSources)
        {
            if (source != null)
            {
                source.volume = volume;
            }
        }
    }
    /// <summary>
    /// 释放资源
    /// </summary>
    public void Release()
    {
        if (battleField != null)
        {
            battleField.OnSpeedRatioChange -= OnSpeedRatioChanged;
            battleField.OnFocusChange -= OnFocusChanged;
        }
        StopAllSounds();
        // 销毁所有 AudioSource 组件
        if (audioSourceObject != null)
        {
            var sources = audioSourceObject.GetComponents<AudioSource>();
            foreach (var source in sources)
            {
                if (source != null)
                {
                    GameObject.Destroy(source);
                }
            }
            audioSourceObject = null;
        }
        audioSourcePool.Clear();
        activeAudioSources.Clear();
        audioIdToSources.Clear();
        audioClipCache.Clear();
    }
}
Main/System/Battle/Sound/BattleSoundManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 06f271d8301f0a14aa83f97a1728166a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant: