diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundsInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundsInspector.cs index 66e38ca81..9a7d983ef 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundsInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Sound/MechSoundsInspector.cs @@ -22,17 +22,15 @@ namespace VisualPinball.Unity.Editor [CustomEditor(typeof(MechSoundsComponent)), CanEditMultipleObjects] public class MechanicalSoundInspector : UnityEditor.Editor { + private SerializedProperty _audioMixerProperty; private SerializedProperty _soundsProperty; private void OnEnable() { + _audioMixerProperty = serializedObject.FindProperty(nameof(MechSoundsComponent.AudioMixer)); _soundsProperty = serializedObject.FindProperty(nameof(MechSoundsComponent.Sounds)); var comp = target as MechSoundsComponent; - var audioSource = comp!.GetComponent(); - if (audioSource != null) { - audioSource.playOnAwake = false; - } } public override void OnInspectorGUI() @@ -45,16 +43,19 @@ public override void OnInspectorGUI() return; } - var audioSource = comp.GetComponent(); - if (audioSource == null) { - EditorGUILayout.HelpBox("Cannot find audio source. This component only works with an audio source on the same GameObject.", MessageType.Error); - return; - } - serializedObject.Update(); EditorGUILayout.PropertyField(_soundsProperty); + // unity doesnt use default values when adding items in a list so force it (Volume=1) + if (GUILayout.Button("Add New MechSound")) + { + comp.Sounds.Add(new MechSound()); + EditorUtility.SetDirty(comp); + } + + EditorGUILayout.PropertyField(_audioMixerProperty); + serializedObject.ApplyModifiedProperties(); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs index 3f328045b..ca84b8fe8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSound.cs @@ -30,6 +30,7 @@ public class MechSound public string TriggerId; [Range(0.0001f, 1)] + // this initialization doesnt work in inspector https://www.reddit.com/r/Unity3D/comments/j5i6cj/inspector_struct_default_values/ public float Volume = 1; public MechSoundAction Action = MechSoundAction.Play; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs index 96b1948fa..2abd13b4b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechSoundsComponent.cs @@ -27,18 +27,19 @@ namespace VisualPinball.Unity { [AddComponentMenu("Visual Pinball/Sounds/Mechanical Sounds")] - [RequireComponent(typeof(AudioSource))] public class MechSoundsComponent : MonoBehaviour { [SerializeField] public List Sounds = new(); - + + [SerializeField] + [Tooltip("If left blank, looks for an Audio Mixer in closest parent up the hierarchy.")] + public AudioMixerGroup AudioMixer; + [NonSerialized] private ISoundEmitter _soundEmitter; [NonSerialized] - private AudioSource _audioSource; - [NonSerialized] - private Dictionary _sounds = new(); + private SerializableDictionary _audioSources = new SerializableDictionary(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private Coroutine _co; @@ -46,17 +47,29 @@ public class MechSoundsComponent : MonoBehaviour private void Awake() { _soundEmitter = GetComponent(); - _audioSource = GetComponent(); - - _sounds = Sounds.ToDictionary(s => s.TriggerId, s => s); } private void Start() { - if (_soundEmitter != null && _audioSource) { + if (AudioMixer == null) + { + // find an Audio Mixer by searching up the hierarchy + AudioSource audioSource = GetComponentInParent(); + if (audioSource != null) + { + AudioMixer = audioSource.outputAudioMixerGroup; + } + else + { + Logger.Warn($"Sounds will not play without an Audio Mixer."); + } + } + + if (_soundEmitter != null) { _soundEmitter.OnSound += EmitSound; } else { + // ? is AudioSource really a dependency here?? Logger.Warn($"Cannot initialize mech sound for {name} due to missing ISoundEmitter or AudioSource."); } } @@ -70,10 +83,36 @@ private void OnDestroy() private void EmitSound(object sender, SoundEventArgs e) { + int clipCount = 0; + + foreach(MechSound sound in Sounds) + { + // filter for the TriggerId + if (sound.TriggerId != e.TriggerId) continue; - if (_sounds.ContainsKey(e.TriggerId)) { + // get or create the AudioSource + AudioSource audioSource; + if (_audioSources.ContainsKey(sound.Sound)) + { + audioSource = _audioSources[sound.Sound]; + } + else + { + audioSource = gameObject.AddComponent(); + audioSource.outputAudioMixerGroup = AudioMixer; + _audioSources.Add(sound.Sound, audioSource); + } - float fade = _sounds[e.TriggerId].Fade; + if (sound.Action == MechSoundAction.Stop) + { + sound.Sound.Stop(audioSource); + Debug.Log($"Stopping sound {e.TriggerId} for {name}"); + // we're done + continue; + } + + // else sound.Action == MechSoundAction.Play + float fade = sound.Fade; bool fadeVolume = false; //convert fade duration from milliseconds to seconds for use with StartFade method @@ -91,7 +130,7 @@ private void EmitSound(object sender, SoundEventArgs e) float volume = e.Volume; AudioMixer audioMixer = GetComponent().outputAudioMixerGroup.audioMixer; - _sounds[e.TriggerId].Sound.Play(_audioSource, volume); + sound.Sound.Play(audioSource, volume); /* set audio mixer volume to decibel equivalent of volume slider value mixer volume is set at 0 dB when added to audiosource @@ -115,9 +154,6 @@ volume of 1 in slider is equivalent to 0 dB Debug.Log($"Playing sound {e.TriggerId} for {name}"); - - } else { - Debug.LogError($"Unknown trigger {e.TriggerId} for {name}"); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs index 498b264db..13a283900 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankApi.cs @@ -18,11 +18,12 @@ using Logger = NLog.Logger; using NLog; using UnityEngine; +using VisualPinball.Engine.VPT; using System.Collections.Generic; namespace VisualPinball.Unity { - public class DropTargetBankApi : IApi, IApiCoilDevice, IApiSwitchDevice + public class DropTargetBankApi : ItemApi, IApi, IApiCoilDevice, IApiSwitchDevice { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -57,7 +58,7 @@ private IApiCoil Coil(string deviceItem) }; } - internal DropTargetBankApi(GameObject go, Player player, PhysicsEngine physicsEngine) + internal DropTargetBankApi(GameObject go, Player player, PhysicsEngine physicsEngine) : base(go, player, physicsEngine) { _dropTargetBankComponent = go.GetComponentInChildren(); _player = player; @@ -102,6 +103,9 @@ private void OnResetCoilEnabled() foreach (var dropTargetApi in _dropTargetApis) { dropTargetApi.IsDropped = false; } + + // ? is this where this goes? + MainComponent.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..a2d85d315 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/DropTargetBank/DropTargetBankComponent.cs @@ -17,19 +17,25 @@ using System.Collections.Generic; using UnityEngine; using VisualPinball.Engine.Game.Engines; +using VisualPinball.Engine.VPT; using System.ComponentModel; using System; +using VisualPinball.Engine.VPT.HitTarget; +using VisualPinball.Engine.IO; +using VisualPinball.Engine.VPT.Table; 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 : MainRenderableComponent, 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 +77,55 @@ private void Awake() } #endregion + + #region ISoundEmitter + + public SoundTrigger[] AvailableTriggers => new[] { + new SoundTrigger { Id = SoundTargetBankReset, Name = "Sound Target Bank Reset" } + }; + + protected override Type MeshComponentType => throw new NotImplementedException(); + + protected override Type ColliderComponentType => throw new NotImplementedException(); + + public override bool HasProceduralMesh => throw new NotImplementedException(); + + public override ItemType ItemType => throw new NotImplementedException(); + + public override string ItemName => throw new NotImplementedException(); + + public event EventHandler OnSound; + + internal void EmitSound(string triggerId, float volume = 1) + { + OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume)); + } + + public override void CopyFromObject(GameObject go) + { + throw new NotImplementedException(); + } + + public override IEnumerable SetData(ItemData data) + { + throw new NotImplementedException(); + } + + public override IEnumerable SetReferencedData(ItemData data, Table table, IMaterialProvider materialProvider, ITextureProvider textureProvider, Dictionary components) + { + throw new NotImplementedException(); + } + + public override ItemData CopyDataTo(ItemData data, string[] materialNames, string[] textureNames, bool forExport) + { + throw new NotImplementedException(); + } + + public override ItemData InstantiateData() + { + throw new NotImplementedException(); + } + + #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs index e2b91f640..22f85af3c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs @@ -151,21 +151,11 @@ internal void EmitSound(string triggerId, float volume = 1) OnSound?.Invoke(this, new SoundEventArgs(triggerId, volume)); } - /// - /// 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 Wiring public IEnumerable AvailableSwitches => new[] { @@ -229,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 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/Mech/ScoreMotorApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs index dceb394d8..5c658b24a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Mech/ScoreMotorApi.cs @@ -17,10 +17,12 @@ using System; using NLog; using UnityEngine; +using VisualPinball.Engine.VPT; using Logger = NLog.Logger; namespace VisualPinball.Unity { + // public class ScoreMotorApi : ItemApi, IApi, IApiSwitchDevice public class ScoreMotorApi : IApi, IApiSwitchDevice { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); @@ -48,6 +50,7 @@ public IApiSwitch Switch(string deviceItem) }; } + // internal ScoreMotorApi(GameObject go, Player player, PhysicsEngine physicsEngine) : base(go, player, physicsEngine) internal ScoreMotorApi(GameObject go, Player player, PhysicsEngine physicsEngine) { _scoreMotorComponent = go.GetComponentInChildren(); @@ -57,6 +60,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); @@ -68,6 +73,11 @@ void IApi.OnInit(BallManager ballManager) private void HandleSwitchChanged(object sender, SwitchEventArgs2 e) { ((DeviceSwitch)Switch(e.Id)).SetSwitch(e.IsEnabled); + + //if (e.IsEnabled) + //{ + // MainComponent.EmitSound(ScoreMotorComponent.SoundScoreClear); + //} } void IApi.OnDestroy() @@ -76,5 +86,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..0e7ab0886 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,6 +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, ISoundEmitter public class ScoreMotorComponent : MonoBehaviour, ISwitchDeviceComponent { public const int MaxIncrease = 5; @@ -57,6 +59,9 @@ public class ScoreMotorComponent : MonoBehaviour, ISwitchDeviceComponent public const string MotorRunningSwitchItem = "motor_running_switch"; public const string MotorStepSwitchItem = "motor_step_switch"; + public const string SoundScoreClear = "sound_score_clear"; + //public const string SoundScoreMotorStep = "sound_score_step"; + public IEnumerable AvailableSwitches => new[] { new GamelogicEngineSwitch(MotorRunningSwitchItem) { @@ -266,6 +271,23 @@ private float ResetScore(float score) } #endregion + + + + //#region ISoundEmitter + + //public SoundTrigger[] AvailableTriggers => new[] { + // new SoundTrigger { Id = SoundScoreClear, Name = "Sound Score Clear" } + //}; + + //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/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 } }