本文实现一个音频库的自定义编辑器,效果如图:

开始实现之前,首先简单介绍该音频库模块,音频库类Audio Database继承自Scriptable Object类,是一个可配置的资源文件:

包含的内容如下,databaseName表示该音频库的名称,outputAudioMixerGroup表示音频播放时的输出混音器组,datasets则是表示所有音频数据的列表:

    /// <summary>/// 音频库/// </summary>[CreateAssetMenu(fileName = "New Audio Database", order = 215)]public class AudioDatabase : ScriptableObject{/// <summary>/// 音频库名称/// </summary>public string databaseName;/// <summary>/// 输出混音器组/// </summary>public AudioMixerGroup outputAudioMixerGroup;/// <summary>/// 音频数据列表/// </summary>public List<AudioData> datasets = new List<AudioData>(0);}

AudioData音频数据类包含两个字段:name 表示该音频数据的名称,clip 表示该音频资源:

using System;
using UnityEngine;namespace SK.Framework
{/// <summary>/// 音频数据/// </summary>[Serializable]public class AudioData{public string name;public AudioClip clip;}
}

该编辑器的布局结构:

首先继承自Editor类,使用CustomEditorAttribute,并重写OnInspectorGUI方法以实现自定义编辑器。

音频库名称是一个string类型字段,因此使用EditorGUILayout中的TextField函数来添加一个文本编辑框:

