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

简介: Unity 编辑器开发实战【Custom Editor】- AudioDatabase Editor 音频库编辑器

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

image.gif

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

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

/// <summary>/// 音频库/// </summary>    [CreateAssetMenu(fileName="New Audio Database", order=215)]
publicclassAudioDatabase : ScriptableObject    {
/// <summary>/// 音频库名称/// </summary>publicstringdatabaseName;
/// <summary>/// 输出混音器组/// </summary>publicAudioMixerGroupoutputAudioMixerGroup;
/// <summary>/// 音频数据列表/// </summary>publicList<AudioData>datasets=newList<AudioData>(0);
    }

image.gif

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

usingSystem;
usingUnityEngine;
namespaceSK.Framework{
/// <summary>/// 音频数据/// </summary>    [Serializable]
publicclassAudioData    {
publicstringname;
publicAudioClipclip;
    }
}

image.gif

该编辑器的布局结构:

image.gif

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

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

usingUnityEditor;
usingUnityEngine;
[CustomEditor(typeof(AudioDatabase))]
publicclassAudioDatabaseEditor : Editor{
privateAudioDatabasedatabase;
privatevoidOnEnable()
    {
database=targetasAudioDatabase;
    }
publicoverridevoidOnInspectorGUI()
    {
//音频库名称varnewDatabaseName=EditorGUILayout.TextField("Database Name", database.databaseName);
if (newDatabaseName!=database.databaseName)
        {
Undo.RecordObject(database, "Name");
database.databaseName=newDatabaseName;
EditorUtility.SetDirty(database);
        }
    }
}

image.gif

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

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

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

image.gif

折叠栏使用EditorGUILayout类中的BeginFadeGroup和EndFadeGroup方法来实现,可以使用一个bool类型字段来实现简单的折叠,不过我们这里用的是AnimBool,它可以实现折叠时的动画效果,效果如下

image.gif

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

usingUnityEngine;
usingUnityEditor;
usingUnityEngine.Audio;
usingUnityEditor.AnimatedValues;
[CustomEditor(typeof(AudioDatabase))]
publicclassAudioDatabaseEditor : Editor{
privateAudioDatabasedatabase;
privateAnimBoolfoldout;
privatevoidOnEnable()
    {
database=targetasAudioDatabase;
foldout=newAnimBool(false, Repaint);
    }
publicoverridevoidOnInspectorGUI()
    {
//音频库名称varnewDatabaseName=EditorGUILayout.TextField("Database Name", database.databaseName);
if (newDatabaseName!=database.databaseName)
        {
Undo.RecordObject(database, "Name");
database.databaseName=newDatabaseName;
EditorUtility.SetDirty(database);
        }
//音频库输出混音器varnewOutputAudioMixerGroup=EditorGUILayout.ObjectField("Output Audio Mixer Group", database.outputAudioMixerGroup, typeof(AudioMixerGroup), false) asAudioMixerGroup;
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 (inti=0; i<database.datasets.Count; i++)
            {
vardata=database.datasets[i];
//水平布局GUILayout.BeginHorizontal();
GUILayout.EndHorizontal();
            }
        }
EditorGUILayout.EndFadeGroup();
    }
}

image.gif

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

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

image.gif

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

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

image.gif

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

//使用音频名称绘制Button按钮 点击后使用PingObject方法定位该音频资源ColorcolorCache=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;

image.gif

image.gif

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

//将秒数转换为00:00时间格式字符串privatestringToTimeFormat(floattime)
{
intseconds= (int)time;
intminutes=seconds/60;
seconds%=60;
returnstring.Format("{0:D2}:{1:D2}", minutes, seconds);
}

image.gif

播放、停止播放及删除按钮的图标用的也均是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)))
{
}

image.gif

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

privateDictionary<AudioData, AudioSource>players;

image.gif

