diff --git a/VisualPinball.Engine/VPT/MechSounds.meta b/VisualPinball.Engine/VPT/MechSounds.meta
new file mode 100644
index 000000000..4d6559344
--- /dev/null
+++ b/VisualPinball.Engine/VPT/MechSounds.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f9a957a9e90457b45ba74b1c0b726c3f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets.meta b/VisualPinball.Unity/Assets/Presets.meta
index 0d4105838..d3dab9898 100644
--- a/VisualPinball.Unity/Assets/Presets.meta
+++ b/VisualPinball.Unity/Assets/Presets.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 74b6f483aa6bd6c49bc05dca5f2c6750
+guid: a61d04b442140514a9bfb858f9ed8f05
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound.meta
new file mode 100644
index 000000000..dc4a50b28
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d6280d7fb0f340b09b058831259ab274
+timeCreated: 1677682143
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundDrawer.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundDrawer.cs
new file mode 100644
index 000000000..84bac4b74
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundDrawer.cs
@@ -0,0 +1,112 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+namespace VisualPinball.Unity.Editor
+{
+ [CustomPropertyDrawer(typeof(MechSound))]
+ public class MechSoundDrawer : PropertyDrawer
+ {
+ public override VisualElement CreatePropertyGUI(SerializedProperty property)
+ {
+ var container = new VisualElement();
+ var treeAsset = AssetDatabase.LoadAssetAtPath(
+ "Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml");
+ treeAsset.CloneTree(container);
+ var triggerDropdown = container.Q("trigger-id");
+ var stopTriggerDropdown = container.Q("stop-trigger-id");
+ var hasStopTriggerToggle = container.Q("has-stop-trigger");
+ var availableTriggers = GetAvailableTriggers(property);
+ if (availableTriggers.Length > 0) {
+ var triggerIdProp = property.FindPropertyRelative("TriggerId");
+ var stopTriggerIdProp = property.FindPropertyRelative("StopTriggerId");
+ ConfigureTriggerDropdown(triggerIdProp, triggerDropdown, availableTriggers);
+ ConfigureTriggerDropdown(stopTriggerIdProp, stopTriggerDropdown, availableTriggers);
+ hasStopTriggerToggle.RegisterValueChangedCallback(
+ e => stopTriggerDropdown.style.display = e.newValue ? DisplayStyle.Flex : DisplayStyle.None);
+ var hasStopTriggerProp = property.FindPropertyRelative("HasStopTrigger");
+ ConfigureInfiniteLoopHelpBox(property, container, hasStopTriggerToggle, hasStopTriggerProp);
+ } else {
+ AddNoTriggersHelpBox(container, triggerDropdown, stopTriggerDropdown, hasStopTriggerToggle);
+ }
+ property.serializedObject.ApplyModifiedProperties();
+ return container;
+ }
+
+ private static void ConfigureTriggerDropdown(SerializedProperty triggerIdProp, DropdownField triggerDropdown, SoundTrigger[] availableTriggers)
+ {
+ var availableTriggerNames = availableTriggers.Select(t => t.Name).ToList();
+ triggerDropdown.choices = availableTriggerNames;
+
+ var isSelectedTriggerValid = availableTriggers.Any(t => t.Id == triggerIdProp.stringValue);
+ if (isSelectedTriggerValid) {
+ triggerDropdown.value = availableTriggers.First(t => t.Id == triggerIdProp.stringValue).Name;
+ } else {
+ triggerDropdown.value = availableTriggerNames[0];
+ triggerIdProp.stringValue = availableTriggers[0].Id;
+ }
+
+ triggerDropdown.RegisterValueChangedCallback(
+ e => {
+ triggerIdProp.stringValue = availableTriggers.FirstOrDefault(t => t.Name == e.newValue).Id;
+ triggerIdProp.serializedObject.ApplyModifiedProperties();
+ });
+ }
+
+ private static void AddNoTriggersHelpBox(VisualElement container, DropdownField triggerDropdown, DropdownField stopTriggerDropdown, Toggle hasStopTriggerToggle)
+ {
+ container.Insert(0, new HelpBox("There are no triggers to choose from", HelpBoxMessageType.Info));
+ triggerDropdown.style.display = DisplayStyle.None;
+ stopTriggerDropdown.style.display = DisplayStyle.None;
+ hasStopTriggerToggle.style.display = DisplayStyle.None;
+ }
+
+ private static void ConfigureInfiniteLoopHelpBox(SerializedProperty rootProp, VisualElement container, Toggle hasStopTriggerToggle, SerializedProperty hasStopTriggerProp)
+ {
+ var soundAssetProp = rootProp.FindPropertyRelative("Sound");
+ var infiniteLoopHelpBox = new HelpBox("The selected sound asset loops and no stop trigger is set, so the sound will loop forever once started.", HelpBoxMessageType.Warning);
+ infiniteLoopHelpBox.style.display = DisplayStyle.None;
+ container.Insert(0, infiniteLoopHelpBox);
+ var soundAssetField = container.Q("sound-asset");
+ soundAssetField.RegisterValueChangedCallback(
+ e => UpdateInfiniteLoopHelpBoxVisbility(soundAssetProp.objectReferenceValue as SoundAsset, hasStopTriggerProp.boolValue, infiniteLoopHelpBox));
+ hasStopTriggerToggle.RegisterValueChangedCallback(
+ e => UpdateInfiniteLoopHelpBoxVisbility(soundAssetProp.objectReferenceValue as SoundAsset, hasStopTriggerProp.boolValue, infiniteLoopHelpBox));
+ }
+
+ private static void UpdateInfiniteLoopHelpBoxVisbility(SoundAsset soundAsset, bool hasStopTrigger, VisualElement box)
+ {
+ if (soundAsset && soundAsset.Loop && !hasStopTrigger)
+ box.style.display = DisplayStyle.Flex;
+ else
+ box.style.display = DisplayStyle.None;
+ }
+
+ private static SoundTrigger[] GetAvailableTriggers(SerializedProperty property)
+ {
+ var mechSoundsComponent = (MechSoundsComponent)property.serializedObject.targetObject;
+ if (mechSoundsComponent.TryGetComponent(out var emitter))
+ return emitter.AvailableTriggers;
+ else
+ return Array.Empty();
+ }
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundDrawer.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundDrawer.cs.meta
new file mode 100644
index 000000000..67e6c6874
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundDrawer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 884fb5b527309ef489e8b27aa9e4809d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs
new file mode 100644
index 000000000..894fd7253
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs
@@ -0,0 +1,158 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System.Linq;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace VisualPinball.Unity.Editor
+{
+ [CustomEditor(typeof(SoundAsset)), CanEditMultipleObjects]
+ public class SoundAssetInspector : UnityEditor.Editor
+ {
+ private SerializedProperty _nameProperty;
+ private SerializedProperty _descriptionProperty;
+ private SerializedProperty _volumeCorrectionProperty;
+ private SerializedProperty _clipsProperty;
+ private SerializedProperty _clipSelectionProperty;
+ private SerializedProperty _randomizePitchProperty;
+ private SerializedProperty _randomizeVolumeProperty;
+ private SerializedProperty _loopProperty;
+
+ private SoundAsset _soundAsset;
+
+ private AudioSource _editorAudioSource;
+ //private AudioMixer _editorAudioMixer;
+
+ private const float ButtonHeight = 30;
+ private const float ButtonWidth = 50;
+
+ private void OnEnable()
+ {
+ _nameProperty = serializedObject.FindProperty(nameof(SoundAsset.Name));
+ _descriptionProperty = serializedObject.FindProperty(nameof(SoundAsset.Description));
+ _volumeCorrectionProperty = serializedObject.FindProperty(nameof(SoundAsset.VolumeCorrection));
+ _clipsProperty = serializedObject.FindProperty(nameof(SoundAsset.Clips));
+ _clipSelectionProperty = serializedObject.FindProperty(nameof(SoundAsset.ClipSelection));
+ _randomizePitchProperty = serializedObject.FindProperty(nameof(SoundAsset.RandomizePitch));
+ _randomizeVolumeProperty = serializedObject.FindProperty(nameof(SoundAsset.RandomizeVolume));
+ _loopProperty = serializedObject.FindProperty(nameof(SoundAsset.Loop));
+
+ _editorAudioSource = GetOrCreateAudioSource();
+ //_editorAudioMixer = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/Assets/Resources/EditorMixer.mixer");
+ //_editorAudioSource.outputAudioMixerGroup = _editorAudioMixer.outputAudioMixerGroup;
+
+ _soundAsset = target as SoundAsset;
+ }
+
+ public override void OnInspectorGUI()
+ {
+ serializedObject.Update();
+
+ EditorGUILayout.PropertyField(_nameProperty, true);
+
+ using (var horizontalScope = new GUILayout.HorizontalScope())
+ {
+ EditorGUILayout.PropertyField(_descriptionProperty, GUILayout.Height(100));
+ }
+
+ EditorGUILayout.PropertyField(_volumeCorrectionProperty, true);
+ EditorGUILayout.PropertyField(_clipsProperty);
+ EditorGUILayout.PropertyField(_clipSelectionProperty, true);
+ EditorGUILayout.PropertyField(_randomizePitchProperty, true);
+ EditorGUILayout.PropertyField(_randomizeVolumeProperty, true);
+ EditorGUILayout.PropertyField(_loopProperty);
+
+ serializedObject.ApplyModifiedProperties();
+
+ // center button
+ GUILayout.BeginHorizontal();
+ GUILayout.FlexibleSpace();
+ if (PlayStopButton()) {
+ PlayStop();
+ }
+ GUILayout.FlexibleSpace();
+ GUILayout.EndHorizontal();
+ }
+
+ private void PlayStop()
+ {
+ if (_editorAudioSource.isPlaying) {
+ _soundAsset.Stop(_editorAudioSource);
+ } else {
+ _soundAsset.Play(_editorAudioSource);
+ }
+ }
+
+ private bool PlayStopButton()
+ {
+ return _editorAudioSource.isPlaying
+ ? GUILayout.Button(new GUIContent("Stop", Icons.StopButton(IconSize.Small, IconColor.Orange)),
+ GUILayout.Height(ButtonHeight), GUILayout.Width(ButtonWidth))
+ : GUILayout.Button(new GUIContent("Play", Icons.PlayButton(IconSize.Small, IconColor.Orange)),
+ GUILayout.Height(ButtonHeight), GUILayout.Width(ButtonWidth));
+ }
+
+ ///
+ /// Gets or creates the AudioSource for playing sounds in the editor.
+ /// The object containing the AudioSource is created in a new, additively loaded scene
+ /// to avoid making changes to the user's currently open scene.
+ ///
+ /// AudioSource for previewing audio assets in the editor
+ private static AudioSource GetOrCreateAudioSource()
+ {
+ Scene editorScene = GetOrCreatePreviewScene();
+ GameObject editorAudio = GetOrCreatePreviewAudioObject(editorScene);
+ if (!editorAudio.TryGetComponent(out var audioSource)) {
+ audioSource = editorAudio.AddComponent();
+ }
+ return audioSource;
+ }
+
+ private static Scene GetOrCreatePreviewScene()
+ {
+ const string sceneName = "VpeEditorScene";
+
+ for (int i = 0; i < SceneManager.loadedSceneCount; i++) {
+ Scene scene = SceneManager.GetSceneAt(i);
+ if (scene.name == sceneName)
+ return scene;
+ }
+
+ Scene previewScene = EditorSceneManager.NewPreviewScene();
+ previewScene.name = sceneName;
+ return previewScene;
+ }
+
+ private static GameObject GetOrCreatePreviewAudioObject(Scene previewScene)
+ {
+ const string audioObjName = "AudioPreview";
+
+ var audioObj = previewScene.GetRootGameObjects()
+ .FirstOrDefault(go => go.name == audioObjName);
+
+ if (audioObj == null) {
+ audioObj = new GameObject(audioObjName);
+ SceneManager.MoveGameObjectToScene(audioObj, previewScene);
+ }
+
+ return audioObj;
+ }
+ }
+}
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs.meta
new file mode 100644
index 000000000..9530058fc
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundAssetInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bace99bbc8f020f49b66c0ce06780514
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml
new file mode 100644
index 000000000..3229f55ee
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml.meta
new file mode 100644
index 000000000..01cf6511b
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundDrawer.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 5edbe5d4628bdb545a05bfa87ed2f700
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.cs
new file mode 100644
index 000000000..d9dc09694
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.cs
@@ -0,0 +1,25 @@
+using UnityEditor;
+using UnityEngine;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+
+namespace VisualPinball.Unity.Editor
+{
+ [CustomEditor(typeof(MechSoundsComponent)), CanEditMultipleObjects]
+ public class SoundsComponentInspector : UnityEditor.Editor
+ {
+ [SerializeField]
+ private VisualTreeAsset inspectorXml;
+
+ public override VisualElement CreateInspectorGUI()
+ {
+ VisualElement inspector = new VisualElement();
+ var comp = target as MechSoundsComponent;
+ if (!comp!.TryGetComponent(out var _))
+ inspector.Add(new HelpBox("Cannot find sound emitter. This component only works with a sound emitter on the same GameObject.", HelpBoxMessageType.Warning));
+ inspectorXml.CloneTree(inspector);
+ return inspector;
+ }
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.cs.meta
new file mode 100644
index 000000000..80e6f578c
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: b54f8fa6e77f2104a898dca7e163c4e6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences:
+ - inspectorXml: {fileID: 9197481963319205126, guid: a6e8546981c540948ae9851157602432,
+ type: 3}
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uss b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uss
new file mode 100644
index 000000000..218263a81
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uss
@@ -0,0 +1,5 @@
+.custom-label {
+ font-size: 20px;
+ -unity-font-style: bold;
+ color: rgb(68, 138, 255);
+}
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uss.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uss.meta
new file mode 100644
index 000000000..ef1e9b51a
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 30f22b4450bbae84eb18d9bc7f656e8c
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uxml b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uxml
new file mode 100644
index 000000000..56f8b2094
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uxml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uxml.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uxml.meta
new file mode 100644
index 000000000..9412b2369
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/SoundsComponentInspector.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: a6e8546981c540948ae9851157602432
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs
index 1a9abde26..3cc82a76f 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/Icons.cs
@@ -80,9 +80,11 @@ public IconVariant(string name, IconSize size, IconColor color)
private const string KickerName = "kicker";
private const string LightGroupName = "light_group";
private const string LightName = "light";
+ private const string LoopButtonName = "player";
private const string MechName = "mech";
private const string MechPinMameName = "mech_pinmame";
private const string PlayfieldName = "playfield";
+ private const string PlayButtonName = "player";
private const string PlugName = "plug";
private const string PlungerName = "plunger";
private const string PrimitiveName = "primitive";
@@ -94,6 +96,7 @@ public IconVariant(string name, IconSize size, IconColor color)
private const string ScoreReelSingleName = "score_reel_single";
private const string SlingshotName = "slingshot";
private const string SpinnerName = "spinner";
+ private const string StopButtonName = "kicker";
private const string SurfaceName = "surface";
private const string SwitchNcName = "switch_nc";
private const string SwitchNoName = "switch_no";
@@ -117,8 +120,8 @@ public IconVariant(string name, IconSize size, IconColor color)
private static readonly string[] Names = {
AssetLibraryName, BallRollerName, BallName, BoltName, BumperName, CalendarName, CannonName, CoilName, DropTargetBankName, DropTargetName, FlasherName,
- FlipperName, GateName, GateLifterName, HitTargetName, KeyName, KickerName, LightGroupName, LightName, MechName, MechPinMameName, PlayfieldName, PlugName,
- PhysicsName, PlungerName, PrimitiveName, RampName, RotatorName, RubberName, ScoreReelName, ScoreReelSingleName, SlingshotName, SpinnerName, SurfaceName,
+ FlipperName, GateName, GateLifterName, HitTargetName, KeyName, KickerName, LightGroupName, LightName, LoopButtonName, MechName, MechPinMameName, PlayfieldName, PlayButtonName, PlugName,
+ PhysicsName, PlungerName, PrimitiveName, RampName, RotatorName, RubberName, ScoreReelName, ScoreReelSingleName, SlingshotName, SpinnerName, StopButtonName, SurfaceName,
SwitchNcName, SwitchNoName, TableName, TeleporterName, TriggerName, TroughName,
CoilEventName, SwitchEventName, LampEventName, LampSeqName, MetalWireGuideName,
PlayerVariableName, PlayerVariableEventName, TableVariableName, TableVariableEventName, UpdateDisplayName, DisplayEventName
@@ -182,12 +185,14 @@ private static IIconLookup[] GetLookups() {
public static Texture2D Key(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(KeyName, size, color);
public static Texture2D Kicker(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(KickerName, size, color);
public static Texture2D Light(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(LightName, size, color);
+ public static Texture2D LoopButton(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(LoopButtonName, size, color);
public static Texture2D LightGroup(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(LightGroupName, size, color);
public static Texture2D Mech(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(MechName, size, color);
public static Texture2D MechPinMame(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(MechPinMameName, size, color);
public static Texture2D MetalWireGuide(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(MetalWireGuideName, size, color);
public static Texture2D Physics(IconSize size = IconSize.Small, IconColor color = IconColor.Gray) => Instance.GetItem(PhysicsName, size, color);
public static Texture2D Playfield(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(PlayfieldName, size, color);
+ public static Texture2D PlayButton(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(PlayButtonName, size, color);
public static Texture2D Plug(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(PlugName, size, color);
public static Texture2D Plunger(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(PlungerName, size, color);
public static Texture2D Primitive(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(PrimitiveName, size, color);
@@ -198,6 +203,7 @@ private static IIconLookup[] GetLookups() {
public static Texture2D ScoreReelSingle(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(ScoreReelSingleName, size, color);
public static Texture2D Slingshot(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(SlingshotName, size, color);
public static Texture2D Spinner(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(SpinnerName, size, color);
+ public static Texture2D StopButton(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(StopButtonName, size, color);
public static Texture2D Surface(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(SurfaceName, size, color);
public static Texture2D Switch(bool isClosed, IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(isClosed ? SwitchNcName : SwitchNoName, size, color);
public static Texture2D Table(IconSize size = IconSize.Large, IconColor color = IconColor.Gray) => Instance.GetItem(TableName, size, color);
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
index 9aee1854e..0642cecde 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs
@@ -24,6 +24,7 @@
using VisualPinball.Engine.Common;
using VisualPinball.Engine.Game;
using VisualPinball.Engine.Game.Engines;
+using VisualPinball.Engine.VPT.Trigger;
using Color = VisualPinball.Engine.Math.Color;
using Logger = NLog.Logger;
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound.meta
new file mode 100644
index 000000000..6554ff4e7
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 24e3e95b7dc44c3ebd511ab4c4194a65
+timeCreated: 1677678507
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/FadeMixerGroup.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/FadeMixerGroup.cs
new file mode 100644
index 000000000..5004059be
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/FadeMixerGroup.cs
@@ -0,0 +1,40 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine.Audio;
+using UnityEngine;
+public static class FadeMixerGroup
+{
+ //note: fade duration below 1 second causes a breakdown of this method
+ public static IEnumerator StartFade(AudioMixer audioMixer, string exposedParam, float duration, float targetVolume)
+ {
+ float currentTime = 0;
+ float currentVol;
+ audioMixer.GetFloat(exposedParam, out currentVol);
+ currentVol = Mathf.Pow(10, currentVol / 20);
+ float targetValue = Mathf.Clamp(targetVolume, 0.0001f, 1);
+ while (currentTime < duration)
+ {
+ currentTime += Time.deltaTime;
+ float newVol = Mathf.Lerp(currentVol, targetValue, currentTime / duration);
+ audioMixer.SetFloat(exposedParam, Mathf.Log10(newVol) * 20);
+ yield return null;
+ }
+ yield break;
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/FadeMixerGroup.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/FadeMixerGroup.cs.meta
new file mode 100644
index 000000000..aba75f0dd
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/FadeMixerGroup.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 39c1161b40f535947ad995908584d38a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/ISoundEmitter.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/ISoundEmitter.cs
new file mode 100644
index 000000000..306387eb7
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/ISoundEmitter.cs
@@ -0,0 +1,48 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using System;
+
+namespace VisualPinball.Unity
+{
+ ///
+ /// An interface for item components that emit mechanical sounds.
+ ///
+ public interface ISoundEmitter
+ {
+ ///
+ /// A list of triggers that can be linked to emitting a sound.
+ ///
+ SoundTrigger[] AvailableTriggers { get; }
+
+ ///
+ /// The sound event, to which the subscribes to.
+ ///
+ event EventHandler OnSound;
+ }
+
+ public readonly struct SoundEventArgs
+ {
+ public readonly string TriggerId;
+ public readonly float Volume;
+
+ public SoundEventArgs(string triggerId, float volume)
+ {
+ TriggerId = triggerId;
+ Volume = volume;
+ }
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/ISoundEmitter.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/ISoundEmitter.cs.meta
new file mode 100644
index 000000000..ca209a4c5
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/ISoundEmitter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7129feb5a774383458adb84d3b08fabe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs
new file mode 100644
index 000000000..ff65cbc3c
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs
@@ -0,0 +1,62 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// ReSharper disable InconsistentNaming
+
+using System;
+using UnityEngine;
+
+namespace VisualPinball.Unity
+{
+ [Serializable]
+ public class MechSound : ISerializationCallbackReceiver
+ {
+ [SerializeReference]
+ public SoundAsset Sound;
+
+ public string TriggerId;
+ public bool HasStopTrigger;
+ public string StopTriggerId;
+
+ [Range(0.0001f, 1)]
+ // This initialization doesnt work in inspector
+ public float Volume = 1;
+
+ [Tooltip("Increments of 1000")]
+ [Min(0)]
+ public float Fade;
+
+ #region DefaultValuesWorkaround
+ // When an instance is created by pressing the + icon on a list in the inspector,
+ // Unity does not apply default values (such as Volume = 1) and no constructor is called.
+ // See https://www.reddit.com/r/Unity3D/comments/j5i6cj/inspector_struct_default_values/.
+ // This workaround applies default values the first time the struct is serialized instead.
+ // It only works for the first instance in the list, because for any subsequent instance Unity
+ // clones the field values of the previous instance, including the areDefaultsApplied flag.
+ [SerializeField]
+ private bool areDefaultsApplied = false;
+
+ public void OnAfterDeserialize() { }
+ public void OnBeforeSerialize()
+ {
+ if (!areDefaultsApplied) {
+ Volume = 1;
+ areDefaultsApplied = true;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs.meta
new file mode 100644
index 000000000..4917bf27a
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e750cca599b34fd1aa5c29d5b38788f4
+timeCreated: 1677682479
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs
new file mode 100644
index 000000000..fc9e7a919
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs
@@ -0,0 +1,93 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// ReSharper disable InconsistentNaming
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NLog;
+using UnityEngine;
+using UnityEngine.Audio;
+using Logger = NLog.Logger;
+
+namespace VisualPinball.Unity
+{
+ [AddComponentMenu("Visual Pinball/Sounds/Mechanical Sounds")]
+ public class MechSoundsComponent : MonoBehaviour
+ {
+ [SerializeField]
+ private List _sounds = new();
+
+ [SerializeField]
+ private AudioMixerGroup _audioMixerGroup;
+
+ private ISoundEmitter _soundEmitter;
+ private CancellationTokenSource tcs;
+
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ private void OnEnable()
+ {
+ _soundEmitter = GetComponent();
+ _soundEmitter.OnSound += HandleSoundEmitterOnSound;
+ tcs = new();
+ }
+
+ private void OnDisable()
+ {
+ if (_soundEmitter != null) {
+ _soundEmitter.OnSound -= HandleSoundEmitterOnSound;
+ }
+ tcs.Cancel();
+ tcs.Dispose();
+ tcs = null;
+ }
+
+ // Async void is ok here because it's an event callback
+ private async void HandleSoundEmitterOnSound(object sender, SoundEventArgs e)
+ {
+ List playTasks = new();
+ foreach (MechSound sound in _sounds.Where(s => s.TriggerId == e.TriggerId))
+ playTasks.Add(Play(sound, tcs.Token));
+ await Task.WhenAll(playTasks);
+ }
+
+ private async Task Play(MechSound sound, CancellationToken ct)
+ {
+ var audioSource = gameObject.AddComponent();
+ try {
+ sound.Sound.Play(audioSource, sound.Volume);
+ _soundEmitter.OnSound += SoundEmitter_OnSound;
+ while (audioSource.isPlaying && !ct.IsCancellationRequested)
+ await Task.Yield();
+ } finally {
+ if (audioSource)
+ Destroy(audioSource);
+ _soundEmitter.OnSound -= SoundEmitter_OnSound;
+ }
+
+ void SoundEmitter_OnSound(object sender, SoundEventArgs eventArgs)
+ {
+ if (sound.HasStopTrigger && eventArgs.TriggerId == sound.StopTriggerId)
+ audioSource.Stop();
+ }
+ }
+ }
+}
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs.meta
new file mode 100644
index 000000000..c79190540
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c28cae51000318145b40983f440013ab
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs
new file mode 100644
index 000000000..9fb849227
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs
@@ -0,0 +1,100 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// ReSharper disable InconsistentNaming
+
+using System;
+using UnityEngine;
+using Random = UnityEngine.Random;
+
+namespace VisualPinball.Unity
+{
+ [CreateAssetMenu(fileName = "Sound", menuName = "Visual Pinball/Sound", order = 102)]
+ public class SoundAsset : ScriptableObject
+ {
+ #region Properties
+
+ public string Name;
+ public string Description;
+
+ [Range(0, 1)]
+ public float VolumeCorrection = 1;
+
+ public AudioClip[] Clips;
+
+ public enum Selection
+ {
+ RoundRobin,
+ Random
+ }
+
+ public Selection ClipSelection;
+
+ [Range(0, 0.3f)]
+ public float RandomizePitch;
+
+ [Range(0, 0.5f)]
+ public float RandomizeVolume;
+
+ public bool Loop;
+
+ #endregion
+
+ #region Runtime
+
+ [NonSerialized]
+ private int _clipIndex = 0;
+
+ #endregion
+
+ public void Play(AudioSource audioSource, float volume = 1)
+ {
+ if (Clips.Length == 0) {
+ return;
+ }
+ audioSource.volume = Volume * volume;
+ audioSource.pitch = Pitch;
+ audioSource.loop = Loop;
+ audioSource.clip = GetClip();
+ audioSource.Play();
+ }
+
+
+ public void Stop(AudioSource audioSource)
+ {
+ audioSource.Stop();
+ }
+
+ private float Pitch => 1f + Random.Range(-RandomizePitch / 2, RandomizePitch / 2);
+ private float Volume => VolumeCorrection - Random.Range(0, RandomizeVolume);
+
+ private AudioClip GetClip()
+ {
+ switch (ClipSelection) {
+ case Selection.RoundRobin:
+ var clip = Clips[_clipIndex];
+ _clipIndex = (_clipIndex + 1) % Clips.Length;
+ return clip;
+
+ case Selection.Random:
+ return Clips[Random.Range(0, Clips.Length)];
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs.meta
new file mode 100644
index 000000000..8fdd6acfd
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundAsset.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8a6287696a5efed489f573445fda4418
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundTrigger.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundTrigger.cs
new file mode 100644
index 000000000..ff5c3c506
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundTrigger.cs
@@ -0,0 +1,39 @@
+// Visual Pinball Engine
+// Copyright (C) 2023 freezy and VPE Team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+namespace VisualPinball.Unity
+{
+ ///
+ /// A sound trigger describes how a mechanical sound is triggered.
+ ///
+ /// During edit time, sound triggers are declared by game items so they
+ /// can be linked to a . During runtime, they
+ /// are used to identify which sound to play.
+ ///
+ public struct SoundTrigger
+ {
+ ///
+ /// The ID of the trigger. When you change the ID of a trigger,
+ /// all already associated triggers will be cleared.
+ ///
+ public string Id;
+
+ ///
+ /// Name of the trigger, used for display purposes only.
+ ///
+ public string Name;
+ }
+}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundTrigger.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundTrigger.cs.meta
new file mode 100644
index 000000000..523a6340d
--- /dev/null
+++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundTrigger.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 82f302a40f4f455d819e782d1f1a6127
+timeCreated: 1677678720
\ No newline at end of file
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
index b8eeffd50..98399a238 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperApi.cs
@@ -165,6 +165,8 @@ void IApiHittable.OnHit(int ballId, bool isUnHit)
OnSwitch(true);
}
}
+
+ MainComponent.EmitSound(BumperComponent.SoundBumperHit);
}
#endregion
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperComponent.cs
index b5c66b8db..9225fdaa1 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Bumper/BumperComponent.cs
@@ -34,7 +34,7 @@ namespace VisualPinball.Unity
{
[AddComponentMenu("Visual Pinball/Game Item/Bumper")]
public class BumperComponent : MainRenderableComponent,
- ISwitchDeviceComponent, ICoilDeviceComponent, IOnSurfaceComponent
+ ISwitchDeviceComponent, ICoilDeviceComponent, IOnSurfaceComponent, ISoundEmitter
{
#region Data
@@ -81,6 +81,22 @@ public class BumperComponent : MainRenderableComponent,
public const float DataMeshScale = 100f;
public const string SocketSwitchItem = "socket_switch";
+ public const string SoundBumperHit = "sound_bumper_hit";
+
+ #endregion
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundBumperHit, Name = "Bumper Hit" }
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
#endregion
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs
index 498b264db..b8c27b0c0 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs
@@ -102,6 +102,8 @@ private void OnResetCoilEnabled()
foreach (var dropTargetApi in _dropTargetApis) {
dropTargetApi.IsDropped = false;
}
+
+ _dropTargetBankComponent.EmitSound(DropTargetBankComponent.SoundTargetBankReset);
}
void IApi.OnDestroy()
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankComponent.cs
index b030c0a8e..c52bc7133 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankComponent.cs
@@ -24,12 +24,14 @@ namespace VisualPinball.Unity
{
[AddComponentMenu("Visual Pinball/Mechs/Drop Target Bank")]
[HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/drop-target-banks.html")]
- public class DropTargetBankComponent : MonoBehaviour, ICoilDeviceComponent, ISwitchDeviceComponent
+ public class DropTargetBankComponent : MonoBehaviour, ICoilDeviceComponent, ISwitchDeviceComponent, ISoundEmitter
{
public const string ResetCoilItem = "reset_coil";
public const string SequenceCompletedSwitchItem = "sequence_completed_switch";
+ public const string SoundTargetBankReset = "sound_target_bank_reset";
+
[ToolboxItem("The number of the drop targets. See documentation of a description of each type.")]
public int BankSize = 1;
@@ -71,5 +73,20 @@ private void Awake()
}
#endregion
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundTargetBankReset, Name = "Sound Target Bank Reset" }
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
+ #endregion
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs
index 25cae1a33..a330524b9 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperApi.cs
@@ -15,7 +15,6 @@
// along with this program. If not, see .
// ReSharper disable EventNeverSubscribedTo.Global
-#pragma warning disable 67
using System;
using Unity.Mathematics;
@@ -35,7 +34,6 @@ public class FlipperApi : CollidableApi
public event EventHandler Init;
-
///
/// Event emitted when the flipper was touched by the ball, but did
/// not collide.
@@ -96,6 +94,8 @@ public void RotateToEnd()
state.Movement.StartRotateToEndTime = PhysicsEngine.TimeMsec;
state.Movement.AngleAtRotateToEnd = state.Movement.Angle;
state.Solenoid.Value = true;
+
+ MainComponent.EmitSound(FlipperComponent.SoundCoilOn, MainComponent.RotatePosition);
}
///
@@ -107,6 +107,8 @@ public void RotateToStart()
ref var state = ref PhysicsEngine.FlipperState(ItemId);
state.Movement.EnableRotateEvent = -1;
state.Solenoid.Value = false;
+
+ MainComponent.EmitSound(FlipperComponent.SoundCoilOff);
}
internal ref FlipperState State => ref PhysicsEngine.FlipperState(ItemId);
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs
index 3ee02ad77..22f85af3c 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs
@@ -23,8 +23,8 @@
using System;
using System.Collections.Generic;
+using Unity.Collections;
using Unity.Mathematics;
-using UnityEditor;
using UnityEngine;
using VisualPinball.Engine.Game.Engines;
using VisualPinball.Engine.Math;
@@ -39,7 +39,7 @@ namespace VisualPinball.Unity
[HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/flippers.html")]
public class FlipperComponent : MainRenderableComponent,
IFlipperData, ISwitchDeviceComponent, ICoilDeviceComponent, IOnSurfaceComponent,
- IRotatableComponent
+ IRotatableComponent, ISoundEmitter
{
#region Data
@@ -130,8 +130,32 @@ public class FlipperComponent : MainRenderableComponent,
public const string HoldCoilItem = "hold_coil";
public const string EosSwitchItem = "eos_switch";
+ public const string SoundCoilOn = "sound_coil_on";
+ public const string SoundCoilOff = "sound_coil_off";
+ public const string SoundCoilCollision = "sound_ball_collision";
+
+ #endregion
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundCoilOn, Name = "Coil On" },
+ new SoundTrigger { Id = SoundCoilOff, Name = "Coil Off"},
+ new SoundTrigger { Id = SoundCoilCollision, Name = "Ball Collision" },
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
#endregion
+
+
+
#region Wiring
public IEnumerable AvailableSwitches => new[] {
@@ -195,6 +219,18 @@ public float2 RotatedPosition {
}
}
+ ///
+ /// Returns the current position of the flipper between 0 and 1, where 0 is the
+ /// start position, and 1 the end position.
+ ///
+ public float RotatePosition {
+ get {
+ var start = (_startAngle + 360) % 360;
+ var end = (EndAngle + 360) % 360;
+ return 1 - (transform.localEulerAngles.y - start) / (end - start);
+ }
+ }
+
#endregion
#region Conversion
@@ -359,7 +395,7 @@ protected void OnDrawGizmosSelected()
}
Gizmos.matrix = Matrix4x4.identity;
- Handles.matrix = Matrix4x4.identity;
+ UnityEditor.Handles.matrix = Matrix4x4.identity;
// Draw enclosing polygon
Gizmos.color = Color.cyan;
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs
index 1764216ce..0d7b1f163 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetApi.cs
@@ -126,6 +126,15 @@ void IApi.OnDestroy()
void IApiHittable.OnHit(int ballId, bool _)
{
Hit?.Invoke(this, new HitEventArgs(ballId));
+
+ MainComponent.EmitSound(TargetComponent.SoundTargetHit);
+ }
+ void IApiDroppable.OnDropStatusChanged(bool isDropped, int ballId)
+ {
+ if (!isDropped)
+ {
+ MainComponent.EmitSound(DropTargetComponent.SoundTargetReset);
+ }
}
#endregion
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs
index 39d88c32e..8662ce5ab 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/DropTargetComponent.cs
@@ -42,6 +42,12 @@ protected override float ZOffset {
}
}
+ #region Overrides and Constants
+
+ public const string SoundTargetReset = "sound_target_reset";
+
+ #endregion
+
#region Conversion
public override IEnumerable SetData(HitTargetData data)
@@ -175,5 +181,14 @@ internal DropTargetState CreateState()
}
#endregion
+
+ #region ISoundEmitter
+
+ public override SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundTargetHit, Name = "Target Drop" },
+ new SoundTrigger { Id = SoundTargetReset, Name = "Target Reset" },
+ };
+
+ #endregion
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs
index 65b8c2b21..9ad68e2e2 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/HitTargetApi.cs
@@ -85,6 +85,8 @@ void IApiHittable.OnHit(int ballId, bool _)
Hit?.Invoke(this, new HitEventArgs(ballId));
Switch?.Invoke(this, new SwitchEventArgs(true, ballId));
OnSwitch(true);
+
+ MainComponent.EmitSound(TargetComponent.SoundTargetHit);
}
#endregion
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/TargetComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/TargetComponent.cs
index a9ff6a93d..bbe593584 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/TargetComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/HitTarget/TargetComponent.cs
@@ -35,7 +35,7 @@
namespace VisualPinball.Unity
{
public abstract class TargetComponent : MainRenderableComponent,
- ISwitchDeviceComponent, ITargetData, IMeshGenerator
+ ISwitchDeviceComponent, ITargetData, IMeshGenerator, ISoundEmitter
{
#region Data
@@ -94,6 +94,8 @@ public Matrix3D GetTransformationMatrix()
public const string SwitchItem = "target_switch";
+ public const string SoundTargetHit = "sound_target_hit";
+
#endregion
#region Wiring
@@ -193,5 +195,20 @@ public override void CopyFromObject(GameObject go)
public override void SetEditorScale(Vector3 scale) => Size = scale;
#endregion
+
+ #region ISoundEmitter
+
+ public virtual SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundTargetHit, Name = "Target Hit" },
+ };
+
+ public event EventHandler OnSound;
+
+ internal virtual void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
+ #endregion
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs
index b78b51141..292416ec2 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs
@@ -158,6 +158,8 @@ private void OnBallDestroyed()
if (kickerState.Collision.HasBall) {
kickerState.Collision.BallId = 0;
}
+
+ MainComponent.EmitSound(KickerComponent.SoundKickerDrain);
}
#endregion
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs
index 872819dde..c0bfcdbc2 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerComponent.cs
@@ -41,7 +41,7 @@ namespace VisualPinball.Unity
[AddComponentMenu("Visual Pinball/Game Item/Kicker")]
public class KickerComponent : MainRenderableComponent,
ICoilDeviceComponent, ITriggerComponent, IBallCreationPosition, IOnSurfaceComponent,
- IRotatableComponent, ISerializationCallbackReceiver
+ IRotatableComponent, ISerializationCallbackReceiver, ISoundEmitter
{
#region Data
@@ -85,6 +85,25 @@ public class KickerComponent : MainRenderableComponent,
public const string SwitchItem = "kicker_switch";
+ public const string SoundKickerDrain = "sound_kicker_drain";
+ public const string SoundKickerBallRelease = "sound_kicker_ball_release";
+
+ #endregion
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundKickerDrain, Name = "Ball Drain" },
+ new SoundTrigger { Id = SoundKickerBallRelease, Name = "Ball Release" },
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
#endregion
#region Wiring
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs
index dceb394d8..40da3b6f8 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs
@@ -43,8 +43,8 @@ public IApiSwitch Switch(string deviceItem)
ScoreMotorComponent.MotorRunningSwitchItem => _motorRunningSwitch,
ScoreMotorComponent.MotorStepSwitchItem => _motorStepSwitch,
_ => throw new ArgumentException($"Unknown switch \"{deviceItem}\". "
- + "Valid names are \"{ScoreReelDisplayComponent.MotorRunningSwitchItem}\", and "
- + "\"{ScoreReelDisplayComponent.MotorStepSwitchItem}\".")
+ + $"Valid names are \"{ScoreMotorComponent.MotorRunningSwitchItem}\", and "
+ + $"\"{ScoreMotorComponent.MotorStepSwitchItem}\".")
};
}
@@ -57,6 +57,8 @@ internal ScoreMotorApi(GameObject go, Player player, PhysicsEngine physicsEngine
_scoreMotorComponent.OnSwitchChanged += HandleSwitchChanged;
}
+ #region Events
+
void IApi.OnInit(BallManager ballManager)
{
_motorRunningSwitch = new DeviceSwitch(ScoreMotorComponent.MotorRunningSwitchItem, false, SwitchDefault.NormallyOpen, _player, _physicsEngine);
@@ -67,7 +69,17 @@ void IApi.OnInit(BallManager ballManager)
private void HandleSwitchChanged(object sender, SwitchEventArgs2 e)
{
- ((DeviceSwitch)Switch(e.Id)).SetSwitch(e.IsEnabled);
+ var deviceSwitch = (DeviceSwitch)Switch(e.Id);
+ deviceSwitch.SetSwitch(e.IsEnabled);
+
+ if (deviceSwitch == _motorStepSwitch && e.IsEnabled) {
+ _scoreMotorComponent.EmitSound(ScoreMotorComponent.SoundScoreMotorStep);
+ } else if (deviceSwitch == _motorRunningSwitch) {
+ if (e.IsEnabled)
+ _scoreMotorComponent.EmitSound(ScoreMotorComponent.SoundScoreMotorStart);
+ else
+ _scoreMotorComponent.EmitSound(ScoreMotorComponent.SoundScoreMotorStop);
+ }
}
void IApi.OnDestroy()
@@ -76,5 +88,7 @@ void IApi.OnDestroy()
Logger.Info($"Destroying {_scoreMotorComponent.name}");
}
+
+ #endregion
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs
index 9cf911f60..a412a737b 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorComponent.cs
@@ -22,6 +22,7 @@
using UnityEngine;
using VisualPinball.Engine.Game.Engines;
using VisualPinball.Engine.VPT.Gate;
+using VisualPinball.Engine.VPT.Sound;
using Logger = NLog.Logger;
namespace VisualPinball.Unity
@@ -31,7 +32,7 @@ namespace VisualPinball.Unity
[AddComponentMenu("Visual Pinball/Mechs/Score Motor")]
[HelpURL("https://docs.visualpinball.org/creators-guide/manual/mechanisms/score-motors.html")]
- public class ScoreMotorComponent : MonoBehaviour, ISwitchDeviceComponent
+ public class ScoreMotorComponent : MonoBehaviour, ISwitchDeviceComponent, ISoundEmitter
{
public const int MaxIncrease = 5;
@@ -57,6 +58,10 @@ public class ScoreMotorComponent : MonoBehaviour, ISwitchDeviceComponent
public const string MotorRunningSwitchItem = "motor_running_switch";
public const string MotorStepSwitchItem = "motor_step_switch";
+ public const string SoundScoreMotorStart = "sound_score_motor_start";
+ public const string SoundScoreMotorStop = "sound_score_motor_stop";
+ public const string SoundScoreMotorStep = "sound_score_motor_step";
+
public IEnumerable AvailableSwitches => new[] {
new GamelogicEngineSwitch(MotorRunningSwitchItem)
{
@@ -266,6 +271,25 @@ private float ResetScore(float score)
}
#endregion
+
+
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundScoreMotorStart, Name = "Sound Score Motor Start" },
+ new SoundTrigger { Id = SoundScoreMotorStop, Name = "Sound Score Motor Stop" },
+ new SoundTrigger { Id = SoundScoreMotorStep, Name = "Sound Score Motor Step" }
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
+ #endregion
}
[Serializable]
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs
index 328d9f430..2e67d87a9 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerApi.cs
@@ -97,6 +97,8 @@ public void PullBack()
} else {
PlungerCommands.PullBack(collComponent.SpeedPull, ref plungerState.Velocity, ref plungerState.Movement);
}
+
+ MainComponent.EmitSound(PlungerComponent.SoundPlungerPull);
}
public void Fire()
@@ -125,6 +127,8 @@ public void Fire()
var pos = (plungerState.Movement.Position - plungerState.Static.FrameEnd) / (plungerState.Static.FrameStart - plungerState.Static.FrameEnd);
PlungerCommands.Fire(pos, ref plungerState.Velocity, ref plungerState.Movement, in plungerState.Static);
}
+
+ MainComponent.EmitSound(PlungerComponent.SoundPlungerRelease);
}
IApiCoil IApiCoilDevice.Coil(string deviceItem) => Coil(deviceItem);
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerComponent.cs
index 6bee71fe9..df660454a 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Plunger/PlungerComponent.cs
@@ -32,7 +32,7 @@ namespace VisualPinball.Unity
{
[AddComponentMenu("Visual Pinball/Game Item/Plunger")]
public class PlungerComponent : MainRenderableComponent,
- ICoilDeviceComponent, IOnSurfaceComponent
+ ICoilDeviceComponent, IOnSurfaceComponent, ISoundEmitter
{
#region Data
@@ -70,6 +70,25 @@ public class PlungerComponent : MainRenderableComponent,
public const string PullCoilId = "c_pull";
public const string FireCoilId = "c_autofire";
+ public const string SoundPlungerPull = "sound_plunger_pull";
+ public const string SoundPlungerRelease = "sound_plunger_release";
+
+ #endregion
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundPlungerPull, Name = "Plunger Pull" },
+ new SoundTrigger { Id = SoundPlungerRelease, Name = "Plunger Release"}
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
#endregion
#region Runtime
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs
index a867681e5..d1935877c 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerApi.cs
@@ -92,10 +92,14 @@ void IApiHittable.OnHit(int ballId, bool isUnHit)
Switch?.Invoke(this, new SwitchEventArgs(false, ballId));
OnSwitch(false);
+ MainComponent.EmitSound(TriggerComponent.SoundTriggerUnhit);
+
} else {
Hit?.Invoke(this, new HitEventArgs(ballId));
Switch?.Invoke(this, new SwitchEventArgs(true, ballId));
OnSwitch(true);
+
+ MainComponent.EmitSound(TriggerComponent.SoundTriggerHit);
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs
index 2205b7d63..8cabfe900 100644
--- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs
@@ -37,7 +37,7 @@ namespace VisualPinball.Unity
{
[AddComponentMenu("Visual Pinball/Game Item/Trigger")]
public class TriggerComponent : MainRenderableComponent,
- ITriggerComponent, IOnSurfaceComponent
+ ITriggerComponent, IOnSurfaceComponent, ISoundEmitter
{
#region Data
@@ -77,6 +77,8 @@ public class TriggerComponent : MainRenderableComponent,
protected override Type ColliderComponentType { get; } = typeof(ColliderComponent);
public const string SwitchItem = "trigger_switch";
+ public const string SoundTriggerHit = "sound_trigger_hit";
+ public const string SoundTriggerUnhit = "sound_trigger_unhit";
#endregion
@@ -324,5 +326,21 @@ public override ItemDataTransformType EditorRotationType{
public override void SetEditorRotation(Vector3 rot) => Rotation = ClampDegrees(rot.x);
#endregion
+
+ #region ISoundEmitter
+
+ public SoundTrigger[] AvailableTriggers => new[] {
+ new SoundTrigger { Id = SoundTriggerHit, Name = "Sound Trigger Hit" },
+ new SoundTrigger { Id = SoundTriggerUnhit, Name = "Sound Trigger Unhit" }
+ };
+
+ public event EventHandler OnSound;
+
+ internal void EmitSound(string triggerId, float volume = 1)
+ {
+ OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume));
+ }
+
+ #endregion
}
}