U3D客户端框架之 音效管理器 与 Fmod介绍安装导入Unity

news2024/11/20 21:36:52

一、Fmod介绍与安装导入Unity

1.Fmod与Unity内置Audio播放器对比

Unity内置的Audio底层使用的是FMOD,但是功能不够齐全,高级一点的功能如混合(Mix)等无法使用;
音效管理应该和Unity工程解耦合,这样子可以减轻音效设计师的负担;
使用FMOD后,游戏中我们只需要关心sound event名字就可以了,对具体音效资源不会产生依赖;
目前FMOD支持Windows, Mac OSX, Android, iOS,其实官方文档中说了对XBOX One,PlayStation系统等系统都有支持;
结合FMOD Studio的官方文档,我们可以总结出使用FMOD的如下优点
1).使用FMOD我们可以使用更少的资源创建更加高级和丰富的音效,减少运行时内存资源消耗;
2).音效管理只需要在FMOD Studio中管理好即可,不需关心具体Unity工程,方便音效管理;
3).编程人员只需要依赖于各种字符串形式的Sound Event和简单的播放API即可,使用简单;
4).平台支持较为完善。说的直白点就是功能更强大,占用内存更少。

Fmod的下载与工程创建

FMOD的使用过程比较简单,复杂之处在于FMOD Studio的使用,音效资源编辑完成后的使用较为简单。所以这篇文章只讲Fmod如何使用,FmodStudio如何编辑音效会专门写一篇文章来讲。

FMOD Studio部分:

1.下载Fmod Studio

Fmod官网下载:https://www.fmod.com/download

  1. 打开FMOD Studio即创建一个新工程,Ctrl + S 确定工程保存位置;
    在这里插入图片描述

3.Window->Audio Bin打开Audio Bin窗口,用于选择工程需要的声音文件,File->Import Audio Files选择工程需要的声音文件;

  1. FMOD Studio中左侧面板Event Tab栏中,右击选择New Event,表示创建一个新的音效(下图使用的是Fmod 官方的Demo工程);
    在这里插入图片描述

5.将Audio bin面板中的将音效文件拖到FMOD Studio的Character文件夹中,会让选择事件的类型,选择完成之后,则会创建一个事件 如下图所示,拖入了一个声音资源进去就自动创建了一个事件:
在这里插入图片描述

6.右击声音事件 Assign to Bank -> Browse -> Music Bank,指定该事件打包后的所属Bank。
在这里插入图片描述
7. Ctrl + S,File->Build All Platforms,然后File->Export GUIDs。

Unity部分

1.去Unity商店,搜索Fmod For Unity 添加至我的资源后在unity包管理器中进行下载,下载完成后会自动导入Unity,此时菜单栏会多出一个FMOD选项;

  1. FMOD->Import Banks,打开刚刚FMOD Studio创建的工程的Build目录,选择工程,Import进来;
    在这里插入图片描述
  2. 选择Main Camera,然后搜索添加Component->Scripts->FMOD Listener组件,必须要添加这个组件,否则听不到声音。
    Fmod Studio下载、工程创建和Fmod的导入Unity到这里就结束了,下面是声音管理器的设计。



二、声音管理器(AudioManager)设计

声音管理器是对Fmod层的封装,为了更便捷的的使用,为了使业务逻辑与原生Fmod API之间解开耦合,后续API变动不会影响业务逻辑。

UML静态视图

在这里插入图片描述

三、声音管理器代码实现

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections.Generic;

using FMOD.Studio;
using FMODUnity;
using UnityEngine;
using YouYou.DataTable;
using YouYou;

namespace Myh
{
    //声音管理器
    public class AudioManager : ManagerBase, IDisposable
    {
        //释放间隔
        //120秒监测一次,把链表中的状态是停止状态的音乐从链表中删除,并且让音效实例停止,并且释放实例
        private int m_ReleaseInterval = 120;

        //下次释放的时间
        private float m_NextReleaseTime = 0;

        //序号
        private int m_Serial = 0;

        //音效字典
        private Dictionary<int, EventInstance> m_DicCurrAudioEvents = new Dictionary<int, EventInstance>();

        //需要释放的音效编号
        private LinkedList<int> m_NeedRemoveList = new LinkedList<int>();

        //BGM
        //当前BGM的名字
        private string m_CurrBGMAudio;

        //当前BGM音量
        private float m_CurrBGMVolume;

        //当前BGM的最大音量
        private float m_CurrBGMMaxVolume;

