diff --git a/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset b/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset index 5ee484e..cfcc419 100644 --- a/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset +++ b/Assets/_Project/Definitions/Towers/TeslaCoilTower.asset @@ -27,7 +27,7 @@ MonoBehaviour: GroundedOnly: 0 Damage: 5 Range: 10 - FireRate: 2 + AttacksPerSecond: 1.5 SplashRadius: 0 ChainCount: 0 ChainRange: 0 diff --git a/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab b/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab index 959f827..cff0c5b 100644 --- a/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab +++ b/Assets/_Project/Prefabs/Towers/Tower_TeslaCoil.prefab @@ -99,7 +99,6 @@ GameObject: - component: {fileID: 805962841523123163} - component: {fileID: 8853488620519990682} - component: {fileID: 4524938955752569428} - - component: {fileID: 42814861218671054} - component: {fileID: -2590971686077483651} m_Layer: 0 m_Name: Tower_TeslaCoil @@ -147,7 +146,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} m_Name: m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject - GlobalObjectIdHash: 280718094 + GlobalObjectIdHash: 1363936954 InScenePlacedSourceGlobalObjectIdHash: 0 DeferredDespawnTick: 0 Ownership: 1 @@ -224,103 +223,6 @@ MonoBehaviour: arcDuration: 0.12 segmentsPerArc: 6 jitter: 0.2 ---- !u!82 &42814861218671054 -AudioSource: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6482414459531823157} - m_Enabled: 1 - serializedVersion: 4 - OutputAudioMixerGroup: {fileID: 0} - m_audioClip: {fileID: 0} - m_Resource: {fileID: 0} - m_PlayOnAwake: 0 - m_Volume: 0.5 - 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!114 &-2590971686077483651 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/_Project/Scenes/Levels/9Player.unity b/Assets/_Project/Scenes/Levels/9Player.unity index 76b7f7f..6dff6f7 100644 --- a/Assets/_Project/Scenes/Levels/9Player.unity +++ b/Assets/_Project/Scenes/Levels/9Player.unity @@ -2222,6 +2222,63 @@ Transform: m_Children: [] m_Father: {fileID: 1097152085} m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &176580460 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 176580462} + - component: {fileID: 176580461} + m_Layer: 0 + m_Name: AudioManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &176580461 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 176580460} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9ec21c01769ef58d1ac26ba89668eeaa, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::TD.Audio.AudioManager + categories: + - category: 0 + maxVoices: 4 + volume: 0.5 + - category: 1 + maxVoices: 4 + volume: 0.5 + - category: 2 + maxVoices: 4 + volume: 0.5 + - category: 3 + maxVoices: 4 + volume: 0.5 +--- !u!4 &176580462 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 176580460} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!43 &203092944 Mesh: m_ObjectHideFlags: 0 @@ -28764,3 +28821,4 @@ SceneRoots: - {fileID: 1097152085} - {fileID: 1889390964} - {fileID: 2139601601} + - {fileID: 176580462} diff --git a/Assets/_Project/Scripts/Audio.meta b/Assets/_Project/Scripts/Audio.meta new file mode 100644 index 0000000..d399f53 --- /dev/null +++ b/Assets/_Project/Scripts/Audio.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d11cb1aeddfee3009879738a45665945 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Audio/AudioCategory.cs b/Assets/_Project/Scripts/Audio/AudioCategory.cs new file mode 100644 index 0000000..ef63198 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioCategory.cs @@ -0,0 +1,5 @@ +// Assets/_Project/Scripts/Audio/AudioCategory.cs +namespace TD.Audio +{ + public enum AudioCategory { TowerFire, EnemyHitDeath, UI, Music } +} diff --git a/Assets/_Project/Scripts/Audio/AudioCategory.cs.meta b/Assets/_Project/Scripts/Audio/AudioCategory.cs.meta new file mode 100644 index 0000000..3947300 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioCategory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2f652b06e3d150798895893892820cc7 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Audio/AudioManager.cs b/Assets/_Project/Scripts/Audio/AudioManager.cs new file mode 100644 index 0000000..961a820 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioManager.cs @@ -0,0 +1,74 @@ +// Assets/_Project/Scripts/Audio/AudioManager.cs +using System.Collections.Generic; +using UnityEngine; + +namespace TD.Audio +{ + public class AudioManager : MonoBehaviour + { + [System.Serializable] + public struct CategoryConfig + { + public AudioCategory category; + public int maxVoices; + [Range(0f, 1f)] + public float volume; + } + + public static AudioManager Instance { get; private set; } + + [SerializeField] private CategoryConfig[] categories; + + private Dictionary pools; + private Dictionary indices; + + private void Awake() + { + if (Instance != null) + { + Destroy(gameObject); + return; + } + Instance = this; + InitializePools(); + } + + private void OnDestroy() + { + if (Instance == this) Instance = null; + } + + private void InitializePools() + { + pools = new Dictionary(); + indices = new Dictionary(); + + foreach (var config in categories) + { + var pool = new AudioSource[config.maxVoices]; + for (int i = 0; i < config.maxVoices; i++) + { + var src = gameObject.AddComponent(); + src.playOnAwake = false; + src.volume = config.volume; + pool[i] = src; + } + pools[config.category] = pool; + indices[config.category] = 0; + } + } + + public void Play(AudioClip clip, AudioCategory category, float pitch = 1f) + { + if (clip == null) return; + if (!pools.TryGetValue(category, out var pool)) return; + + int idx = indices[category]; + var src = pool[idx % pool.Length]; + src.pitch = pitch; + src.clip = clip; + src.Play(); + indices[category] = idx + 1; + } + } +} diff --git a/Assets/_Project/Scripts/Audio/AudioManager.cs.meta b/Assets/_Project/Scripts/Audio/AudioManager.cs.meta new file mode 100644 index 0000000..855be40 --- /dev/null +++ b/Assets/_Project/Scripts/Audio/AudioManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ec21c01769ef58d1ac26ba89668eeaa \ No newline at end of file diff --git a/Assets/_Project/Scripts/Audio/README.md b/Assets/_Project/Scripts/Audio/README.md new file mode 100644 index 0000000..2f7d81c --- /dev/null +++ b/Assets/_Project/Scripts/Audio/README.md @@ -0,0 +1,36 @@ +# Audio System + +Centralised voice-limited audio for the game. Lives in `Assets/_Project/Scripts/Audio/`. + +## How it works + +`AudioManager` is a per-scene MonoBehaviour singleton. On `Awake` it pre-allocates a pool of `AudioSource` components (on its own GameObject) for each sound category. Callers request playback via `AudioManager.Instance.Play(clip, category, pitch)` — the manager picks the next source in that category's pool using round-robin, interrupting the oldest sound if all voices are busy. + +## Categories (`AudioCategory` enum) + +| Category | Typical use | +|---|---| +| `TowerFire` | Tower attack sounds | +| `EnemyHitDeath` | Enemy damage and death sounds | +| `UI` | Button clicks, placement confirmation, etc. | +| `Music` | Background music | + +Voice limits and volume per category are configured in the Inspector on the `AudioManager` scene GameObject. + +## Scene setup + +An empty GameObject named `AudioManager` must exist in each gameplay scene with the `AudioManager` component attached. Configure the `Categories` array in the Inspector — one entry per `AudioCategory` value. + +Suggested defaults: +- TowerFire: 4 voices, volume 1.0 +- EnemyHitDeath: 8 voices, volume 1.0 +- UI: 4 voices, volume 1.0 +- Music: 2 voices, volume 0.8 + +## Adding a new sound-emitting component + +1. Get a reference to an `AudioClip` via `[SerializeField]` +2. Call `AudioManager.Instance?.Play(clip, AudioCategory.YourCategory)` at the point the sound should trigger +3. Pitch randomization (if desired) is the caller's responsibility: `Random.Range(minPitch, maxPitch)` passed as the third argument + +See `TeslaTowerFireSound.cs` in `Scripts/Combat/` as a reference implementation. diff --git a/Assets/_Project/Scripts/Audio/README.md.meta b/Assets/_Project/Scripts/Audio/README.md.meta new file mode 100644 index 0000000..284147a --- /dev/null +++ b/Assets/_Project/Scripts/Audio/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4acf87ebbb79a6ce482e6e4767d27b41 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs b/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs index 69de453..9294b85 100644 --- a/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs +++ b/Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs @@ -1,4 +1,5 @@ // Assets/_Project/Scripts/Combat/TeslaTowerFireSound.cs +using TD.Audio; using UnityEngine; namespace TD.Combat @@ -14,7 +15,6 @@ namespace TD.Combat /// as the Tesla Coil gains more audio (idle hum, charge-up, etc.). /// [RequireComponent(typeof(TowerCombat))] - [RequireComponent(typeof(AudioSource))] public class TeslaTowerFireSound : MonoBehaviour { [Tooltip("Sound played each time the coil discharges.")] @@ -25,12 +25,10 @@ namespace TD.Combat [SerializeField] private float maxPitch = 1.07f; private TowerCombat combat; - private AudioSource audioSource; private void Awake() { - combat = GetComponent(); - audioSource = GetComponent(); + combat = GetComponent(); } private void OnEnable() @@ -45,12 +43,7 @@ namespace TD.Combat private void HandleAreaFired(Vector3[] positions) { - if (fireClip != null) - { - audioSource.pitch = Random.Range(minPitch, maxPitch); - audioSource.clip = fireClip; - audioSource.Play(); - } + AudioManager.Instance?.Play(fireClip, AudioCategory.TowerFire, Random.Range(minPitch, maxPitch)); } } }