//播放按钮if (GUILayout.Button(EditorGUIUtility.IconContent("PlayButton"), GUILayout.Width(20f)))
{
if (!players.ContainsKey(data))
    {
//创建一个物体并添加AudioSource组件 varsource=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();
}

image.gif

image.gif

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

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

image.gif

image.gif

最终代码:

usingUnityEngine;
usingUnityEditor;
usingSystem.Linq;
usingUnityEngine.Audio;
usingSystem.Collections.Generic;
usingUnityEditor.AnimatedValues;
[CustomEditor(typeof(AudioDatabase))]
publicclassAudioDatabaseEditor : Editor{
privateAudioDatabasedatabase;
privateAnimBoolfoldout;
privateintcurrentIndex=-1;
privateDictionary<AudioData, AudioSource>players;
privatevoidOnEnable()
    {
database=targetasAudioDatabase;
foldout=newAnimBool(false, Repaint);
players=newDictionary<AudioData, AudioSource>();
EditorApplication.update+=Update;
    }
privatevoidOnDestroy()
    {
EditorApplication.update-=Update;
foreach (varplayerinplayers)
        {
DestroyImmediate(player.Value.gameObject);
        }
players.Clear();
    }
privatevoidUpdate()
    {
Repaint();
foreach (varplayerinplayers)
        {
if (!player.Value.isPlaying)
            {
DestroyImmediate(player.Value.gameObject);
players.Remove(player.Key);
break;
            }
        }
    }
publicoverridevoidOnInspectorGUI()
    {
//音频库名称varnewDatabaseName=EditorGUILayout.TextField("Database Name", database.databaseName);
if (newDatabaseName!=database.databaseName)
        {
Undo.RecordObject(database, "Name");
database.databaseName=newDatabaseName;
EditorUtility.SetDirty(database);
        }
//音频库输出混音器varnewOutputAudioMixerGroup=EditorGUILayout.ObjectField("Output Audio Mixer Group", database.outputAudioMixerGroup, typeof(AudioMixerGroup), false) asAudioMixerGroup;
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 (inti=0; i<database.datasets.Count; i++)
            {
vardata=database.datasets[i];
GUILayout.BeginHorizontal();
//绘制音频图标GUILayout.Label(EditorGUIUtility.IconContent("SceneViewAudio"), GUILayout.Width(20f));
//音频数据名称varnewName=EditorGUILayout.TextField(data.name, GUILayout.Width(120f));
if (newName!=data.name)
                {
Undo.RecordObject(database, "Data Name");
data.name=newName;
EditorUtility.SetDirty(database);
                }
//使用音频名称绘制Button按钮 点击后使用PingObject方法定位该音频资源ColorcolorCache=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;
//若该音频正在播放 计算其播放进度 stringprogress=players.ContainsKey(data) ?ToTimeFormat(players[data].time) : "00:00";
GUI.color=newColor(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")})",
newGUIStyle(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组件 varsource=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));
RectlastRect=GUILayoutUtility.GetLastRect();
vardropRect=newRect(lastRect.x+2f, lastRect.y-2f, 120f, 20f);
boolcontainsMouse=dropRect.Contains(Event.current.mousePosition);
if (containsMouse)
                {
switch (Event.current.type)
                    {
caseEventType.DragUpdated:
boolcontainsAudioClip=DragAndDrop.objectReferences.OfType<AudioClip>().Any();
DragAndDrop.visualMode=containsAudioClip?DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;
Event.current.Use();
Repaint();
break;
caseEventType.DragPerform:
IEnumerable<AudioClip>audioClips=DragAndDrop.objectReferences.OfType<AudioClip>();
foreach (varaudioClipinaudioClips)
                            {
if (database.datasets.Find(m=>m.clip==audioClip) ==null)
                                {
Undo.RecordObject(database, "Add");
database.datasets.Add(newAudioData() { name=audioClip.name, clip=audioClip });
EditorUtility.SetDirty(database);
                                }
                            }
Event.current.Use();
Repaint();
break;
                    }
                }
Colorcolor=GUI.color;
GUI.color=newColor(GUI.color.r, GUI.color.g, GUI.color.b, containsMouse? .9f : .5f);
GUI.Box(dropRect, "Drop AudioClips Here", newGUIStyle(GUI.skin.box) { fontSize=10 });
GUI.color=color;
            }
GUILayout.EndHorizontal();
        }
EditorGUILayout.EndFadeGroup();
serializedObject.ApplyModifiedProperties();
    }
//将秒数转换为00:00时间格式字符串privatestringToTimeFormat(floattime)
    {
intseconds= (int)time;
intminutes=seconds/60;
seconds%=60;
returnstring.Format("{0:D2}:{1:D2}", minutes, seconds);
    }
}

