Add SoundEffect audio system (ScriptableObject-based) with test assets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-10 13:25:56 +02:00
1 parent 75f5704ab8
commit 0be503c927
22 files changed
+741

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8938d848d62e6114092296326d25e582
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bb15841fc23ed264e98e4b3c07a3c742
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,114 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Prototype.Editor
{
public class AudioMixerSliderGUI : IMGUIContainer
{
public Object @object;
public string propertyPath;
public Vector2 valueRange = new(0, 1);
public float defaultValue;
bool dragging;
public AudioMixerSliderGUI() : base()
{
onGUIHandler = OnGUI;
RegisterCallback<MouseDownEvent>(OnMouseDown);
RegisterCallback<MouseMoveEvent>(OnMouseMove);
RegisterCallback<MouseUpEvent>(OnMouseUp);
}
SerializedProperty property
=> @object && !string.IsNullOrEmpty(propertyPath)
? new SerializedObject(@object).FindProperty(propertyPath)
: null;
public float value
{
get
{
return property?.floatValue ?? 0;
}
set
{
var property = this.property;
if (property is null)
return;
var newValue = Mathf.Clamp(value, valueRange.x, valueRange.y);
if (property.floatValue == newValue)
return;
property.floatValue = newValue;
property.serializedObject.ApplyModifiedProperties();
MarkDirtyRepaint();
}
}
Rect r => contentRect;
float s => r.width / 12;
float h => r.height / 16;
Rect r_inner => new(0, h / 2 + 8, r.width - s * 2, r.height - h - 18);
Rect r_slide => new(r.width / 2 - s / 2, r_inner.y, s, r_inner.height);
Rect r_button
{
get
{
var t = Mathf.InverseLerp(valueRange.x, valueRange.y, value);
return new(
(r.width - r_inner.width) / 2,
r_inner.y + r_inner.height * (1 - t) - h / 2,
r_inner.width,
h
);
}
}
void OnGUI()
{
EditorGUI.DrawRect(r, new(0, 0, 0, .3f));
EditorGUI.DrawRect(r_slide, new(0, 0, 0, .4f));
EditorGUI.DrawRect(r_button, new(1, 1, .6f, dragging ? .8f : .4f));
}
void OnMouseDown(MouseDownEvent e)
{
switch (e.button)
{
case 0:
dragging = true;
break;
case 1:
value = defaultValue;
break;
}
}
void OnMouseMove(MouseMoveEvent e)
{
if (!dragging)
return;
var t = 1 - (e.localMousePosition.y - r_inner.y) / r_inner.height;
value = Mathf.Lerp(valueRange.x, valueRange.y, t);
}
void OnMouseUp(MouseUpEvent e)
{
switch (e.button)
{
case 0:
dragging = false;
break;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c818c5d558214584f80ca414dd1cab70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,142 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UIElements;
namespace Prototype.Editor
{
public class AudioMixerWindow : EditorWindow
{
[MenuItem("Tools/Audio Mixer")]
static void Init()
{
GetWindow<AudioMixerWindow>().titleContent = new("Audio Mixer");
}
AudioMixerGroup audioMixerGroup;
List<SoundEffect> soundEffects;
readonly List<(SoundEffect soundEffect, VisualElement element)> soundEffectTracks = new();
void OnEnable()
{
audioMixerGroup = null;
soundEffects = null;
RefreshLayout();
}
void RefreshLayout()
{
var root = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
AssetDatabase.GUIDToAssetPath(
AssetDatabase.FindAssets(
$"t:{nameof(VisualTreeAsset)} {nameof(AudioMixerWindow)}"
)[0]
)
).Instantiate();
rootVisualElement.Clear();
rootVisualElement.Add(root);
root.StretchToParentSize();
root.Q<ObjectField>().RegisterValueChangedCallback(x =>
{
audioMixerGroup = x.newValue as AudioMixerGroup;
RefreshTracksVisibility();
});
}
void OnInspectorUpdate()
{
var refreshTracksVisibility = false;
if (soundEffects is null)
{
soundEffects = new();
foreach (var guid in AssetDatabase.FindAssets($"t:{nameof(SoundEffect)}"))
{
soundEffects.Add(
AssetDatabase.LoadAssetAtPath<SoundEffect>(
AssetDatabase.GUIDToAssetPath(guid)
)
);
}
RefreshSoundEffectTracks();
refreshTracksVisibility = true;
}
if (refreshTracksVisibility)
RefreshTracksVisibility();
}
void RefreshSoundEffectTracks()
{
soundEffectTracks.Clear();
if (soundEffects is not null)
{
foreach (var soundEffect in soundEffects)
{
var track = new VisualElement();
track.AddToClassList("track");
var applyButton = new Button(soundEffect.ApplyVolumeMultiplierFactor)
{
text = "Apply"
};
applyButton.AddToClassList("accent");
track.Add(applyButton);
track.Add(
new AudioMixerSliderGUI
{
@object = soundEffect,
propertyPath = "volumeMultiplierFactor",
valueRange = new Vector2(0, 2),
defaultValue = 1
}
);
track.Add(
new Label(
soundEffect.name
.Replace(" (Sound Effect)", "")
.Replace(" - BGM", "")
.Replace(" - SFX", "")
)
);
track.Add(
new Button(() => Selection.activeObject = soundEffect)
{
text = "Select"
}
);
soundEffectTracks.Add((soundEffect, track));
}
}
}
void RefreshTracksVisibility()
{
var content = rootVisualElement.Q<ScrollView>().contentContainer;
content.Clear();
foreach (var (soundEffect, track) in soundEffectTracks)
{
if (audioMixerGroup &&
audioMixerGroup != soundEffect.audioMixerGroup)
{
continue;
}
content.Add(track);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db79bb44fd8fed0449189fb735c16e0a
@@ -0,0 +1,41 @@
#root #unity-content-container {
padding-left: 2px;
}
.track {
margin-right: 2px;
width: 50px;
}
.track > AudioMixerSliderGUI {
flex-grow: 1;
}
.track > Label {
overflow: hidden;
height: 42px;
white-space: normal;
-unity-text-align: middle-center;
font-size: 9px;
}
Button {
padding: 2.5px 0;
margin: 2px 0;
font-size: 10.5px;
border-color: transparent;
border-radius: 0;
}
Button { background-color: rgba(105, 155, 205, .25) }
Button:hover { background-color: rgba(55, 155, 255, .4) }
Button:active { background-color: rgba(0, 155, 255, .6) }
Button.accent { background-color: rgba(55, 205, 105, .25) }
Button.accent:hover { background-color: rgba(0, 205, 105, .4) }
Button.accent:active { background-color: rgba(0, 205, 105, .55) }
Button.disabled {
background-color: rgba(0, 0, 0, .2);
color: transparent;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe45eeb67beba6548bfeab8e7921df39
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
@@ -0,0 +1,10 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Assets/_/Scripts/Audio/Editor/AudioMixerWindow.uss?fileID=7433441132597879392&amp;guid=fe45eeb67beba6548bfeab8e7921df39&amp;type=3#AudioMixerWindow" />
<ui:VisualElement name="root" style="flex-grow: 1;">
<ui:VisualElement style="flex-direction: row;">
<uie:ObjectField allow-scene-objects="false" type="UnityEngine.Audio.AudioMixerGroup, UnityEngine.AudioModule" style="flex-grow: 1;" />
<ui:EnumField type="Prototype.Editor.AudioMixerWindow+SoundType, Assembly-CSharp-Editor" style="flex-basis: 160px;" />
</ui:VisualElement>
<ui:ScrollView scroll-deceleration-rate="0,135" elasticity="0,1" name="tracks" mode="Horizontal" vertical-scroller-visibility="Hidden" horizontal-scroller-visibility="AlwaysVisible" style="flex-grow: 1;" />
</ui:VisualElement>
</ui:UXML>
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7baee57de0202b94d829ac4ab1fc2016
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
+177
View File
@@ -0,0 +1,177 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
namespace Prototype
{
[CreateAssetMenu(menuName = "Prototype/Sound Effect")]
public class SoundEffect : ScriptableObject
{
[SerializeField] SoundEffectDistanceRange distanceRange;
[SerializeField] Vector2 volumeMultiplier = new(1, 1);
[SerializeField] Vector2 pitch = new(.95f, 1.05f);
[Header("Additional Settings")]
public AudioMixerGroup audioMixerGroup;
public float volumeMultiplierFactor = 1;
[SerializeField] bool loop;
[SerializeField] Vector2 fade = new();
[Header("Additional Settings")]
[SerializeField, Range(0, 1)] float defaultVolume = .8f;
[SerializeField, Range(-1, 1)] float additionalGain = 0;
[SerializeField] float maxPlayInterval = .1f;
[Header("Clips")]
[SerializeField] List<AudioClip> clips;
public int ClipCount => clips.Count;
float lastPlayTime;
public SoundEffectOptions ToOptions() => new()
{
audioMixerGroup = audioMixerGroup,
volumeMultiplier = Random.Range(volumeMultiplier.x, volumeMultiplier.y) * volumeMultiplierFactor,
pitch = Random.Range(pitch.x, pitch.y),
loop = loop,
distanceRange = distanceRange ? distanceRange.range : null,
fade = fade
};
#if UNITY_EDITOR
public void ApplyVolumeMultiplierFactor()
{
volumeMultiplier *= volumeMultiplierFactor;
volumeMultiplierFactor = 1;
}
void OnEnable()
{
Initialize();
UnityEditor.EditorApplication.playModeStateChanged -= PlayModeStateChanged;
UnityEditor.EditorApplication.playModeStateChanged += PlayModeStateChanged;
}
void PlayModeStateChanged(UnityEditor.PlayModeStateChange change)
{
if (change == UnityEditor.PlayModeStateChange.EnteredEditMode)
{
Initialize();
return;
}
if (change == UnityEditor.PlayModeStateChange.ExitingEditMode)
{
Initialize();
return;
}
}
#endif
void Initialize()
{
lastPlayTime = -1;
}
public AudioClip GetClip(int index)
{
return clips[index];
}
public SoundEffectInstance Play()
{
return PlayInternal(Vector3.zero, 0);
}
public SoundEffectInstance Play(Vector3 position)
{
return PlayInternal(position, 0);
}
public SoundEffectInstance Play(Transform transform)
{
return PlayInternal(transform.position, 0);
}
public SoundEffectInstance Play(Vector3 position, float delay)
{
return PlayInternal(position, delay);
}
public SoundEffectInstance Play(Transform transform, float delay)
{
return PlayInternal(transform.position, delay);
}
public SoundEffectInstance PlayFirstClip()
{
return PlayInternal(Vector3.zero, 0, true);
}
public SoundEffectInstance PlayFirstClip(bool destroyOnLoad)
{
return PlayInternal(Vector3.zero, 0, true, destroyOnLoad);
}
public SoundEffectInstance PlayFirstClipAt(Vector3 position)
{
return PlayInternal(position, 0, true);
}
public SoundEffectInstance PlayFirstClipWithDelayAt(Vector3 position, float delay)
{
return PlayInternal(position, delay, true);
}
SoundEffectInstance PlayInternal(Vector3 position, float delay, bool firstClip = false, bool destroyOnLoad = true)
{
var options = ToOptions();
options.delay = delay;
options.destroyOnLoad = destroyOnLoad;
AudioClip clip;
if (firstClip)
{
clip = clips[0];
}
else
{
int rngIndex = Random.Range(0, clips.Count);
clip = clips[rngIndex];
}
var time = Time.time;
if (!Mathf.Approximately(maxPlayInterval, 0))
{
if (time - lastPlayTime < maxPlayInterval)
{
return null;
}
}
lastPlayTime = time;
var gameObject = new GameObject($"{this.name} -- {clip.name}");
gameObject.SetActive(false);
gameObject.transform.position = position;
if (!options.volume.HasValue)
{
options.volume = defaultVolume;
}
options.volume += additionalGain;
var soundEffectInstance = gameObject.AddComponent<SoundEffectInstance>();
soundEffectInstance.clip = clip;
soundEffectInstance.options = options;
gameObject.SetActive(true);
return soundEffectInstance;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d395190edf0c4124382f84bc10ba3b22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
using UnityEngine;
namespace Prototype
{
[CreateAssetMenu(menuName = "Prototype/Sound Effect Distance Range")]
public class SoundEffectDistanceRange : UnityEngine.ScriptableObject
{
public Vector2 range = new SoundEffectOptions().GetDistanceRange();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 523ce7d83fde21c419a5d637cbe7bf04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using System.Collections;
using UnityEngine;
namespace Prototype
{
[AddComponentMenu("/")]
public class SoundEffectInstance : MonoBehaviour
{
public AudioClip clip { get; set; }
public SoundEffectOptions options { get; set; }
AudioSource audioSource;
void Awake()
{
audioSource = gameObject.AddComponent<AudioSource>();
{
audioSource.playOnAwake = false;
audioSource.dopplerLevel = 0;
audioSource.outputAudioMixerGroup = options.audioMixerGroup;
audioSource.clip = clip;
audioSource.volume = options.GetVolume();
audioSource.pitch = options.GetPitch();
var range = options.GetDistanceRange();
audioSource.spatialBlend = range.x == 0 && range.y == 0 ? 0 : 1;
audioSource.rolloffMode = AudioRolloffMode.Linear;
audioSource.minDistance = range.x;
audioSource.maxDistance = range.y;
audioSource.loop = options.GetLoop();
}
if (!options.ShouldDestroyOnLoad())
{
DontDestroyOnLoad(this);
}
}
IEnumerator Start()
{
var delay = options.GetDelay();
if (delay > 0)
{
yield return new WaitForSeconds(delay);
}
audioSource.Play();
if (audioSource.loop)
{
yield break;
}
yield return new WaitForSecondsRealtime(audioSource.clip.length + .1f);
Destroy(gameObject);
}
public void Stop()
{
Destroy(gameObject);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3d53bc68612e074d803a25c5580bf81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using UnityEngine;
using UnityEngine.Audio;
namespace Prototype
{
public struct SoundEffectOptions
{
public AudioMixerGroup audioMixerGroup;
public float? volume;
public float? volumeMultiplier;
public float GetVolume() => (volume ?? 1) * (volumeMultiplier ?? 1);
public float? pitch;
public float GetPitch() => pitch ?? 1;
public bool? loop;
public bool GetLoop() => loop ?? false;
public Vector2? distanceRange;
public Vector2 GetDistanceRange() => distanceRange ?? new(30, 200);
public Vector2? fade;
public Vector2 GetFade() => fade ?? new();
public bool? destroyOnLoad;
public bool ShouldDestroyOnLoad() => destroyOnLoad ?? true;
public float? delay;
public float GetDelay() => delay ?? 0;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe1ba58c96e939f4f89333877ba25123
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 523ce7d83fde21c419a5d637cbe7bf04, type: 3}
m_Name: Sound Effect Distance Range
m_EditorClassIdentifier: Assembly-CSharp::Prototype.SoundEffectDistanceRange
range: {x: 1, y: 20}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a1e7b7b0fca7d504dbf0a8ea6a4787c8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+25
View File
@@ -0,0 +1,25 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d395190edf0c4124382f84bc10ba3b22, type: 3}
m_Name: Test Sound Effect
m_EditorClassIdentifier: Assembly-CSharp::Prototype.SoundEffect
distanceRange: {fileID: 11400000, guid: a1e7b7b0fca7d504dbf0a8ea6a4787c8, type: 2}
volumeMultiplier: {x: 1, y: 1}
pitch: {x: 0.95, y: 1.05}
audioMixerGroup: {fileID: 24300002, guid: 4c2f5be6ec68f3e4aa5ef0471d7f2e22, type: 2}
volumeMultiplierFactor: 1
loop: 1
fade: {x: 0, y: 0}
defaultVolume: 0.8
additionalGain: 0
maxPlayInterval: 0.1
clips: []
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9f1562b7c4308e948aea57f616daf046
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant: