riversong code showcase
This commit is contained in:
8
Source/Riversong/Game/Audio/AudioSystemGroup.cs
Normal file
8
Source/Riversong/Game/Audio/AudioSystemGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
|
||||
public class AudioSystemGroup : GameSystemGroup
|
||||
{
|
||||
}
|
||||
}
|
||||
82
Source/Riversong/Game/Audio/BackgroundMusicSystem.cs
Normal file
82
Source/Riversong/Game/Audio/BackgroundMusicSystem.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[GameSystemGroup(typeof(AudioSystemGroup))]
|
||||
public class BackgroundMusicSystem : GameSystem, IInitializable, IDisposable
|
||||
{
|
||||
[InjectService]
|
||||
private GameConfig _config;
|
||||
|
||||
[InjectService]
|
||||
private IScene _scene;
|
||||
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
private AudioSource _audioSource;
|
||||
|
||||
private AudioClip _mainThemeClip;
|
||||
|
||||
private AudioClip _gameplayClip;
|
||||
|
||||
public BackgroundMusicSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public async UniTask InitializeAsync()
|
||||
{
|
||||
_audioSource = new GameObject("BGM").AddComponent<AudioSource>();
|
||||
_audioSource.transform.SetParent(_scene.SceneFolders.AudioSources);
|
||||
_audioSource.playOnAwake = false;
|
||||
_audioSource.loop = true;
|
||||
|
||||
_mainThemeClip = await _config.Audio.MainThemeClip.LoadAssetAsync<AudioClip>();
|
||||
_gameplayClip = await _config.Audio.GameplayClip.LoadAssetAsync<AudioClip>();
|
||||
|
||||
_signalBus.Subscribe<GameInitializationCompletedSignal>(OnGameInitializationCompleted);
|
||||
_signalBus.Subscribe<GameStartedSignal>(OnGameStarted);
|
||||
_signalBus.Subscribe<WorldReadySignal>(OnWorldReady);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<GameInitializationCompletedSignal>(OnGameInitializationCompleted);
|
||||
_signalBus.Unsubscribe<GameStartedSignal>(OnGameStarted);
|
||||
_signalBus.Unsubscribe<WorldReadySignal>(OnWorldReady);
|
||||
}
|
||||
|
||||
private void OnGameInitializationCompleted(GameInitializationCompletedSignal signal)
|
||||
{
|
||||
Play(_mainThemeClip, _config.Audio.MainThemeVolume);
|
||||
}
|
||||
|
||||
private void OnGameStarted(GameStartedSignal signal)
|
||||
{
|
||||
_ = FadeOutAsync(2);
|
||||
}
|
||||
|
||||
private async UniTask FadeOutAsync(float duration)
|
||||
{
|
||||
await Tween.Custom(_audioSource.volume, 0, duration, value => _audioSource.volume = value, Ease.Linear);
|
||||
}
|
||||
|
||||
private void OnWorldReady(WorldReadySignal signal)
|
||||
{
|
||||
Play(_gameplayClip, _config.Audio.GameplayVolume);
|
||||
}
|
||||
|
||||
private void Play(AudioClip clip, float volume)
|
||||
{
|
||||
_audioSource.Stop();
|
||||
|
||||
_audioSource.clip = clip;
|
||||
_audioSource.volume = volume;
|
||||
|
||||
_audioSource.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Source/Riversong/Game/Audio/ISoundPlayer.cs
Normal file
34
Source/Riversong/Game/Audio/ISoundPlayer.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine.Audio;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface ISoundPlayer
|
||||
{
|
||||
void Play(AudioResource resource);
|
||||
|
||||
void PlayAt(AudioResource resource, float3 position);
|
||||
|
||||
void PlayAt(AudioResource resource, int2 tile);
|
||||
|
||||
AudioResource GetSystemSound(SystemSoundId soundId);
|
||||
}
|
||||
|
||||
public static class SoundPlayerExtensions
|
||||
{
|
||||
public static void Play(this ISoundPlayer soundPlayer, SystemSoundId soundId)
|
||||
{
|
||||
soundPlayer.Play(soundPlayer.GetSystemSound(soundId));
|
||||
}
|
||||
|
||||
public static void PlayAt(this ISoundPlayer soundPlayer, SystemSoundId soundId, float3 position)
|
||||
{
|
||||
soundPlayer.PlayAt(soundPlayer.GetSystemSound(soundId), position);
|
||||
}
|
||||
|
||||
public static void PlayAt(this ISoundPlayer soundPlayer, SystemSoundId soundId, int2 tile)
|
||||
{
|
||||
soundPlayer.PlayAt(soundPlayer.GetSystemSound(soundId), tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs
Normal file
120
Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Unity.Mathematics;
|
||||
using Time = UnityEngine.Time;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class PlaySoundOnEventSystem : GameSystem, IInitializable, IDisposable
|
||||
{
|
||||
[InjectService]
|
||||
private ISoundPlayer _soundPlayer;
|
||||
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
[InjectService]
|
||||
private IGameDatabase _gameDatabase;
|
||||
|
||||
[InjectService]
|
||||
private ITileSpace _tileSpace;
|
||||
|
||||
private Dictionary<int, float> _soundPlayedTimestamps = new();
|
||||
|
||||
public PlaySoundOnEventSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
_signalBus.Subscribe<BuildingPlacementAnimationCompletedSignal>(OnBuildingPlacementAnimationCompleted);
|
||||
_signalBus.Subscribe<ConstructionSiteDeletedSignal>(OnConstructionSiteDeleted);
|
||||
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
|
||||
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
|
||||
_signalBus.Subscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
|
||||
_signalBus.Subscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
|
||||
_signalBus.Subscribe<BuildMenuButtonUnlockAnimationStartedSignal>(OnBuildMenuButtonUnlockAnimationStarted);
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<BuildingPlacementAnimationCompletedSignal>(OnBuildingPlacementAnimationCompleted);
|
||||
_signalBus.Unsubscribe<ConstructionSiteDeletedSignal>(OnConstructionSiteDeleted);
|
||||
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
|
||||
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
|
||||
_signalBus.Unsubscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
|
||||
_signalBus.Unsubscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
|
||||
_signalBus.Unsubscribe<BuildMenuButtonUnlockAnimationStartedSignal>(OnBuildMenuButtonUnlockAnimationStarted);
|
||||
}
|
||||
|
||||
private bool UpdatePlayedTimestamp(int key)
|
||||
{
|
||||
var timestamp = _soundPlayedTimestamps.GetValueOrDefault(key);
|
||||
|
||||
const float minInterval = 0.1f;
|
||||
if (Time.unscaledTime - timestamp < minInterval) return false;
|
||||
|
||||
_soundPlayedTimestamps[key] = Time.unscaledTime;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnBuildingPlacementAnimationCompleted(BuildingPlacementAnimationCompletedSignal signal)
|
||||
{
|
||||
PlaySystemSound(SystemSoundId.BuildingPlaced);
|
||||
}
|
||||
|
||||
private void OnConstructionSiteDeleted(ConstructionSiteDeletedSignal signal)
|
||||
{
|
||||
PlaySystemSound(SystemSoundId.BuildingDeleted);
|
||||
}
|
||||
|
||||
private void OnBuildingDeleted(BuildingDeletedSignal signal)
|
||||
{
|
||||
if (signal.Options == DeleteBuildingOptions.Silent) return;
|
||||
|
||||
PlaySystemSound(SystemSoundId.BuildingDeleted);
|
||||
}
|
||||
|
||||
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
|
||||
{
|
||||
if (!signal.Building.Definition.IsHouse || !UpdatePlayedTimestamp((int)SystemSoundId.HouseUpgraded)) return;
|
||||
|
||||
var position = (float3)_tileSpace.GetRectWorldCenter(signal.Building.Rect);
|
||||
_soundPlayer.PlayAt(SystemSoundId.HouseUpgraded, position);
|
||||
}
|
||||
|
||||
private void OnRawResourcesRemoved(RawResourcesRemovedSignal signal)
|
||||
{
|
||||
foreach (var resourceNode in signal.ResourceNodes)
|
||||
{
|
||||
var definition = _gameDatabase.WithId<ResourceNodeDefinition>(resourceNode.DefinitionId);
|
||||
|
||||
if (!UpdatePlayedTimestamp(definition.RuntimeId)) continue;
|
||||
|
||||
if (signal.Reason == RawResourcesRemovedSignal.RemovalReason.Harvested)
|
||||
_soundPlayer.PlayAt(definition.DeleteAudio, resourceNode.Tile);
|
||||
else
|
||||
_soundPlayer.Play(definition.DeleteAudio);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoadTileUpdated(RoadTileUpdatedSignal signal)
|
||||
{
|
||||
PlaySystemSound(signal.RoadTileAdded ? SystemSoundId.RoadTilePlaced : SystemSoundId.RoadTileDeleted);
|
||||
}
|
||||
|
||||
private void OnBuildMenuButtonUnlockAnimationStarted(BuildMenuButtonUnlockAnimationStartedSignal signal)
|
||||
{
|
||||
PlaySystemSound(SystemSoundId.UnlockNotification);
|
||||
}
|
||||
|
||||
private void PlaySystemSound(SystemSoundId soundId)
|
||||
{
|
||||
if (UpdatePlayedTimestamp((int)soundId)) _soundPlayer.Play(soundId);
|
||||
}
|
||||
}
|
||||
}
|
||||
184
Source/Riversong/Game/Audio/SoundPlayerSystem.cs
Normal file
184
Source/Riversong/Game/Audio/SoundPlayerSystem.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
using Object = UnityEngine.Object;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[Service(typeof(ISoundPlayer))]
|
||||
[GameSystemGroup(typeof(AudioSystemGroup))]
|
||||
[InitializeAfter(typeof(BackgroundMusicSystem))]
|
||||
public class SoundPlayerSystem : GameSystem, IInitializable, IUpdatable, ISoundPlayer, IDrawGizmos
|
||||
{
|
||||
private const int ChannelCount = 16;
|
||||
|
||||
[InjectService]
|
||||
private ITileSpace _tileSpace;
|
||||
|
||||
[InjectService]
|
||||
private IScene _scene;
|
||||
|
||||
[InjectService]
|
||||
private ICameraProperties _cameraProperties;
|
||||
|
||||
[InjectService]
|
||||
private GameConfig _config;
|
||||
|
||||
private AudioSourcePool _poolNonSpatial;
|
||||
|
||||
private AudioSourcePool _poolSpatial;
|
||||
|
||||
private SystemSoundLibrary _systemSoundLibrary;
|
||||
|
||||
public SoundPlayerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public async UniTask InitializeAsync()
|
||||
{
|
||||
var systemSoundLibraryTask = _config.Audio.SystemSoundLibrary.LoadAssetAsync<SystemSoundLibrary>().ToUniTask();
|
||||
var audioSourcePrefabTask = _config.Audio.AudioSourcePrefab.LoadAssetAsync().ToUniTask();
|
||||
|
||||
_systemSoundLibrary = await systemSoundLibraryTask;
|
||||
|
||||
var audioSourcePrefab = (await audioSourcePrefabTask).GetComponent<AudioSource>();
|
||||
await InitializePoolAsync(audioSourcePrefab);
|
||||
}
|
||||
|
||||
private async UniTask InitializePoolAsync(AudioSource audioSourcePrefab)
|
||||
{
|
||||
_poolNonSpatial = InitializePool(audioSourcePrefab, "Sound_{0:00} (2D)");
|
||||
await UniTask.NextFrame();
|
||||
|
||||
_poolSpatial = InitializePool(audioSourcePrefab, "Sound_{0:00} (3D)");
|
||||
await UniTask.NextFrame();
|
||||
}
|
||||
|
||||
private AudioSourcePool InitializePool(AudioSource audioSourcePrefab, string nameFormat)
|
||||
{
|
||||
var audioSources = new AudioSource[ChannelCount];
|
||||
|
||||
for (var i = 0; i < ChannelCount; i++)
|
||||
{
|
||||
var source = Object.Instantiate(audioSourcePrefab, _scene.SceneFolders.AudioSources);
|
||||
source.name = string.Format(nameFormat, i);
|
||||
|
||||
audioSources[i] = source;
|
||||
}
|
||||
|
||||
return new AudioSourcePool(audioSources);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
UpdateSpatialAudioSources();
|
||||
}
|
||||
|
||||
private void UpdateSpatialAudioSources()
|
||||
{
|
||||
var cameraPosition = ((float3)_scene.MainCamera.transform.position).xz;
|
||||
|
||||
var horizontalDistanceRange = _config.Audio.SpatialAudioHorizontalDistanceRange;
|
||||
var zoomRange = _config.Audio.SpatialAudioZoomRange;
|
||||
|
||||
foreach (var audioSource in _poolSpatial.AudioSources)
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
if (!audioSource.isPlaying) continue;
|
||||
#endif
|
||||
|
||||
var p = ((float3)audioSource.transform.position).xz;
|
||||
|
||||
var horizontalDistance = math.distance(cameraPosition, p);
|
||||
|
||||
var t = math.saturate(math.unlerp(horizontalDistanceRange.x, horizontalDistanceRange.y, horizontalDistance));
|
||||
var attenuation = 1 - t * t;
|
||||
|
||||
var zoomFactor = 1 - math.unlerp(zoomRange.x, zoomRange.y, _cameraProperties.Zoom);
|
||||
|
||||
audioSource.volume = attenuation * zoomFactor;
|
||||
}
|
||||
}
|
||||
|
||||
public void Play(AudioResource resource)
|
||||
{
|
||||
var source = _poolNonSpatial.GetAudioSource();
|
||||
|
||||
source.resource = resource;
|
||||
|
||||
source.Play();
|
||||
}
|
||||
|
||||
public void PlayAt(AudioResource resource, float3 position)
|
||||
{
|
||||
var source = _poolSpatial.GetAudioSource();
|
||||
|
||||
source.resource = resource;
|
||||
source.transform.position = position;
|
||||
|
||||
source.Play();
|
||||
}
|
||||
|
||||
public void PlayAt(AudioResource resource, int2 tile)
|
||||
{
|
||||
PlayAt(resource, _tileSpace.TileToWorld(tile));
|
||||
}
|
||||
|
||||
public AudioResource GetSystemSound(SystemSoundId soundId)
|
||||
{
|
||||
return _systemSoundLibrary.Sounds.GetValueOrDefault(soundId);
|
||||
}
|
||||
|
||||
public void DrawGizmos(bool selected)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
const int width = 60;
|
||||
const int height = 8;
|
||||
|
||||
var camera = _scene.MainCamera;
|
||||
|
||||
foreach (var audioSource in _poolSpatial.AudioSources)
|
||||
{
|
||||
var screenPoint = camera.WorldToScreenPoint(audioSource.transform.position);
|
||||
if (screenPoint.z < 0) continue;
|
||||
|
||||
var p = new Vector2(screenPoint.x - width * 0.5f, camera.pixelHeight - screenPoint.y);
|
||||
|
||||
Handles.BeginGUI();
|
||||
|
||||
EditorGUI.DrawRect(new Rect(p.x, p.y, width, height), new Color(0, 0, 0, 0.6f));
|
||||
|
||||
var volume = audioSource.volume;
|
||||
var color = Color.Lerp(Color.red, Color.green, volume);
|
||||
EditorGUI.DrawRect(new Rect(p.x + 0.5f * (width * (1 - volume)), p.y, width * volume, height), color);
|
||||
|
||||
Handles.EndGUI();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private class AudioSourcePool
|
||||
{
|
||||
private int _nextChannel;
|
||||
|
||||
public AudioSourcePool(AudioSource[] audioSources)
|
||||
{
|
||||
AudioSources = audioSources;
|
||||
}
|
||||
|
||||
public AudioSource[] AudioSources { get; }
|
||||
|
||||
public AudioSource GetAudioSource()
|
||||
{
|
||||
var source = AudioSources[_nextChannel];
|
||||
_nextChannel = (_nextChannel + 1) % ChannelCount;
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Source/Riversong/Game/Audio/SystemSoundId.cs
Normal file
21
Source/Riversong/Game/Audio/SystemSoundId.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public enum SystemSoundId
|
||||
{
|
||||
UIClick,
|
||||
|
||||
BuildingPlaced,
|
||||
|
||||
BuildingDeleted,
|
||||
|
||||
HouseUpgraded,
|
||||
|
||||
RoadTilePlaced,
|
||||
|
||||
RoadTileDeleted,
|
||||
|
||||
UnlockNotification,
|
||||
|
||||
OnboardingMessage
|
||||
}
|
||||
}
|
||||
13
Source/Riversong/Game/Audio/SystemSoundLibrary.cs
Normal file
13
Source/Riversong/Game/Audio/SystemSoundLibrary.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Audio;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[CreateAssetMenu(fileName = "SystemSoundLibrary", menuName = "Riversong Code Showcase/System Sound Library")]
|
||||
public class SystemSoundLibrary : SerializedScriptableObject
|
||||
{
|
||||
public Dictionary<SystemSoundId, AudioResource> Sounds;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user