using UnityEditor;
using UnityEngine;[CustomEditor(typeof(AudioDatabase))]
public class AudioDatabaseEditor : Editor
{private AudioDatabase database;private void OnEnable(){database = target as AudioDatabase;}public override void OnInspectorGUI(){//音频库名称var newDatabaseName = EditorGUILayout.TextField("Database Name", database.databaseName);if (newDatabaseName != database.databaseName){Undo.RecordObject(database, "Name");database.databaseName = newDatabaseName;EditorUtility.SetDirty(database);}}
}

其中Undo.RecordObject方法用于实现撤销、恢复操作。即当我们修改音频库名称后,使用Ctrl+Z可以撤销修改的操作,撤销后使用Ctrl+Y可以恢复撤销的内容。EditorUtility类中的SetDirty方法则用于标识该物体已经被修改,以实现资产更新保存。上述这两个方法将会大量用到。

outputAudioMixerGroup使用ObjectField方法来实现赋值和更改,objType参数传入AudioMixerGroup的类型即可:

var newOutputAudioMixerGroup = EditorGUILayout.ObjectField("Output Audio Mixer Group", database.outputAudioMixerGroup, typeof(AudioMixerGroup), false) as AudioMixerGroup;
if (newOutputAudioMixerGroup != database.outputAudioMixerGroup)
{Undo.RecordObject(database, "Output");database.outputAudioMixerGroup = newOutputAudioMixerGroup;EditorUtility.SetDirty(database);
}

折叠栏使用EditorGUILayout类中的BeginFadeGroup和EndFadeGroup方法来实现,可以使用一个bool类型字段来实现简单的折叠,不过我们这里用的是AnimBool,它可以实现折叠时的动画效果,效果如下:(AnimBool的使用在以往的博客中有介绍:Unity编辑器开发之AnimBool)

在折叠栏为打开状态时,遍历音频数据列表,每一项数据添加一个水平布局,从左到右依次添加音频图标、音频名称、一个Button按钮、时长信息、播放、停止、删除按钮。

using UnityEngine;
using UnityEditor;
using UnityEngine.Audio;
using UnityEditor.AnimatedValues;[CustomEditor(typeof(AudioDatabase))]
public class AudioDatabaseEditor : Editor
{private AudioDatabase database;private AnimBool foldout;private void OnEnable(){database = target as AudioDatabase;foldout = new AnimBool(false, Repaint);}public override void OnInspectorGUI(){//音频库名称var newDatabaseName = EditorGUILayout.TextField("Database Name", database.databaseName);if (newDatabaseName != database.databaseName){Undo.RecordObject(database, "Name");database.databaseName = newDatabaseName;EditorUtility.SetDirty(database);}//音频库输出混音器var newOutputAudioMixerGroup = EditorGUILayout.ObjectField("Output Audio Mixer Group", database.outputAudioMixerGroup, typeof(AudioMixerGroup), false) as AudioMixerGroup;if (newOutputAudioMixerGroup != database.outputAudioMixerGroup){Undo.RecordObject(database, "Output");database.outputAudioMixerGroup = newOutputAudioMixerGroup;EditorUtility.SetDirty(database);}//音频数据折叠栏 使用AnimBool实现动画效果foldout.target = EditorGUILayout.Foldout(foldout.target, "Datasets");if (EditorGUILayout.BeginFadeGroup(foldout.faded)){for (int i = 0; i < database.datasets.Count; i++){var data = database.datasets[i];//水平布局GUILayout.BeginHorizontal();GUILayout.EndHorizontal();}}EditorGUILayout.EndFadeGroup();}
}

音频图标使用的是Unity中内置的图标,如何查看Unity中的内置图标在如下链接的博客中有介绍:Unity编辑器开发之GUIIcon 有了图标的名称后,通过EditorGUIUtility类中的IconContent方法进行实现:

//绘制音频图标
GUILayout.Label(EditorGUIUtility.IconContent("SceneViewAudio"), GUILayout.Width(20f));

音频数据的名称为string类型字段,也通过TextField进行实现:

//音频数据名称
var newName = EditorGUILayout.TextField(data.name, GUILayout.Width(120f));
if (newName != data.name)
{Undo.RecordObject(database, "Data Name");data.name = newName;EditorUtility.SetDirty(database);
}

添加Button按钮,点击该按钮后,使用EditorGUIUtility类中的PingObject方法定位该项数据中的音频资源,绘制按钮时使用不同颜色来区分当前项是否为选中的音频数据项,声明一个int类型字段currentIndex,用于表示当前选中项的索引值

//使用音频名称绘制Button按钮 点击后使用PingObject方法定位该音频资源
Color colorCache = GUI.color;
GUI.color = currentIndex == i ? Color.cyan : colorCache;
if (GUILayout.Button(data.clip != null ? data.clip.name : "Null"))
{currentIndex = i;EditorGUIUtility.PingObject(data.clip);
}
GUI.color = colorCache;

播放进度和音频时长均为float类型,我们需要一个将时长转化为00:00时间格式的方法,代码如下:

//将秒数转换为00:00时间格式字符串
private string ToTimeFormat(float time)
{int seconds = (int)time;int minutes = seconds / 60;seconds %= 60;return string.Format("{0:D2}:{1:D2}", minutes, seconds);
}

播放、停止播放及删除按钮的图标用的也均是Unity中的内置图标,分别为PlayButton、PauseButton和Toolbar Minus:

//播放按钮
if (GUILayout.Button(EditorGUIUtility.IconContent("PlayButton"), GUILayout.Width(20f)))
{}
//停止播放按钮
if (GUILayout.Button(EditorGUIUtility.IconContent("PauseButton"), GUILayout.Width(20f)))
{}
//删除按钮 点击后删除该项音频数据
if (GUILayout.Button(EditorGUIUtility.IconContent("Toolbar Minus"), GUILayout.Width(20f)))
{}

我们声明一个字典来存储当前正在播放的音频项,点击播放按钮时,创建一个带有Audio Source组件的物体并用其播放,将其添加到字典中,点击停止播放按钮时,将其从字典移除,并销毁物体,点击删除按钮时,也要判断该项如果正在播放,先要进行移除和销毁,再删除该音频数据项:

private Dictionary<AudioData, AudioSource> players;
//播放按钮
if (GUILayout.Button(EditorGUIUtility.IconContent("PlayButton"), GUILayout.Width(20f)))
{if (!players.ContainsKey(data)){//创建一个物体并添加AudioSource组件 var source = EditorUtility.CreateGameObjectWithHideFlags("Audio Player", HideFlags.HideAndDontSave).AddComponent<AudioSource>();source.clip = data.clip;source.outputAudioMixerGroup = database.outputAudioMixerGroup;source.Play();players.Add(data, source);}
}
//停止播放按钮
if (GUILayout.Button(EditorGUIUtility.IconContent("PauseButton"), GUILayout.Width(20f)))
{if (players.ContainsKey(data)){DestroyImmediate(players[data].gameObject);players.Remove(data);}
}
//删除按钮 点击后删除该项音频数据
if (GUILayout.Button(EditorGUIUtility.IconContent("Toolbar Minus"), GUILayout.Width(20f)))
{Undo.RecordObject(database, "Delete");database.datasets.Remove(data);if (players.ContainsKey(data)){DestroyImmediate(players[data].gameObject);players.Remove(data);}EditorUtility.SetDirty(database);Repaint();
}

最后绘制一个矩形区域,当拖拽AudioClip资源到该区域时,添加音频数据项,使用DragAndDrop类来实现:

//以下代码块中绘制了一个矩形区域,将AudioClip资产拖到该区域则添加一项音频数据
GUILayout.BeginHorizontal();
{GUILayout.Label(GUIContent.none, GUILayout.ExpandWidth(true));Rect lastRect = GUILayoutUtility.GetLastRect();var dropRect = new Rect(lastRect.x + 2f, lastRect.y - 2f, 120f, 20f);bool containsMouse = dropRect.Contains(Event.current.mousePosition);if (containsMouse){switch (Event.current.type){case EventType.DragUpdated:bool containsAudioClip = DragAndDrop.objectReferences.OfType<AudioClip>().Any();DragAndDrop.visualMode = containsAudioClip ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;Event.current.Use();Repaint();break;case EventType.DragPerform:IEnumerable<AudioClip> audioClips = DragAndDrop.objectReferences.OfType<AudioClip>();foreach (var audioClip in audioClips){if (database.datasets.Find(m => m.clip == audioClip) == null){Undo.RecordObject(database, "Add");database.datasets.Add(new AudioData() { name = audioClip.name, clip = audioClip });EditorUtility.SetDirty(database);}}Event.current.Use();Repaint();break;}}Color color = GUI.color;GUI.color = new Color(GUI.color.r, GUI.color.g, GUI.color.b, containsMouse ? .9f : .5f);GUI.Box(dropRect, "Drop AudioClips Here", new GUIStyle(GUI.skin.box) { fontSize = 10 });GUI.color = color;
}
GUILayout.EndHorizontal();

最终代码:

using UnityEngine;
using UnityEditor;
using System.Linq;
using UnityEngine.Audio;
using System.Collections.Generic;
using UnityEditor.AnimatedValues;[CustomEditor(typeof(AudioDatabase))]
public class AudioDatabaseEditor : Editor
{private AudioDatabase database;private AnimBool foldout;private int currentIndex = -1;private Dictionary<AudioData, AudioSource> players;private void OnEnable(){database = target as AudioDatabase;foldout = new AnimBool(false, Repaint);players = new Dictionary<AudioData, AudioSource>();EditorApplication.update += Update;}private void OnDestroy(){EditorApplication.update -= Update;foreach (var player in players){DestroyImmediate(player.Value.gameObject);}players.Clear();}private void Update(){Repaint();foreach (var player in players){if (!player.Value.isPlaying){DestroyImmediate(player.Value.gameObject);players.Remove(player.Key);break;}}}public override void OnInspectorGUI(){//音频库名称var newDatabaseName = EditorGUILayout.TextField("Database Name", database.databaseName);if (newDatabaseName != database.databaseName){Undo.RecordObject(database, "Name");database.databaseName = newDatabaseName;EditorUtility.SetDirty(database);}//音频库输出混音器var newOutputAudioMixerGroup = EditorGUILayout.ObjectField("Output Audio Mixer Group", database.outputAudioMixerGroup, typeof(AudioMixerGroup), false) as AudioMixerGroup;if (newOutputAudioMixerGroup != database.outputAudioMixerGroup){Undo.RecordObject(database, "Output");database.outputAudioMixerGroup = newOutputAudioMixerGroup;EditorUtility.SetDirty(database);}//音频数据折叠栏 使用AnimBool实现动画效果foldout.target = EditorGUILayout.Foldout(foldout.target, "Datasets");if (EditorGUILayout.BeginFadeGroup(foldout.faded)){for (int i = 0; i < database.datasets.Count; i++){var data = database.datasets[i];GUILayout.BeginHorizontal();//绘制音频图标GUILayout.Label(EditorGUIUtility.IconContent("SceneViewAudio"), GUILayout.Width(20f));//音频数据名称var newName = EditorGUILayout.TextField(data.name, GUILayout.Width(120f));if (newName != data.name){Undo.RecordObject(database, "Data Name");data.name = newName;EditorUtility.SetDirty(database);}//使用音频名称绘制Button按钮 点击后使用PingObject方法定位该音频资源Color colorCache = GUI.color;GUI.color = currentIndex == i ? Color.cyan : colorCache;if (GUILayout.Button(data.clip != null ? data.clip.name : "Null")){currentIndex = i;EditorGUIUtility.PingObject(data.clip);}GUI.color = colorCache;//若该音频正在播放 计算其播放进度 string progress = players.ContainsKey(data) ? ToTimeFormat(players[data].time) : "00:00";GUI.color = new Color(GUI.color.r, GUI.color.g, GUI.color.b, players.ContainsKey(data) ? .9f : .5f);//显示信息:播放进度 / 音频时长 (00:00 / 00:00)GUILayout.Label($"({progress} / {(data.clip != null ? ToTimeFormat(data.clip.length) : "00:00")})",new GUIStyle(GUI.skin.label) { alignment = TextAnchor.LowerRight, fontSize = 8, fontStyle = FontStyle.Italic }, GUILayout.Width(60f));GUI.color = colorCache;//播放按钮if (GUILayout.Button(EditorGUIUtility.IconContent("PlayButton"), GUILayout.Width(20f))){if (!players.ContainsKey(data)){//创建一个物体并添加AudioSource组件 var source = EditorUtility.CreateGameObjectWithHideFlags("Audio Player", HideFlags.HideAndDontSave).AddComponent<AudioSource>();source.clip = data.clip;source.outputAudioMixerGroup = database.outputAudioMixerGroup;source.Play();players.Add(data, source);}}//停止播放按钮if (GUILayout.Button(EditorGUIUtility.IconContent("PauseButton"), GUILayout.Width(20f))){if (players.ContainsKey(data)){DestroyImmediate(players[data].gameObject);players.Remove(data);}}//删除按钮 点击后删除该项音频数据if (GUILayout.Button(EditorGUIUtility.IconContent("Toolbar Minus"), GUILayout.Width(20f))){Undo.RecordObject(database, "Delete");database.datasets.Remove(data);if (players.ContainsKey(data)){DestroyImmediate(players[data].gameObject);players.Remove(data);}EditorUtility.SetDirty(database);Repaint();}GUILayout.EndHorizontal();}EditorGUILayout.Space();//以下代码块中绘制了一个矩形区域,将AudioClip资产拖到该区域则添加一项音频数据GUILayout.BeginHorizontal();{GUILayout.Label(GUIContent.none, GUILayout.ExpandWidth(true));Rect lastRect = GUILayoutUtility.GetLastRect();var dropRect = new Rect(lastRect.x + 2f, lastRect.y - 2f, 120f, 20f);bool containsMouse = dropRect.Contains(Event.current.mousePosition);if (containsMouse){switch (Event.current.type){case EventType.DragUpdated:bool containsAudioClip = DragAndDrop.objectReferences.OfType<AudioClip>().Any();DragAndDrop.visualMode = containsAudioClip ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;Event.current.Use();Repaint();break;case EventType.DragPerform:IEnumerable<AudioClip> audioClips = DragAndDrop.objectReferences.OfType<AudioClip>();foreach (var audioClip in audioClips){if (database.datasets.Find(m => m.clip == audioClip) == null){Undo.RecordObject(database, "Add");database.datasets.Add(new AudioData() { name = audioClip.name, clip = audioClip });EditorUtility.SetDirty(database);}}Event.current.Use();Repaint();break;}}Color color = GUI.color;GUI.color = new Color(GUI.color.r, GUI.color.g, GUI.color.b, containsMouse ? .9f : .5f);GUI.Box(dropRect, "Drop AudioClips Here", new GUIStyle(GUI.skin.box) { fontSize = 10 });GUI.color = color;}GUILayout.EndHorizontal();}EditorGUILayout.EndFadeGroup();serializedObject.ApplyModifiedProperties();}//将秒数转换为00:00时间格式字符串private string ToTimeFormat(float time){int seconds = (int)time;int minutes = seconds / 60;seconds %= 60;return string.Format("{0:D2}:{1:D2}", minutes, seconds);}
}

Unity 编辑器开发实战【Custom Editor】- AudioDatabase Editor 音频库编辑器相关推荐

  1. 《Unity虚拟现实开发实战》——第1章,第1.8节小结

    本节书摘来自华章出版社<Unity虚拟现实开发实战>一书中的第1章,第1.8节小结,作者[美] 乔纳森·林诺维斯,更多章节内容可以访问云栖社区"华章计算机"公众号查看. ...

  2. 《Unity虚拟现实开发实战》——第3章,第3.6节虚拟现实设备的运行原理

    本节书摘来自华章出版社<Unity虚拟现实开发实战>一书中的第3章,第3.6节虚拟现实设备的运行原理,作者[美] 乔纳森·林诺维斯,更多章节内容可以访问云栖社区"华章计算机&qu ...

  3. unity应用开发实战案例_Unity开发实战游戏教学案例分享

    进行项目实战是快速入门或提升Unity开发的关键.Asset Store资源商店中,有大量完整项目模板和教学案例,帮助您通过项目实战,让你体会到Unity开发的成就感. 本文我们为大家准备了三款实战游 ...

  4. 移动端H5页面编辑器开发实战--经验技巧篇

    一.前言 在上一篇<原理结构篇>中,主要针对移动端网页进行了分类描述,并介绍了H5编辑器的需求.原理以及框架结构,本文将延续开发实战这一主题,针对策略和开发技巧做进一步的介绍. 二.策略篇 ...

  5. unity应用开发实战案例_Unity3D游戏引擎开发实战从入门到精通

    Unity3D游戏引擎开发实战从入门到精通(坦克大战项目实战.NGUI开发.GameObject) 一.Unity3D游戏引擎开发实战从入门到精通是怎么样的一门课程(介绍) 1.1.Unity3D游戏 ...

  6. Unity 编辑器开发实战【Custom Editor】- 为UI视图制作动画编辑器

    为了更方便地为UI视图添加动画,将动画的编辑功能封装在了UI View类中,可以通过编辑器快速的为视图编辑动画.动画分为两种类型,一种是Unity中的Animator动画,该类型直接通过一个字符串类型 ...

  7. Unity 编辑器开发实战【Custom Editor】- FSM Editor

    本文介绍如何为FSM有限状态机模块实现一个自定义编辑器面板,FSM的详细代码在上一篇文章中有介绍,链接地址: 在Unity中构建FSM有限状态机 下面是最终效果: 首先,自定义一个编辑器面板,需要用到 ...

  8. Unity 编辑器开发实战【Editor Window】- BlendShape调试工具

    Skin Mesh Renderer组件编辑器本身包含BlendShape的调试滑动条,但是当数量较多想要重置时较为麻烦,下面介绍的工具添加了这些调试滑动条的同时,增加了一键重置的功能: 代码如下: ...

  9. Unity 编辑器开发实战【Editor Window】- 关于提高Proto通信协议文件生成效率的考虑

    在项目中使用Protobuf作为通信协议时,需要用到protogen.exe程序将.proto文件编译成.cs文件再导入Unity工程中使用: 例如我们创建一个ProtoTest.proto文件: 然 ...

最新文章

  1. CentOS通过 All-in-One 模式安装 KubeSphere技巧
  2. python加载shellcode免杀 简介
  3. 福建信息技术学院计算机系男生宿舍怎么样,广西职业技术学院宿舍怎么样
  4. libevent的两个服务端、客户端示例(C语言)
  5. mos 控制交流_小米智能排插的220V交流电压信号的开关控制电路设计
  6. 最新(2019)斯坦福CS224n深度学习自然语言处理课程(视频+笔记+2017年合集)
  7. 麦克纳姆轮运动特性分析
  8. Android应用程序的Activity启动过程简要介绍和学习计划 .
  9. Windows 10 KB3124200补丁无法安装的临时解决方案
  10. HTML网页设计:四、超链接
  11. H3C 路由器中VLAN隔离
  12. pta计算个人所得税
  13. 对话清华大学周昊,详解IJCAI杰出论文及其背后的故事
  14. C++ 游戏开发(一)图形库EasyX的安装及测试
  15. Unity4.3.1引擎源码编译过程
  16. 计算机如何开启多个用户,电脑上如何打开多个微信账号?
  17. 神经网络和图神经网络,神经网络的图怎么画
  18. mysql查询3个表_mysql如何实现多表查询,三个数据表的联合查询?
  19. torch.randint()
  20. 22年最强Java面试八股文界的“六边形战士”,堪称天花板!

热门文章

  1. Android开发:使用Viewpager模仿驾考宝典试卷答题界面
  2. Win 10 安装手机驱动
  3. 数据结构和算法思维导图
  4. 简单实验用三极管制作单按键开关来控制LED
  5. Station娱乐影音系统
  6. [PlantSimulation]UserInterface(进阶)
  7. dw网页设计期末设计一个网页_《Dreamweaver网页设计》期末试卷
  8. 主机托管与云计算的共生
  9. 一个企业固定资产折旧案例
  10. Android Studio nativeLibraryDirectories=[/data/app/com.lukouapp-1/lib/arm64, /vendor/lib64, /syste