        //当前BGM的Fmod Instance
        private EventInstance BGMEventInstance;

        //当前BGM的定时器,用来控制音量
        private TimeAction m_CurrBGMTimeAction;


        public AudioManager()
        {
            //下次释放时间
            m_NextReleaseTime = Time.time;
        }


        public override void Init()
        {
            m_ReleaseInterval = GameEntry.ParamsSetting.GetGradeParamData(ConstDefine.AudioAssetBundlePath, GameEntry.CurrDeviceGrade);
        }

        public void LoadBanks(BaseAction onComplete)
        {
#if DISABLE_ASSETBUNDLE && UNITY_EDITOR
             //编辑器模式加载
            string[] arr = Directory.GetFiles(Application.dataPath+"/Download/Audio","*.bytes");
            int len = arr.Length;
            for (int i = 0; i < len; ++i)
            {
                //根据路径拿到文件信息
                FileInfo file = new FileInfo(arr[i]);
                TextAsset asset = UnityEditor.AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/Download/Audio/"+file.Name);
                RuntimeManager.LoadBank(asset);
            }
            onComplete?.Invoke();
#else
            GameEntry.Resource.ResLoaderManager.LoadAssetBundle(ConstDefine.AudioAssetBundlePath, onComplete: (AssetBundle bundle) =>
              {
                  TextAsset[] arr = bundle.LoadAllAssets<TextAsset>();

                  int len = arr.Length;
                  for (int i = 0; i < len; ++i)
                  {
                      //加载bank
                      RuntimeManager.LoadBank(arr[i]);
                  }

                  //通知上层
                  onComplete?.Invoke();
              });
#endif
        }

        //设置BGM音量
        public void SetBGMVolume(float value)
        {
            BGMEventInstance.setVolume(value);
        }

        //暂停BGM
        public void PauseBGM(bool pause)
        {
            if (!BGMEventInstance.isValid())        //bgm事件实例无效
            {
                CheckBGMEventInstance();            //重播
            }

            if (BGMEventInstance.isValid())         //有效
            {
                BGMEventInstance.setPaused(pause);
            }
        }

        //播放BGM
        public void StopBGM()
        {
            if (BGMEventInstance.isValid())
            {
                //把音量变成0,再停止
                m_CurrBGMTimeAction = GameEntry.Time.CreateTimeAction();
                m_CurrBGMTimeAction.Init(null, 0, 0.05f, 100, null, (int loop) =>
                     {
                         m_CurrBGMVolume -= 0.1f;
                         m_CurrBGMVolume = Mathf.Max(m_CurrBGMVolume, 0);
                         SetBGMVolume(m_CurrBGMVolume);
                         if (m_CurrBGMVolume == 0)
                         {
                             m_CurrBGMTimeAction.Stop();
                             //BGMEventInstance.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
                         }
                     },
                     () =>
                     {
                         BGMEventInstance.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
                     }).Run();
            }
        }

        /// <summary>
        /// BGM切换参数
        /// </summary>
        /// <param name="newEvent">事件名</param>
        /// <param name="value">参数类型</param>
        public void BGMSwitch(string newEvent, float value)
        {
            BGMEventInstance.setParameterByName(newEvent, value);
        }

        //检查BGM实例,如果存在,停止之前的BGM,淡出新BGM
        private void CheckBGMEventInstance()
        {
            if (!string.IsNullOrEmpty(m_CurrBGMAudio))
            {
                //立即停止
                if (BGMEventInstance.isValid())
                {
                    BGMEventInstance.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
                    BGMEventInstance.release();
                }

                //使用事件播放新的实例
                BGMEventInstance = RuntimeManager.CreateInstance(m_CurrBGMAudio);

                m_CurrBGMVolume = 0f;

                SetBGMVolume(m_CurrBGMVolume);

                BGMEventInstance.start();

                //声音从0-max,逐渐变大
                m_CurrBGMTimeAction = GameEntry.Time.CreateTimeAction();
                m_CurrBGMTimeAction.Init(null, 0, 05f, 100, null, (int loop) =>
                     {
                         m_CurrBGMVolume += 0.1f;
                         m_CurrBGMVolume = Mathf.Min(m_CurrBGMMaxVolume, m_CurrBGMVolume);
                         SetBGMVolume(m_CurrBGMVolume);

                         //声音到最大了
                         if (m_CurrBGMVolume == m_CurrBGMMaxVolume)
                         {
                             m_CurrBGMTimeAction.Stop();
                         }
                     }, null).Run();
            }
        }

