jamesperet пре 3 година
комит
715582926f

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# This .gitignore file should be placed at the root of your Unity project directory
+#
+# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
+#
+/[Ll]ibrary/
+/[Tt]emp/
+/[Oo]bj/
+/[Bb]uild/
+/[Bb]uilds/
+/[Ll]ogs/
+/[Uu]ser[Ss]ettings/
+
+# MemoryCaptures can get excessive in size.
+# They also could contain extremely sensitive data
+/[Mm]emoryCaptures/
+
+# Asset meta data should only be ignored when the corresponding asset is also ignored
+!/[Aa]ssets/**/*.meta
+
+# Uncomment this line if you wish to ignore the asset store tools plugin
+# /[Aa]ssets/AssetStoreTools*
+
+# Autogenerated Jetbrains Rider plugin
+/[Aa]ssets/Plugins/Editor/JetBrains*
+
+# Visual Studio cache directory
+.vs/
+
+# Gradle cache directory
+.gradle/
+
+# Autogenerated VS/MD/Consulo solution and project files
+ExportedObj/
+.consulo/
+*.csproj
+*.unityproj
+*.sln
+*.suo
+*.tmp
+*.user
+*.userprefs
+*.pidb
+*.booproj
+*.svd
+*.pdb
+*.mdb
+*.opendb
+*.VC.db
+
+# Unity3D generated meta files
+*.pidb.meta
+*.pdb.meta
+*.mdb.meta
+
+# Unity3D generated file on crash reports
+sysinfo.txt
+
+# Builds
+*.apk
+*.aab
+*.unitypackage
+
+# Crashlytics generated file
+crashlytics-build.properties
+
+# Packed Addressables
+/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
+
+# Temporary auto-generated Android Assets
+/[Aa]ssets/[Ss]treamingAssets/aa.meta
+/[Aa]ssets/[Ss]treamingAssets/aa/*

+ 8 - 0
Prefabs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f9fdf5cce903afa4d9648d42989cdb19
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 161 - 0
Prefabs/SoundEmmiterPrefab.prefab

@@ -0,0 +1,161 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &8894808049857426241
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 9008372337273690662}
+  - component: {fileID: 8146855551636768750}
+  - component: {fileID: 843770249315000744}
+  - component: {fileID: 3782178483463691228}
+  m_Layer: 0
+  m_Name: SoundEmmiterPrefab
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &9008372337273690662
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8894808049857426241}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 1093.3066, y: 474.2215, z: -1808.875}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &8146855551636768750
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8894808049857426241}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 5775068f97aa254458719742adabb71a, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  trigger: {fileID: 3782178483463691228}
+  audioSource: {fileID: 843770249315000744}
+  audioClip: {fileID: 0}
+  sfx: {fileID: 0}
+  soundEmitterPrefabPool: 
+--- !u!82 &843770249315000744
+AudioSource:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8894808049857426241}
+  m_Enabled: 1
+  serializedVersion: 4
+  OutputAudioMixerGroup: {fileID: 0}
+  m_audioClip: {fileID: 0}
+  m_PlayOnAwake: 1
+  m_Volume: 1
+  m_Pitch: 1
+  Loop: 0
+  Mute: 0
+  Spatialize: 0
+  SpatializePostEffects: 0
+  Priority: 128
+  DopplerLevel: 1
+  MinDistance: 1
+  MaxDistance: 500
+  Pan2D: 0
+  rolloffMode: 0
+  BypassEffects: 0
+  BypassListenerEffects: 0
+  BypassReverbZones: 0
+  rolloffCustomCurve:
+    serializedVersion: 2
+    m_Curve:
+    - serializedVersion: 3
+      time: 0
+      value: 1
+      inSlope: 0
+      outSlope: 0
+      tangentMode: 0
+      weightedMode: 0
+      inWeight: 0.33333334
+      outWeight: 0.33333334
+    - serializedVersion: 3
+      time: 1
+      value: 0
+      inSlope: 0
+      outSlope: 0
+      tangentMode: 0
+      weightedMode: 0
+      inWeight: 0.33333334
+      outWeight: 0.33333334
+    m_PreInfinity: 2
+    m_PostInfinity: 2
+    m_RotationOrder: 4
+  panLevelCustomCurve:
+    serializedVersion: 2
+    m_Curve:
+    - serializedVersion: 3
+      time: 0
+      value: 0
+      inSlope: 0
+      outSlope: 0
+      tangentMode: 0
+      weightedMode: 0
+      inWeight: 0.33333334
+      outWeight: 0.33333334
+    m_PreInfinity: 2
+    m_PostInfinity: 2
+    m_RotationOrder: 4
+  spreadCustomCurve:
+    serializedVersion: 2
+    m_Curve:
+    - serializedVersion: 3
+      time: 0
+      value: 0
+      inSlope: 0
+      outSlope: 0
+      tangentMode: 0
+      weightedMode: 0
+      inWeight: 0.33333334
+      outWeight: 0.33333334
+    m_PreInfinity: 2
+    m_PostInfinity: 2
+    m_RotationOrder: 4
+  reverbZoneMixCustomCurve:
+    serializedVersion: 2
+    m_Curve:
+    - serializedVersion: 3
+      time: 0
+      value: 1
+      inSlope: 0
+      outSlope: 0
+      tangentMode: 0
+      weightedMode: 0
+      inWeight: 0.33333334
+      outWeight: 0.33333334
+    m_PreInfinity: 2
+    m_PostInfinity: 2
+    m_RotationOrder: 4
+--- !u!135 &3782178483463691228
+SphereCollider:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8894808049857426241}
+  m_Material: {fileID: 0}
+  m_IsTrigger: 0
+  m_Enabled: 1
+  serializedVersion: 2
+  m_Radius: 0.5
+  m_Center: {x: 0, y: 0, z: 0}

+ 7 - 0
Prefabs/SoundEmmiterPrefab.prefab.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 11f3d36bb078e034aa99a08d5a801014
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 28 - 0
Readme.md

@@ -0,0 +1,28 @@
+# 📦 KairoEngine.SFX.v0.1.2
+
+
+
+### 🛑Required packages
+
+- `KairoEngine.Core`
+- Editor Coroutines v1.0.0 (unity package)
+
+### 📄Namespaces
+
+- `KairoEngine.SFX`
+
+### 📙Modules
+
+### 🔷Components
+
+### ✔Getting Started
+
+### 📚Explanation
+
+### 📚Tutorials
+
+### 📚Reference
+
+### 🧰Functions
+
+### 🎈Back Log

+ 7 - 0
Readme.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 33fb24e4ec8307b4399a01d19aba468d
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3217a22bd9e10be40b433820679cd328
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/GameActions.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a7b41da0ab5db6b45adeaa0229b6e3bc
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 120 - 0
Runtime/GameActions/PlaySFXClipGameAction.cs

@@ -0,0 +1,120 @@
+using System;
+using System.Collections;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core;
+using KairoEngine.Core.GameActions;
+
+namespace KairoEngine.SFX.GameActions
+{
+
+    [System.Serializable, HideReferenceObjectPicker]
+    public class PlaySFXClipGameAction : GameActionBase
+    {
+        public override string name 
+        { 
+            get
+            {
+                return $"Play {(sfxClip != null ? sfxClip.name : "SFX Clip")}"; 
+            }
+        }
+
+        public override GameActionsController controller { 
+            get => _controller; 
+            set 
+            {
+                _controller = value;
+                typeName = "PlaySFXClipGameAction";
+                GetCompatibleVariablenames();
+            }
+        }
+        public override string GetTypeName() => "PlaySFXClipGameAction";
+        public override string GetActionName() => "Play SFX Clip";
+
+        [FoldoutGroup("@name")]
+        public SFXClip sfxClip;
+        [FoldoutGroup("@name"), NonSerialized, ShowInInspector, ValueDropdown("possibleVariables", IsUniqueList = false)] public string parent;
+        private IEnumerable possibleVariables = new ValueDropdownList<string>();
+
+        public PlaySFXClipGameAction(GameActionsController controller) : base(controller)
+        {
+            this.controller = controller;
+            className =  this.GetType().AssemblyQualifiedName;
+        }
+
+        public override void Initialize(GameActionObjectSerializer objectSerializer)
+        {
+            var obj = objectSerializer.DeserializeScriptableObject($"{sfxClipName}-sfxClip");
+            if(obj != null && sfxClip == null) sfxClip = (SFXClip)obj;
+        }
+
+        public override void Start()
+        { 
+            GameObject parentObj = GetVariable<GameObject>(parent, null);
+            Vector3 pos = new Vector3();
+            if(parentObj != null) pos = parentObj.transform.position;
+            SoundController.EmmitSound(sfxClip, pos);
+            _done = true;
+            _started = true;
+        }
+
+        public override void Update() { }
+
+        public override void Restart()
+        {
+            _done = false;
+            _started = false;
+        }
+
+        public static PlaySFXClipGameAction JSONToPlaySFXClipGameAction(string data)
+        {
+            return JsonUtility.FromJson<PlaySFXClipGameAction>(data);
+        }
+
+        private PlaySFXClipGameAction Duplicate(GameActionsController controller)
+        {
+            PlaySFXClipGameAction action = new PlaySFXClipGameAction(controller == null ? this.controller : controller);
+            action.controller = controller;
+            return action;
+        }
+
+        private T GetVariable<T>(string title, T defaultValue)
+        {
+            for (int i = 0; i < controller.context.variables.Count; i++)
+            {
+                if(controller.context.variables[i].name == title)
+                {
+                    return controller.context.variables[i].GetValue<T>(defaultValue);
+                }
+            }
+            return defaultValue;
+        }
+
+        [HideInInspector] public string sfxClipName;
+
+        public override void OnBeforeSerialize(GameActionObjectSerializer serializer, int n, int depth) 
+        { 
+            if(sfxClip != null)
+            {   
+                sfxClipName = sfxClip.name;
+                serializer.SerializeScriptableObject($"{sfxClipName}-sfxClip", sfxClip);
+            }
+        }
+        public override void OnBeforeDeserialize(GameActionObjectSerializer serializer, int n, int depth) 
+        { 
+            
+        }
+
+        private void GetCompatibleVariablenames()
+        {
+            if(_controller == null) return;
+            if(_controller.context == null) return;
+            possibleVariables = _controller.context.variables
+                //.Where(x => x.data is GameActionContextString)
+                .Select(x => new ValueDropdownItem(x.name, x.name)); 
+        }
+    }
+}
+

+ 11 - 0
Runtime/GameActions/PlaySFXClipGameAction.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f3ad772046e85da4584193e9763b49d4
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 17 - 0
Runtime/KairoEngine.SFX.asmdef

@@ -0,0 +1,17 @@
+{
+    "name": "KairoEngine.SFX",
+    "references": [
+        "GUID:7e5ae6a38d1532248b4c890eca668b06",
+        "GUID:478a2357cc57436488a56e564b08d223",
+        "GUID:adb0f400804b2c0448682f502e5eb39b"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Runtime/KairoEngine.SFX.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d5cdde771ecbd5e47bdbe207903a3b3c
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 163 - 0
Runtime/SFXClip.cs

@@ -0,0 +1,163 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Audio;
+using Sirenix.OdinInspector;
+#if UNITY_EDITOR
+using Unity.EditorCoroutines.Editor;
+#endif
+
+namespace KairoEngine.SFX
+{
+    public enum SFXClipType
+    {
+        Single,
+        RandomList,
+        Steps
+    }
+
+    public enum SFXSpace
+    {
+        Stereo,
+        GameWorld,
+        Mix
+    }
+
+    [HideMonoScript]
+    [CreateAssetMenu(menuName = "KairoEngine/SFX/Clip", fileName = "SFXClip", order = 1)]
+    public class SFXClip : ScriptableObject
+    {
+        [Space, Title("Audio Clip")]
+
+        [PropertyOrder(0)] public SFXClipType sfxType = SFXClipType.Single;
+
+        [PropertyOrder(2), Required, ShowIf("@sfxType == SFXClipType.Single")] 
+        public AudioClip clip;
+
+        [PropertyOrder(3), Required, ShowIf("@sfxType == SFXClipType.RandomList")] 
+        public List<AudioClip> clips;
+
+        [PropertyOrder(4), Required, ShowIf("@sfxType == SFXClipType.Steps"), ShowInInspector]
+        public List<SFXClipStep> clipSteps;
+
+
+        #region PlayButtons
+        #if UNITY_EDITOR
+
+        private bool stopSteps = false;
+
+        [PropertyOrder(5), Button("Stop"), HorizontalGroup("Controls")] 
+        public void StopEditor()
+        {
+            if(sfxType == SFXClipType.Single && clip != null) SFXUtil.StopClip(clip);
+            else if(sfxType == SFXClipType.RandomList && clips != null) 
+            {
+                if(clips.Count > 0)
+                {
+                    foreach (var clip in clips) SFXUtil.StopClip(clip);      
+                }
+                
+            }
+            else if(sfxType == SFXClipType.Steps && clipSteps != null) 
+            {
+                stopSteps = true;
+                foreach (var step in clipSteps) step.clip.StopEditor();
+            }
+            SFXUtil.StopAllClips();
+        }
+
+        [PropertyOrder(6), Button("Play"), HorizontalGroup("Controls")] 
+        public void PlayEditor()
+        {
+            if(sfxType == SFXClipType.Single && clip != null) SFXUtil.PlayClip(clip);
+            else if(sfxType == SFXClipType.RandomList && clips != null) 
+            {
+                if(clips.Count > 0)
+                {
+                    SFXUtil.PlayClip(clips[Random.Range(0, clips.Count)]);
+                }
+                
+            }
+            else if(sfxType == SFXClipType.Steps && clipSteps != null) 
+            {
+                stopSteps = false;
+                foreach (var step in clipSteps)
+                {
+                    EditorCoroutineUtility.StartCoroutineOwnerless(PlayStep(step.startTime, step.clip));
+                }
+                
+            }
+        }
+        
+        public IEnumerator PlayStep(float waitTime, SFXClip clip)
+        {
+            bool hasPlayed = false;
+            var waitFor = new EditorWaitForSeconds(waitTime);
+            while (hasPlayed == false)
+            {
+                yield return waitFor;
+                if(stopSteps == false)
+                {
+                    hasPlayed = true;
+                    if(clip.sfxType == SFXClipType.Single) SFXUtil.PlayClip(clip.clip);
+                    else if (clip.sfxType == SFXClipType.RandomList)
+                    {
+                        if(clip.clips != null) 
+                        {
+                            if(clip.clips.Count > 0)
+                            {
+                                SFXUtil.PlayClip(clip.clips[Random.Range(0, clip.clips.Count)]);
+                            }
+                            
+                        }
+                    }
+                }
+                
+            }
+            
+        }
+        
+        #endif
+        #endregion
+        
+        
+        [Title("Clip Settings")]
+
+        [PropertyOrder(7), ShowIf("@sfxType == SFXClipType.Single || sfxType == SFXClipType.RandomList")] public bool loop = false;
+        [PropertyOrder(8), Range(0f, 1f)] public float volume = 1f;
+        [PropertyOrder(9), Range(0f, 0.2f)] public float volumeVariation = 0.05f;
+        [PropertyOrder(10), Range(0f, 2f)] public float pitch = 1f;
+        [PropertyOrder(11), Range(0f, 0.2f)] public float pitchVariation = 0.05f;
+
+        [PropertyOrder(12), HorizontalGroup("Trim"), LabelText("@trim ? \"Trim Start/Stop\" : \"Trim\"")] public bool trim = false;
+        [PropertyOrder(13), HorizontalGroup("Trim", Width = 0.272f), HideLabel, SuffixLabel("seconds", true), ShowIf("@trim")] public float trimStartDuration = 0;
+        [PropertyOrder(14), HorizontalGroup("Trim", Width = 0.272f), HideLabel, SuffixLabel("seconds", true), ShowIf("@trim")] public float trimEndDuration = 0;
+
+        [PropertyOrder(15), HorizontalGroup("FadeIn"), LabelText("@fadeIn ? \"Fade In Duration\" : \"Fade In\"")] public bool fadeIn = false;
+        [PropertyOrder(16), HorizontalGroup("FadeIn", Width = 0.55f), HideLabel, SuffixLabel("seconds", true), ShowIf("@fadeIn")] public float fadeInDuration = 1f;
+        [PropertyOrder(17), HorizontalGroup("FadeOut"), LabelText("@fadeOut ? \"Fade Out Duration\" : \"Fade Out\"")] public bool fadeOut = false;
+        [PropertyOrder(18), HorizontalGroup("FadeOut", Width = 0.55f), HideLabel, SuffixLabel("seconds", true), ShowIf("@fadeOut")] public float fadeOutDuration = 1f;
+
+        [PropertyOrder(19)] public SFXSpace soundSpace = SFXSpace.GameWorld;
+
+        [PropertyOrder(20), Range(-1f, 1f), ShowIf("@soundSpace == SFXSpace.Stereo || soundSpace == SFXSpace.Mix")] public float pan = 0f;
+        [PropertyOrder(21), Range(0f, 0.2f), ShowIf("@soundSpace == SFXSpace.Stereo || soundSpace == SFXSpace.Mix")] public float panVariation = 0.05f;
+        [PropertyOrder(22), Range(0f, 1f), ShowIf("@soundSpace == SFXSpace.Mix")] public float spatialBlend = 0f;
+        [PropertyOrder(23),Range(0f, 0.2f), ShowIf("@soundSpace == SFXSpace.Mix")] public float spatialBlendVariation = 0.05f;
+
+        [PropertyOrder(24), ShowIf("@soundSpace == SFXSpace.GameWorld || soundSpace == SFXSpace.Mix"), LabelText("Distance")]
+        [HorizontalGroup("Distance"), SuffixLabel("min", true)] 
+        public float minDistance = 3f;
+
+        [PropertyOrder(25), ShowIf("@soundSpace == SFXSpace.GameWorld || soundSpace == SFXSpace.Mix"), HideLabel()] 
+        [HorizontalGroup("Distance", Width = 0.3f), SuffixLabel("max", true)] 
+        public float maxDistance = 100f;
+
+        [PropertyOrder(26), Required, HideIf("@sfxType == SFXClipType.Steps")] public AudioMixerGroup audioMixerChannel;
+        [PropertyOrder(27)] public bool removeOnSceneChange = false;
+
+        [Title("World Settings")]
+        [PropertyOrder(28)] public bool sensorDetection = true;
+        [PropertyOrder(29), ShowIf(@"sensorDetection"), Range(0f, 100f)] public float soundRadius = 2f;
+    }
+}

+ 11 - 0
Runtime/SFXClip.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f75cb162c6166c84fb38e8aaa6f171af
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: 3261b2c9388573e42ae49b2dbf7e34e0, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 15 - 0
Runtime/SFXClipStep.cs

@@ -0,0 +1,15 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.SFX
+{
+    [System.Serializable]
+    public class SFXClipStep
+    {
+        [HorizontalGroup("SFXClip"), HideLabel] public SFXClip clip;
+
+        [HorizontalGroup("SFXClip", 50), SuffixLabel("s", true), HideLabel] public float startTime = 0f;
+    }
+}

+ 11 - 0
Runtime/SFXClipStep.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 61c8684bbfed1454aae4f19db3165424
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 35 - 0
Runtime/SFXGroup.cs

@@ -0,0 +1,35 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.SFX
+{
+    [System.Serializable]
+    [HideMonoScript]
+    [CreateAssetMenu(menuName = "KairoEngine/SFX/Group", fileName = "SFXGroup", order = 2)]
+    public class SFXGroup : SerializedScriptableObject
+    {
+        [Space] public Dictionary<PhysicMaterial, SFXClip> clips = new Dictionary<PhysicMaterial, SFXClip>();
+
+        public void Play(PhysicMaterial physicMaterial, Vector3 position)
+        {
+            if(!physicMaterial) 
+            {
+                Debug.LogError($"Missing physicMaterial in call to SFXGroup ({this.name})", this);
+                return;
+            }
+            string materialName = physicMaterial.name.Replace(" (Instance)", "");
+            SFXClip clip = null;
+            foreach(KeyValuePair<PhysicMaterial,SFXClip> item in clips)
+            {
+                if(item.Key.name == materialName) clip = item.Value;
+            }
+            if(clip != null) SoundController.EmmitSound(clip, position);
+            else Debug.LogError($"Missing SFX clip for material \'{materialName}\' in SFXGroup ({this.name})", this);
+
+            
+            
+        }
+    }
+}

+ 11 - 0
Runtime/SFXGroup.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5b32631489c27a1448c96c41c5e8afc4
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 24 - 0
Runtime/SFXManager.cs

@@ -0,0 +1,24 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.SFX
+{
+    public class SFXManager : MonoBehaviour
+    {
+        private static SFXManager _instance;
+        public static SFXManager instance
+        {
+            get
+            {
+                if(_instance == null) _instance = FindObjectOfType<SFXManager>();
+                return _instance;
+            }
+        }
+        
+        [SerializeField] private AudioSource defaultAudioSource;
+
+        public List<SFXClip> clips;
+    }
+}

+ 11 - 0
Runtime/SFXManager.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b3384ce01dd31c744b48226b46ad9bd9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 23 - 0
Runtime/SFXPlayer.cs

@@ -0,0 +1,23 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.SFX
+{
+    public class SFXPlayer : MonoBehaviour
+    {
+        public SFXClip sfxClip;
+        public bool playOnStart = true;
+
+        void Start()
+        {
+            if(playOnStart) Play();   
+        }
+
+        public void Play()
+        {
+            SoundController.EmmitSound(sfxClip, this.transform.position); 
+        }
+    }
+}

+ 11 - 0
Runtime/SFXPlayer.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3b7e3556dc250f74aa278f842e92e107
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 64 - 0
Runtime/SFXUtil.cs

@@ -0,0 +1,64 @@
+#if UNITY_EDITOR
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+using System.Reflection;
+
+namespace KairoEngine.SFX
+{
+    public static class SFXUtil
+    {
+        public static void PlayClip(AudioClip clip, int startSample = 0, bool loop = false)
+        {
+            System.Reflection.Assembly unityEditorAssembly = typeof(AudioImporter).Assembly;
+            System.Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
+            System.Reflection.MethodInfo method = audioUtilClass.GetMethod(
+                "PlayClip",
+                System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public,
+                null,
+                new System.Type[] { typeof(AudioClip), typeof(int), typeof(bool) },
+                null
+            );
+            method.Invoke(
+                null,
+                new object[] { clip, startSample, loop }
+            );
+        }
+
+        public static void StopClip(AudioClip clip) {
+			Assembly unityEditorAssembly = typeof(AudioImporter).Assembly;
+			System.Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
+			MethodInfo method = audioUtilClass.GetMethod(
+				"StopClip",
+				BindingFlags.Static | BindingFlags.Public,
+				null,
+				new System.Type[] {
+				typeof(AudioClip)
+			},
+			null
+			);
+			method.Invoke(
+				null,
+				new object[] {
+				clip
+			}
+			);
+		}
+
+        public static void StopAllClips () {
+			Assembly unityEditorAssembly = typeof(AudioImporter).Assembly;
+			System.Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
+			MethodInfo method = audioUtilClass.GetMethod(
+				"StopAllClips",
+				BindingFlags.Static | BindingFlags.Public
+				);
+
+			method.Invoke(
+				null,
+				null
+				);
+		}
+    }
+}
+#endif

+ 11 - 0
Runtime/SFXUtil.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f75293b4e3fd2554a8d6226e245030ac
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 104 - 0
Runtime/SoundController.cs

@@ -0,0 +1,104 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Audio;
+using Sirenix.OdinInspector;
+using QFSW.MOP2;
+
+namespace KairoEngine.SFX
+{
+    // Todo: Remove old EmmitSound function.
+
+    /// <summary>
+    /// Controls playing SFX clips.
+    /// </summary>
+    public class SoundController : SerializedMonoBehaviour
+    {
+        
+        [Tooltip("The prefab that will be used as the sound emitter. It needs a audio source and a sphere colllider.")]
+        public GameObject soundEmitterPrefab;
+
+        public static string soundEmitterPool;
+        public string _soundEmitterPool;
+    
+        public static GameObject soundsParent;
+
+        [Tooltip("Container GameObject for the instantiated sound emitter prefabs.")]
+        public  GameObject _soundsParent;
+
+        [Tooltip("A dictionary of sound SFX clips and their names")]
+        public Dictionary<string, SFXClip> soundLibrary = new Dictionary<string, SFXClip>();
+
+        [ShowInInspector, Tooltip("A list of instantiated sound emitters. They are deleted when their audio stops playing.")]
+        public static List<SoundEmitter> soundEmitters = new List<SoundEmitter>();
+
+        public static SoundController instance;
+
+        public void Start()
+        {
+            //soundEmitterPrefab = _soundEmitterPrefab;
+            soundEmitterPool = _soundEmitterPool;
+            soundsParent = _soundsParent;
+            SoundController.instance = this;
+        }
+
+        public static void EmmitSound(AudioClip clip, Vector3 position, float volume, float radius, string audioMixerGroupName, bool world = true)
+        {
+            // AudioMixer mixer = intance.soundEffectsMixer as AudioMixer;
+            // if(mixer == null) 
+            // {
+            //     Debug.Log("Could not find asset SoundEffectsMixer"); 
+            //     return;
+            // }
+            GameObject soundEmitterObj;
+            if(instance.soundEmitterPrefab != null) soundEmitterObj = GameObject.Instantiate(instance.soundEmitterPrefab, soundsParent.transform);
+            else soundEmitterObj = MasterObjectPooler.Instance.GetPool(soundEmitterPool).GetObject(position);
+            soundEmitterObj.transform.parent = soundsParent.transform;
+            SoundEmitter soundEmitter = soundEmitterObj.GetComponent<SoundEmitter>();
+            // AudioMixerGroup audioMixerGroup = mixer.FindMatchingGroups(audioMixerGroupName)[0];
+            soundEmitter.Initialize(clip, position, volume, radius, null, world);
+            soundEmitters.Add(soundEmitter);
+        }
+
+        public static void EmmitSound(string clipName, Vector3 position)
+        {
+            SFXClip clip = null;
+            if (instance.soundLibrary.TryGetValue(clipName, out clip))
+            {
+                EmmitSound(clip, position);
+            }
+            else
+            {
+                Debug.LogError($"No Sound SFX clip found in Sound Library for \'{clipName}\'");
+            }
+        }
+
+        public static void EmmitSound(SFXClip clip, Vector3 position)
+        {
+            if(clip.sfxType == SFXClipType.Steps)
+            {
+                SoundController ctrl = SoundController.instance;
+                foreach (var step in clip.clipSteps)
+                {
+                   ctrl.StartCoroutine(ctrl.EmmitSoundInFuture(step.startTime, step.clip, position));
+                }
+            }
+            else
+            {
+                GameObject soundEmitterObj;
+                if(instance.soundEmitterPrefab != null) soundEmitterObj = GameObject.Instantiate(instance.soundEmitterPrefab, soundsParent.transform);
+                else soundEmitterObj =  MasterObjectPooler.Instance.GetPool(soundEmitterPool).GetObject(position);
+                soundEmitterObj.transform.parent = soundsParent.transform;
+                SoundEmitter soundEmitter = soundEmitterObj.GetComponent<SoundEmitter>();
+                soundEmitter.Initialize(clip, position);
+                soundEmitters.Add(soundEmitter);
+            }
+        }
+
+        public IEnumerator EmmitSoundInFuture(float waitTime, SFXClip clip, Vector3 position)
+        {
+            yield return new WaitForSeconds(waitTime);
+            EmmitSound(clip, position);
+        }
+    }
+}

+ 11 - 0
Runtime/SoundController.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e006f8fc9ad837c418b1c10b05fa03ee
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 218 - 0
Runtime/SoundEmitter.cs

@@ -0,0 +1,218 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Audio;
+using QFSW.MOP2;
+using UnityEngine.SceneManagement;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.SFX
+{
+
+    // Todo: Remove playing SFX on scene change
+    
+    public class SoundEmitter : MonoBehaviour
+    {
+        public SphereCollider trigger;
+        public AudioSource audioSource;
+
+        [ReadOnly]
+        public AudioClip audioClip;
+        public SFXClip sfx;
+
+        public string soundEmitterPrefabPool = "";
+
+        private float fadeInTime;
+        private float fadeOutTime;
+        private Coroutine fadeOutCoroutine;
+
+        private bool done = false;
+
+        /// <summary>
+        /// play a sound at a location in the world using an Audio Clip.
+        /// </summary>
+        /// <param name="audioClip">The audio clip to be played</param>
+        /// <param name="location">The Vector3 position to play the sound in the world</param>
+        /// <param name="volume">Audio clip volume</param>
+        /// <param name="radius">Radius for SoundSensors</param>
+        /// <param name="audioMixerGroup">Audio Mixer Channel</param>
+        /// <param name="world">Trigger SoundSensors in world?</param>
+        public void Initialize(AudioClip audioClip, Vector3 location, float volume, float radius, AudioMixerGroup audioMixerGroup, bool world = true)
+        {
+            this.audioClip = audioClip;
+            this.trigger.radius = radius;
+            transform.position = location;
+            this.audioSource.volume = volume;
+            this.audioSource.outputAudioMixerGroup = audioMixerGroup;
+            this.audioSource.clip = this.audioClip;   
+            this.done = false;
+            if(world == false)
+            {
+                trigger.enabled = false;
+            }
+            this.audioSource.Play();
+        }
+
+        /// <summary>
+        /// Play a sound at a location in the world using a SFX Clip.
+        /// </summary>
+        /// <param name="sfx">SFX Clip with audio clip and parameters.</param>
+        /// <param name="location">Vector3 representing the location to play the sound.</param>
+        public void Initialize(SFXClip sfx, Vector3 location)
+        {
+            this.sfx = sfx;
+            this.done = false;
+            // Audio Clip
+            if(sfx.sfxType == SFXClipType.Single)
+            {
+                // Single
+                this.audioClip = sfx.clip;
+            } 
+            else if(sfx.sfxType == SFXClipType.RandomList)
+            {
+                // Random from list
+                this.audioClip = sfx.clips[Random.Range(0, sfx.clips.Count)];
+            }
+            this.audioSource.clip = this.audioClip;
+            // Sound Location
+            transform.position = location;
+            // Audio Mixer Channel
+            this.audioSource.outputAudioMixerGroup = sfx.audioMixerChannel;
+            // Loop
+            if(sfx.sfxType == SFXClipType.Single) this.audioSource.loop = sfx.loop;
+            // Volume
+            this.audioSource.volume = sfx.volume + Random.Range(-sfx.volumeVariation, sfx.volumeVariation);
+            // Pitch
+            this.audioSource.pitch = sfx.pitch + Random.Range(-sfx.pitchVariation, sfx.pitchVariation);
+            // Spatial or stereo audio
+            if(sfx.soundSpace != SFXSpace.Stereo) 
+            {
+                this.audioSource.spatialize = true;
+            }
+            else
+            {
+                this.audioSource.spatialize = false;
+                this.audioSource.spatialBlend = 0;
+            }
+            // Pan
+            if(sfx.soundSpace != SFXSpace.GameWorld) 
+            {
+                // Stereo audio Pan
+                this.audioSource.panStereo = sfx.pan + Random.Range(-sfx.panVariation, sfx.panVariation);
+            }
+            else 
+            {
+                // 3D audio has no Pan
+                this.audioSource.panStereo = 0;
+            }
+            // Spatial Blend
+            if(sfx.soundSpace == SFXSpace.Mix)
+            {
+                this.audioSource.spatialBlend = sfx.spatialBlend + Random.Range(-sfx.spatialBlendVariation, sfx.spatialBlendVariation);
+            }
+            // Set Distance
+            if(sfx.soundSpace == SFXSpace.GameWorld || sfx.soundSpace == SFXSpace.Mix)
+            {
+                this.audioSource.minDistance = sfx.minDistance;
+                this.audioSource.maxDistance = sfx.maxDistance;
+            }
+            // Use world sensor
+            if(sfx.sensorDetection == true) 
+            {
+                trigger.enabled = true;
+                this.trigger.radius = sfx.soundRadius;
+            }
+            else trigger.enabled = true;
+            // Set Fade In
+            if(sfx.fadeIn) 
+            {
+                this.audioSource.volume = 0f;
+                this.fadeInTime = 0f;
+                StartCoroutine(FadeIn());
+            }
+            // Set Fade out
+            if(sfx.fadeOut) 
+            {
+                fadeOutTime = 0f;
+                fadeOutCoroutine = StartCoroutine(FadeOut());
+            }
+            // Set Remove on scene change
+            // ! Turn this on when dependencies are solved
+            //if(sfx.removeOnSceneChange) TransitionEvents.OnChangeScene += OnSceneChanged;
+            // Trim and Play the audio clip
+            if(sfx.trim) this.audioSource.time = sfx.trimStartDuration;
+            this.audioSource.Play();
+            if(sfx.trim) this.audioSource.SetScheduledEndTime(AudioSettings.dspTime +(sfx.trimEndDuration - sfx.trimStartDuration));
+
+        }
+
+        void Update()
+        {
+            if(done == true) return;
+            // Destroy this Gameobject when the sound is done playing
+            if(audioSource.isPlaying == false)
+            {
+                if(sfx.loop && sfx.sfxType == SFXClipType.RandomList)
+                {
+                    Initialize(sfx, transform.position);
+                }
+                else
+                {
+                    Remove();
+                }
+            } 
+            
+        }
+
+        private void Remove()
+        {
+            SoundController.soundEmitters.Remove(this);
+            // ! Enable when dependencies are resolved
+            //if(sfx.removeOnSceneChange) TransitionEvents.OnChangeScene -= OnSceneChanged;
+            sfx = null;
+            if(MasterObjectPooler.Instance != null) MasterObjectPooler.Instance.GetPool(soundEmitterPrefabPool).Release(this.gameObject);
+            else Destroy(this.gameObject);
+        }
+
+        IEnumerator FadeIn()
+        {
+            float start = Time.realtimeSinceStartup;
+            while(Time.realtimeSinceStartup <= (start + (sfx.fadeInDuration)))
+            {
+                fadeInTime = (Time.realtimeSinceStartup - start);
+                this.audioSource.volume = Mathf.Lerp(0, 1, fadeInTime/sfx.fadeInDuration);
+                yield return null;
+            }
+            this.audioSource.volume = 1;
+        }
+
+        IEnumerator FadeOut(bool wait = true, bool remove = false, float time = 0)
+        {
+            fadeOutTime = 0f;
+            if(time == 0) time = sfx.fadeOutDuration;
+            if(wait) yield return new WaitForSecondsRealtime((this.audioSource.clip.length - this.audioSource.time) - time);
+            if(sfx == null) yield return 0;
+            float start = Time.realtimeSinceStartup;
+            while(Time.realtimeSinceStartup < (start + (time)))
+            {
+                fadeOutTime = (Time.realtimeSinceStartup - start);
+                this.audioSource.volume = Mathf.Lerp(1, 0, fadeOutTime/time);
+                yield return null;
+            }
+            this.audioSource.volume = 0;
+            if(remove) Remove();
+        }
+
+        // ! Enable when dependencies are solved
+        /*
+        private void OnSceneChanged(SceneChangeData sceneData, float time)
+        {
+            StopCoroutine(fadeOutCoroutine);
+            done = true;
+            if(sfx == null) return;
+            if(sfx.fadeOut) StartCoroutine(FadeOut(false, true, time));
+            else Remove();
+        }
+        */
+    }
+}

+ 11 - 0
Runtime/SoundEmitter.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5775068f97aa254458719742adabb71a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 16 - 0
package.json

@@ -0,0 +1,16 @@
+{
+    "name": "at.kairoscope.kairoengine.sfx",
+    "displayName": "SFX",
+    "version": "0.1.2",
+    "unity": "2020.3",
+    "description": "Library for dealing with sound effects.",
+    "dependencies": {
+      "at.kairoscope.kairoengine.core":"0.1.6",
+      "com.unity.editorcoroutines":"1.0.0"
+    },
+    "repository": {
+      "type": "git",
+      "url": "https://git.kairoscope.net/kairoengine/sfx.git"
+    },
+    "author": "Kairoscope"
+  }