image.gif

目录
相关文章
|
6天前
|
JavaScript 前端开发 API
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
本文介绍了如何在Vue 3项目中使用v-md-editor组件库来创建markdown编辑器和预览组件。文章提供了安装步骤、如何在main.js中进行全局配置、以及如何在页面中使用VMdEditor和VMdPreview组件的示例代码。此外,还提供了一个完整示例的链接,包括编辑器和预览组件的使用效果和代码。
vue3 v-md-editor markdown编辑器(VMdEditor)和预览组件(VMdPreview )的使用
|
2月前
|
JavaScript
基于Vue2.X/Vue3.X对Monaco Editor在线代码编辑器进行封装与使用
这篇文章介绍了如何在Vue 2.X和Vue 3.X项目中封装和使用Monaco Editor在线代码编辑器,包括安装所需依赖、创建封装组件、在父组件中调用以及处理Vue 3中可能遇到的问题。
308 1
基于Vue2.X/Vue3.X对Monaco Editor在线代码编辑器进行封装与使用
|
2月前
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
67 3
|
2月前
|
开发者 图形学 开发工具
Unity编辑器神级扩展攻略:从批量操作到定制Inspector界面,手把手教你编写高效开发工具,解锁编辑器隐藏潜能
【8月更文挑战第31天】Unity是一款强大的游戏开发引擎,支持多平台发布与高度可定制的编辑器环境。通过自定义编辑器工具,开发者能显著提升工作效率。本文介绍如何使用C#脚本扩展Unity编辑器功能,包括批量调整游戏对象位置、创建自定义Inspector界面及项目统计窗口等实用工具,并提供具体示例代码。理解并应用这些技巧,可大幅优化开发流程,提高生产力。
126 1
|
2月前
|
开发者 图形学 Java
揭秘Unity物理引擎核心技术:从刚体动力学到关节连接,全方位教你如何在虚拟世界中重现真实物理现象——含实战代码示例与详细解析
【8月更文挑战第31天】Unity物理引擎对于游戏开发至关重要,它能够模拟真实的物理效果,如刚体运动、碰撞检测及关节连接等。通过Rigidbody和Collider组件,开发者可以轻松实现物体间的互动与碰撞。本文通过具体代码示例介绍了如何使用Unity物理引擎实现物体运动、施加力、使用关节连接以及模拟弹簧效果等功能,帮助开发者提升游戏的真实感与沉浸感。
39 1
|
2月前
|
存储 JavaScript 前端开发
Vue中通过集成Quill富文本编辑器实现公告的发布。Vue项目中vue-quill-editor的安装与使用【实战开发应用】
文章展示了在Vue项目中通过集成Quill富文本编辑器实现公告功能的完整开发过程,包括前端的公告发布、修改、删除操作以及后端的数据存储和处理逻辑。
Vue中通过集成Quill富文本编辑器实现公告的发布。Vue项目中vue-quill-editor的安装与使用【实战开发应用】
|
1月前
一款非常棒的十六进制编辑器 —— 010 Editor
一款非常棒的十六进制编辑器 —— 010 Editor
|
1月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
48 0
|
2月前
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
66 0
|
2月前
|
开发者 图形学 C#
揭秘游戏沉浸感的秘密武器:深度解析Unity中的音频设计技巧,从背景音乐到动态音效,全面提升你的游戏氛围艺术——附实战代码示例与应用场景指导
【8月更文挑战第31天】音频设计在游戏开发中至关重要,不仅能增强沉浸感,还能传递信息,构建氛围。Unity作为跨平台游戏引擎,提供了丰富的音频处理功能,助力开发者轻松实现复杂音效。本文将探讨如何利用Unity的音频设计提升游戏氛围,并通过具体示例代码展示实现过程。例如,在恐怖游戏中,阴森的背景音乐和突然的脚步声能增加紧张感;在休闲游戏中,轻快的旋律则让玩家感到愉悦。
43 0