        //播放BGM
        public void PlayBGM(string bgmEvent, float volume = 1f)
        {
            m_CurrBGMAudio = bgmEvent;
            m_CurrBGMMaxVolume = volume;
            CheckBGMEventInstance();
        }

        // 开始播放BGM
        public void StartBGM()
        {
            BGMEventInstance.start();
        }


        public void OnUpdate()
        {
            if (Time.time > m_NextReleaseTime + m_ReleaseInterval)
            {
                m_NextReleaseTime = Time.time;
                Release();
            }
        }

        /// <summary>
        /// 播放音效
        /// </summary>
        /// <param name="eventPath">声音事件</param>
        /// <param name="volume">音量</param>
        /// <param name="parameterName">参数名称</param>
        /// <param name="value">参数值</param>
        /// <param name="is3D">是否3D</param>
        /// <param name="pos3D">3D位置</param>
        /// <returns>音效实例编号</returns>
        public int PlayAudio(string eventPath, float volume = 1f, string parameterName = null, float value = 0f,
                             bool is3D = false, Vector3 pos3D = default(Vector3))
        {
            if (string.IsNullOrEmpty(eventPath))
                return -1;

            EventInstance instance = RuntimeManager.CreateInstance(eventPath);

            //设置该事件的参数和值
            if (!string.IsNullOrEmpty(parameterName))
            {
                instance.setParameterByName(parameterName, value);
            }

            if (is3D)
            {
                //设置3d属性
                instance.set3DAttributes(pos3D.To3DAttributes());
            }

            instance.start();

            int serialId = ++m_Serial;

            m_DicCurrAudioEvents[serialId] = instance;

            return serialId;
        }

        //播放音效
        public int PlayAudio(int audioId, string paramName = null, float value = 0f, Vector3 pos3D = default(Vector3))
        {
            if (GameEntry.Procedure != null &&
                (int)GameEntry.Procedure.CurrProcedureState <= (int)ProcedureState.Preload)
            {
                return -1;
            }

            DTSys_Audio? entity = GameEntry.DataTable.Sys_AudioList.GetEntity(audioId);
            if (entity != null)
            {
                DTSys_Audio sys_Audio = entity.Value;
                return PlayAudio(sys_Audio.AssetPath, sys_Audio.Volume, paramName, value, sys_Audio.Is3D == 1, pos3D);
            }
            else
            {
                GameEntry.LogError("Audio不存在Id={0}", audioId);
                return -1;
            }
        }

        //设置音效参数
        public void SetParameterForAudio(int serialId, string paramName, float value)
        {
            EventInstance instance;
            if (m_DicCurrAudioEvents.TryGetValue(serialId, out instance))
            {
                if (instance.isValid())
                {
                    instance.setParameterByName(paramName, value);
                }
            }
        }


        /// <summary>
        /// 暂停某个音效
        /// </summary>
        /// <param name="serialId">音效实例编号</param>
        /// <param name="paused">是否暂停</param>
        public bool PausedAudio(int serialId, bool paused = true)
        {
            EventInstance eventInstance;
            if (m_DicCurrAudioEvents.TryGetValue(serialId, out eventInstance))
            {
                if (eventInstance.isValid())
                {
                    return eventInstance.setPaused(paused) == FMOD.RESULT.OK;
                }
            }
            return false;
        }

        /// <summary>
        /// 停止某个音效
        /// </summary>
        /// <param name="serialId">音效实例编号</param>
        public bool StopAudio(int serialId, FMOD.Studio.STOP_MODE mode = FMOD.Studio.STOP_MODE.IMMEDIATE)
        {
            EventInstance eventInstance;
            if (m_DicCurrAudioEvents.TryGetValue(serialId, out eventInstance))
            {
                if (eventInstance.isValid())
                {
                    var result = eventInstance.stop(mode);
                    eventInstance.release();
                    m_DicCurrAudioEvents.Remove(serialId);
                    return result == FMOD.RESULT.OK;
                }
            }
            return false;
        }

        public void StopAllAudio()
        {
            IEnumerator<KeyValuePair<int, EventInstance>> iter = m_DicCurrAudioEvents.GetEnumerator();
            while (iter.MoveNext())
            {
                EventInstance instance = iter.Current.Value;
                instance.release();
            }
            m_DicCurrAudioEvents.Clear();
        }

        private void Release()
        {
            LinkedListNode<int> iter = m_NeedRemoveList.First;
            while (iter != null)
            {
                LinkedListNode<int> next = iter.Next;
                int serialId = iter.Value;
                m_DicCurrAudioEvents.Remove(serialId);
                m_NeedRemoveList.Remove(iter);
                iter = next;
            }
        }

