Unity 编辑器开发实战【Custom Editor】- AudioDatabase Editor 音频库编辑器
本文实现一个音频库的自定义编辑器,效果如图:
开始实现之前,首先简单介绍该音频库模块,音频库类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);
-
}
-
}
文章来源: coderz.blog.csdn.net,作者:CoderZ1010,版权归原作者所有,如需转载,请联系作者。
原文链接:coderz.blog.csdn.net/article/details/122511478
- 点赞
- 收藏
- 关注作者
评论(0)