        public void Dispose()
        {
        }

    }
}

四、测试代码

经过测试一切正常

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Myh;

class TestAudio : ITest
{
    public void OnTestStart()
    {
        //throw new NotImplementedException();
    }

    public void OnTestUpdate()
    {
        /*
        //BGM
        //开始
        if (Input.GetKeyDown(KeyCode.Q))
        {
            //GameEntry.
            GameEntry.Audio.PlayBGM("event:/BackGround/Audio_Bg_ChangAn", 1f);
        }
        //暂停
        else if (Input.GetKeyDown(KeyCode.W))
        {
            GameEntry.Audio.PauseBGM(true);
        }
        //继续
        else if (Input.GetKeyDown(KeyCode.E))
        {
            GameEntry.Audio.PauseBGM(false);
        }
        else if (Input.GetKeyDown(KeyCode.R))
        {
            GameEntry.Audio.StopBGM();
        }
        else if (Input.GetKeyDown(KeyCode.T))
        {

        }
        //音效
        else if (Input.GetKeyDown(KeyCode.X))
        {
            GameEntry.Audio.PlayAudio("event:/Fight/NvYao_attack1",is3D:true,pos3D:new Vector3(-1,1,2));
        }
        */
    }
}

参考文章:https://blog.csdn.net/zhaoguanghui2012/article/details/50458498

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/159222.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ArcGIS基础实验操作100例--实验86矢量面重叠分析

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 空间分析篇--实验86 矢量面重叠分析 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&am…

初阶指针详解✍

目录1.内存和地址2.指针变量的大小3.指针类型的意义意义1&#xff1a;指针访问权限的大小意义2&#xff1a;指针类型决定指针的步长4.野指针野指针成因如何规避野指针5.指针的运算指针加减整数指针减指针指针的比较运算6.指针与数组的关系7.二级指针1.内存和地址 内存是电脑上特…

2、C语言程序规范

目录 1. 代码缩进 2. 变量、常量命名规范 3. 函数的命名规范 4. #include指令 5. 注释 6. main函数 7.函数返回值 8. 变量赋初值 俗话说&#xff0c;“没有规矩&#xff0c;不成方圆。” 如&#xff1a;第一个程序 #include <stdio.h>void main(){printf("…

基于java Springmvc+mybatis 电影院售票管理系统设计和实现以及文档

基于java Springmvcmybatis 电影院售票管理系统设计和实现以及文档 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留…

vue报错汇总

项目场景&#xff1a; 使用vue报错汇总。 1、项目启动不报错也不成功 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 项目启动时&#xff0c;一直启动不成功&#xff0c;末句提示 98% emitting Copyplugin… 原因分析&#xff1a; 最有可能是因为require或者import了…

系统设计技巧:使用Postgres作为发布/订阅和作业服务器

如果在项目中需要发布/订阅和作业服务器&#xff0c;可以尝试使用 Postgres。它将为您提供大量数据完整性和性能保证&#xff0c;并且不需要您或您的团队学习任何新技术。如果你正在做任何足够复杂的项目&#xff0c;你将需要一个 发布/订阅[1] 服务器来处理事件。本文将向你介…

黑马“兔年限定”春节礼盒准时送达,快来领!

哈咯艾瑞巴蒂&#xff0c;我是播妞前几天一个热搜引起了我的注意# 原来兔年要打384天的工 #看到这标题播妞突然头皮发紧我搜索了一下&#xff0c;原来是......农历癸卯兔年全年共有384天今年的春节是2023年1月22号2024年的春节是2月10号从今年春节到明年的春节算一年由于“闰二…

XCTF:ics-05

测试了所有功能点&#xff0c;大部分没有做出来&#xff0c;只有设备维护中心可以点击 查看源码&#xff0c;发现云平台设备维护中心这里有一个超链接 看到变量传参page&#xff0c;有点像页面文件包含功能&#xff0c;那有可能存在文件包含&#xff0c;测试下&#xff1a; …

ORB-SLAM2 --- LoopClosing::CorrectLoop函数

目录 1.函数作用 2.函数流程 3.code 4.函数解析 4.1 结束局部地图线程、全局BA&#xff0c;为闭环矫正做准备 4.2 根据共视关系更新当前关键帧与其它关键帧之间的连接关系 4.3 通过位姿传播&#xff0c;得到Sim3优化后&#xff0c;与当前帧相连的关键帧的位姿&#xf…

什么是计算机中的高速公路-总线?

文章目录总线是什么&#xff1f;常见总线类型有哪些&#xff1f;总线的串行和并行的区别&#xff1f;数据总线地址总线CPU的寻址能力32位CPU最大支持4G内存&#xff1f;控制总线总线的共享性和独自性系统总线的结构单总线结构双总线结构三总线结构总线传输的四个阶段总线仲裁集…

谷粒商城项目笔记之高级篇(二)

目录1.7 认证服务1.7.1 环境搭建1&#xff09;、创建认证服务微服务2&#xff09;、引入依赖3)、添加相应的域名4&#xff09;、动静分离5&#xff09;、nacos中注册6&#xff09;、配置网关7)、测试访问登录页面8&#xff09;、实现各个页面之间跳转1.7.2 验证码功能1)、验证码…

C++的类介绍(封装特性)

一、类的定义 1.1定义 类是c语言对编程思想的概括深化&#xff0c;其前期的C语言使能面向过程的语言&#xff0c;思想是注重对程序每一步的理解&#xff1b;而面向过程的是C语言之父把生活的类与对象的思想应用于程序设计之中&#xff0c;把程序抽象成一个个对象。 C面向对象…

将时间序列转换为指定的频率并指定填充方法来填充缺失值的DataFrame.asfreq()方法

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 时间序列的插值操作 提升序列中时间密度 DataFrame.asfreq() 选择题 关于以下python代码说法错误的一项是? import pandas as pd myIndexpd.date_range(1/12/2023,periods3,freqT) dfpd.D…

深入了解 LinkedBlockingQueue阻塞队列

1. 前言 今天来逐个方法解析下LinkedBlockingQueue. 其实跟上篇文件深入了解ArrayBlockingQueue 阻塞队列很类似。只不过底层实现不同。其实看名字就能看出来到底依据什么实现的。好了&#xff0c;废话不多说了&#xff0c;接下来让我们开始吧 至于API的使用情况跟ArrayBlockin…

JS垃圾回收

什么是GC GC就是Garbage Collection,程序工作过程中会产生很多垃圾&#xff0c;这些垃圾是程序不用的内存或者是之前用过了&#xff0c;以后不会再用的内存空间&#xff0c;而GC就是负责回收垃圾的。当然也不是所有的语言都会自带GC&#xff0c;比如Java、Python、Javascript等…

电脑修改用户(User)文件夹名称

情景&#xff1a;Windows 11 的用户名与 C 盘&#xff08;系统盘&#xff09;中的文件夹名称不对应&#xff08;可能是由于重装系统导致的&#xff09;&#xff0c;例如我笔记本中系统用户名是 “fly”&#xff0c;但文件夹名称却是“16490”。 Step 1&#xff1a;打开Administ…

智能图像处理:基于边缘去除和迭代式内容矫正的复杂文档图像校正

本文简要介绍ACM MM 2022录用论文“Marior: Margin Removal and Iterative Content Rectification for Document Dewarping in the Wild”的主要工作。该论文针对现有的矫正方法只能在紧密裁剪的文档图像上获得较为理想的矫正效果这一不足&#xff0c;提出了一个新的矫正方法Ma…

基于webrtc多人音视频的研究(一)

众所周知&#xff0c;WebRTC非常适合点对点&#xff08;即一对一&#xff09;的音视频会话。然而&#xff0c;当我们的客户要求超越一对一&#xff0c;即一对多、多对一设置多对多的解决方案或者服务&#xff0c;那么问题就来了&#xff1a;“我们应该采用什么样的架构&#xf…

利用AirTest实现自动安装APK-跳过vivo手机安装验证

利用AirTest实现自动安装APK-跳过vivo手机安装验证 前言 最近在帮测试组看个问题&#xff0c;他们在自动化测试的时候&#xff0c;通过adb install 命令在vivo手机上安装apk的时候出现”外部来源应用&#xff0c;未经vivo安全性和兼容性检测&#xff0c;请谨慎安装“的提示页面…

仅需一行Python代码,为图片上版权!

哈啰&#xff0c;大家好&#xff0c;我是派森酱&#xff0c;一个Python技术爱好者。今天一个朋友跟我吐槽&#xff1a;前段时间&#xff0c;我辛辛苦苦整理的一份XX攻略&#xff0c;分享给自己的一些朋友&#xff0c;结果今天看到有人堂而皇之地拿着这份攻略图片去引流&#xf…