Skip to content

Commit cd31ae6

Browse files
Remove singleton pattern, add static game class, pool sfx players, cleanup events, docs and more, wip (#188)
* Remove singleton pattern from Services * Remove singleton pattern from AudioManager * Remove singleton pattern from SceneManager * Remove singleton pattern from GameConsole * Remove singleton pattern from MetricsOverlay * Remove singleton pattern from Profiler * Adjust all references to refactored managers * Add static Game class for global managers Introduces a static Game class providing global access to core managers and services via the Autoloads singleton. * Refactor namespaces from GodotUtils to __TEMPLATE__ Updated all Framework and UI classes to use the __TEMPLATE__ namespace instead of GodotUtils, and adjusted using directives accordingly. * Improve SFX player cleanup in AudioManager * Updated documentation for AudioManager.cs * Refactor scene switching to use dedicated methods Introduced dedicated methods in SceneManager for switching to specific menu scenes (MainMenu, Options, ModLoader, Credits) and updated all usages to call these methods instead of accessing MenuScenes directly. * Improve event cleanup on Button * Update GodotUtils.dll * Create GodotUtils.xml * Pool sfx players in audio manager for performance * Prefer [Export] over unique lookup with % * Fire and forget on exit game * Catch PreQuit subscriber exceptions * Prefer [Export] over GetNode<T>(...) * Update GodotUtils.dll * Remove singleton pattern from Logger and more - All messages are dequeued instead of one at a time to prevent backlog buildup - Removed duplicate ResetColor() calls - Removed hard game console dependency (optional now) - Made _messages and _console readonly * Separate BBColor in its own file from Logger.cs * Remove unused code in Logger * Improve readability of Logger by removing lines * Create BBColor.cs.uid * Call Quit() after Editor.Restart() * Search window title instead of process name * Revert from [Export] to GetNode due to Godot Bug * Fix crash: Assign popup menu console in _Ready() * Remove unused code in PopupMenu and improve readability
1 parent ac8730b commit cd31ae6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1633
-457
lines changed
Lines changed: 85 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,154 @@
11
using Godot;
2-
using GodotUtils.UI;
2+
using GodotUtils;
33
using System;
4-
using System.Collections.Generic;
5-
using System.Diagnostics;
64

7-
namespace GodotUtils;
5+
namespace __TEMPLATE__;
86

97
public class AudioManager : IDisposable
108
{
11-
private const float MinDefaultRandomPitch = 0.8f;
12-
private const float MaxDefaultRandomPitch = 1.2f;
13-
private const float RandomPitchThreshold = 0.1f;
14-
private const int MutedVolume = -80;
15-
private const int MutedVolumeNormalized = -40;
9+
private const float MinDefaultRandomPitch = 0.8f; // Default minimum pitch value for SFX.
10+
private const float MaxDefaultRandomPitch = 1.2f; // Default maximum pitch value for SFX.
11+
private const float RandomPitchThreshold = 0.1f; // Minimum difference in pitch between repeated sounds.
12+
private const int MutedVolume = -80; // dB value representing mute.
13+
private const int MutedVolumeNormalized = -40; // Normalized muted volume for volume mapping.
1614

17-
private static AudioManager _instance;
1815
private AudioStreamPlayer _musicPlayer;
1916
private ResourceOptions _options;
2017
private Autoloads _autoloads;
2118
private float _lastPitch;
2219

23-
private List<AudioStreamPlayer2D> _activeSfxPlayers = [];
20+
private GodotNodePool<AudioStreamPlayer2D> _sfxPool;
2421

22+
/// <summary>
23+
/// Initializes the AudioManager by attaching a music player to the given autoload node.
24+
/// </summary>
2525
public AudioManager(Autoloads autoloads)
2626
{
27-
if (_instance != null)
28-
throw new InvalidOperationException($"{nameof(AudioManager)} was initialized already");
29-
30-
_instance = this;
3127
_autoloads = autoloads;
32-
_options = OptionsManager.GetOptions();
28+
_options = Game.Options.GetOptions();
3329

3430
_musicPlayer = new AudioStreamPlayer();
3531
autoloads.AddChild(_musicPlayer);
32+
33+
_sfxPool = new GodotNodePool<AudioStreamPlayer2D>(autoloads,
34+
() => new AudioStreamPlayer2D());
3635
}
3736

37+
/// <summary>
38+
/// Frees all managed players and clears references for cleanup.
39+
/// </summary>
3840
public void Dispose()
3941
{
4042
_musicPlayer.QueueFree();
41-
_activeSfxPlayers.Clear();
42-
43-
_instance = null;
43+
_sfxPool.Clear();
4444
}
4545

46-
public static void PlayMusic(AudioStream song, bool instant = true, double fadeOut = 1.5, double fadeIn = 0.5)
46+
/// <summary>
47+
/// Plays a music track, instantly or with optional fade between tracks. Music volume is in config scale (0-100).
48+
/// </summary>
49+
public void PlayMusic(AudioStream song, bool instant = true, double fadeOut = 1.5, double fadeIn = 0.5)
4750
{
48-
if (!instant && _instance._musicPlayer.Playing)
51+
if (!instant && _musicPlayer.Playing)
4952
{
5053
// Slowly transition to the new song
51-
PlayAudioCrossfade(_instance._musicPlayer, song, _instance._options.MusicVolume, fadeOut, fadeIn);
54+
PlayAudioCrossfade(_musicPlayer, song, _options.MusicVolume, fadeOut, fadeIn);
5255
}
5356
else
5457
{
5558
// Instantly switch to the new song
56-
PlayAudio(_instance._musicPlayer, song, _instance._options.MusicVolume);
59+
PlayAudio(_musicPlayer, song, _options.MusicVolume);
5760
}
5861
}
5962

6063
/// <summary>
61-
/// Plays a <paramref name="sound"/> at <paramref name="position"/>.
64+
/// Plays a sound effect at the specified global position with randomized pitch to reduce repetition. Volume is normalized (0-100).
6265
/// </summary>
63-
/// <param name="parent"></param>
64-
/// <param name="sound"></param>
65-
public static void PlaySFX(AudioStream sound, Vector2 position, float minPitch = MinDefaultRandomPitch, float maxPitch = MaxDefaultRandomPitch)
66+
public void PlaySFX(AudioStream sound, Vector2 position, float minPitch = MinDefaultRandomPitch, float maxPitch = MaxDefaultRandomPitch)
6667
{
67-
AudioStreamPlayer2D sfxPlayer = new()
68-
{
69-
Stream = sound,
70-
VolumeDb = NormalizeConfigVolume(_instance._options.SFXVolume),
71-
PitchScale = GetRandomPitch(minPitch, maxPitch)
72-
};
73-
74-
sfxPlayer.Finished += () =>
75-
{
76-
sfxPlayer.QueueFree();
77-
_instance._activeSfxPlayers.Remove(sfxPlayer);
78-
};
79-
80-
_instance._autoloads.AddChild(sfxPlayer);
81-
_instance._activeSfxPlayers.Add(sfxPlayer);
68+
AudioStreamPlayer2D sfxPlayer = _sfxPool.Get();
8269

8370
sfxPlayer.GlobalPosition = position;
71+
sfxPlayer.Stream = sound;
72+
sfxPlayer.VolumeDb = NormalizeConfigVolume(_options.SFXVolume);
73+
sfxPlayer.PitchScale = GetRandomPitch(minPitch, maxPitch);
74+
sfxPlayer.Finished += OnFinished;
8475
sfxPlayer.Play();
76+
77+
void OnFinished()
78+
{
79+
sfxPlayer.Finished -= OnFinished;
80+
_sfxPool.Release(sfxPlayer);
81+
}
8582
}
8683

87-
public static void FadeOutSFX(double fadeTime = 1)
84+
/// <summary>
85+
/// Fades out all currently playing sound effects over the specified duration in seconds.
86+
/// </summary>
87+
public void FadeOutSFX(double fadeTime = 1)
8888
{
89-
foreach (AudioStreamPlayer2D sfxPlayer in _instance._activeSfxPlayers)
89+
foreach (AudioStreamPlayer2D sfxPlayer in _sfxPool.ActiveNodes)
9090
{
9191
new GodotTween(sfxPlayer).Animate(AudioStreamPlayer.PropertyName.VolumeDb, MutedVolume, fadeTime);
9292
}
9393
}
9494

95-
public static void SetMusicVolume(float volume)
95+
/// <summary>
96+
/// Sets the music volume, affecting current playback. Volume is in config scale (0-100).
97+
/// </summary>
98+
public void SetMusicVolume(float volume)
9699
{
97-
_instance._musicPlayer.VolumeDb = NormalizeConfigVolume(volume);
98-
_instance._options.MusicVolume = volume;
100+
_musicPlayer.VolumeDb = NormalizeConfigVolume(volume);
101+
_options.MusicVolume = volume;
99102
}
100103

101-
public static void SetSFXVolume(float volume)
104+
/// <summary>
105+
/// Sets the SFX volume for all active sound effect players. Volume is in config scale (0-100).
106+
/// </summary>
107+
public void SetSFXVolume(float volume)
102108
{
103-
_instance._options.SFXVolume = volume;
109+
_options.SFXVolume = volume;
104110

105111
float mappedVolume = NormalizeConfigVolume(volume);
106112

107-
foreach (AudioStreamPlayer2D sfxPlayer in _instance._activeSfxPlayers)
113+
foreach (AudioStreamPlayer2D sfxPlayer in _sfxPool.ActiveNodes)
108114
{
109115
sfxPlayer.VolumeDb = mappedVolume;
110116
}
111117
}
112118

119+
/// <summary>
120+
/// Generates a random pitch between min and max, avoiding values too similar to the previous sound.
121+
/// </summary>
122+
private float GetRandomPitch(float min, float max)
123+
{
124+
RandomNumberGenerator rng = new();
125+
rng.Randomize();
126+
127+
float pitch = rng.RandfRange(min, max);
128+
129+
while (Mathf.Abs(pitch - _lastPitch) < RandomPitchThreshold)
130+
{
131+
rng.Randomize();
132+
pitch = rng.RandfRange(min, max);
133+
}
134+
135+
_lastPitch = pitch;
136+
return pitch;
137+
}
138+
139+
/// <summary>
140+
/// Instantly plays the given audio stream with the specified player and volume.
141+
/// </summary>
113142
private static void PlayAudio(AudioStreamPlayer player, AudioStream song, float volume)
114143
{
115144
player.Stream = song;
116145
player.VolumeDb = NormalizeConfigVolume(volume);
117146
player.Play();
118147
}
119148

149+
/// <summary>
150+
/// Smoothly crossfades between songs by fading out the current and fading in the new one. Volume is in config scale (0-100).
151+
/// </summary>
120152
private static void PlayAudioCrossfade(AudioStreamPlayer player, AudioStream song, float volume, double fadeOut, double fadeIn)
121153
{
122154
new GodotTween(player)
@@ -126,25 +158,11 @@ private static void PlayAudioCrossfade(AudioStreamPlayer player, AudioStream son
126158
.AnimateProp(NormalizeConfigVolume(volume), fadeIn).EaseIn();
127159
}
128160

161+
/// <summary>
162+
/// Maps a config volume value (0-100) to an AudioStreamPlayer VolumeDb value, returning mute if zero.
163+
/// </summary>
129164
private static float NormalizeConfigVolume(float volume)
130165
{
131166
return volume == 0 ? MutedVolume : volume.Remap(0, 100, MutedVolumeNormalized, 0);
132167
}
133-
134-
private static float GetRandomPitch(float min, float max)
135-
{
136-
RandomNumberGenerator rng = new();
137-
rng.Randomize();
138-
139-
float pitch = rng.RandfRange(min, max);
140-
141-
while (Mathf.Abs(pitch - _instance._lastPitch) < RandomPitchThreshold)
142-
{
143-
rng.Randomize();
144-
pitch = rng.RandfRange(min, max);
145-
}
146-
147-
_instance._lastPitch = pitch;
148-
return pitch;
149-
}
150168
}

Framework/Autoloads/Autoloads.cs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
using __TEMPLATE__.Debugging;
2+
using __TEMPLATE__.UI;
3+
using __TEMPLATE__.UI.Console;
14
using Godot;
2-
using GodotUtils.UI;
3-
using GodotUtils.UI.Console;
5+
using GodotUtils;
6+
using System;
7+
using System.Threading.Tasks;
8+
49
#if DEBUG
510
using GodotUtils.Debugging;
611
#endif
7-
using System;
8-
using System.Threading.Tasks;
912

10-
namespace GodotUtils;
13+
namespace __TEMPLATE__;
1114

1215
// Autoload
1316
// Access this with GetNode<Autoloads>("/root/Autoloads")
@@ -19,17 +22,17 @@ public partial class Autoloads : Node
1922

2023
public static Autoloads Instance { get; private set; }
2124

22-
// Game developers should be able to access each individual manager
23-
public ComponentManager ComponentManager { get; private set; }
25+
public ComponentManager ComponentManager { get; private set; } // Cannot use [Export] here because Godot will bug out and unlink export path in editor after setup completes and restarts the editor
26+
public GameConsole GameConsole { get; private set; } // Cannot use [Export] here because Godot will bug out and unlink export path in editor after setup completes and restarts the editor
2427
public AudioManager AudioManager { get; private set; }
2528
public OptionsManager OptionsManager { get; private set; }
2629
public Services Services { get; private set; }
2730
public MetricsOverlay MetricsOverlay { get; private set; }
2831
public SceneManager SceneManager { get; private set; }
29-
public GameConsole GameConsole { get; private set; }
32+
public Profiler Profiler { get; private set; }
3033

3134
#if NETCODE_ENABLED
32-
private Logger _logger;
35+
public Logger Logger { get; private set; }
3336
#endif
3437

3538
#if DEBUG
@@ -43,13 +46,14 @@ public override void _EnterTree()
4346

4447
Instance = this;
4548
ComponentManager = GetNode<ComponentManager>("ComponentManager");
46-
GameConsole = GetNode<GameConsole>("%Console");
4749
SceneManager = new SceneManager(this, _scenes);
4850
Services = new Services(this);
4951
MetricsOverlay = new MetricsOverlay();
52+
Profiler = new Profiler();
53+
GameConsole = GetNode<GameConsole>("%Console");
5054

5155
#if NETCODE_ENABLED
52-
_logger = new Logger(GameConsole);
56+
Logger = new Logger(GameConsole);
5357
#endif
5458
}
5559

@@ -76,7 +80,7 @@ public override void _Process(double delta)
7680
#endif
7781

7882
#if NETCODE_ENABLED
79-
_logger.Update();
83+
Logger.Update();
8084
#endif
8185
}
8286

@@ -85,11 +89,11 @@ public override void _PhysicsProcess(double delta)
8589
MetricsOverlay.UpdatePhysics();
8690
}
8791

88-
public override async void _Notification(int what)
92+
public override void _Notification(int what)
8993
{
9094
if (what == NotificationWMCloseRequest)
9195
{
92-
await QuitAndCleanup();
96+
ExitGame().FireAndForget();
9397
}
9498
}
9599

@@ -98,32 +102,27 @@ public override void _ExitTree()
98102
AudioManager.Dispose();
99103
OptionsManager.Dispose();
100104
SceneManager.Dispose();
101-
Services.Dispose();
102-
MetricsOverlay.Dispose();
103105

104106
#if DEBUG
105107
_visualizeAutoload.Dispose();
106108
#endif
107109

108110
#if NETCODE_ENABLED
109-
_logger.Dispose();
111+
Logger.Dispose();
110112
#endif
111113

112114
Profiler.Dispose();
113115

114116
Instance = null;
115117
}
116118

117-
// Using deferred is always complicated...
119+
// I'm pretty sure Deferred must be called from a script that extends from Node
118120
public void DeferredSwitchSceneProxy(string rawName, Variant transTypeVariant)
119121
{
120-
if (SceneManager.Instance == null)
121-
return;
122-
123-
SceneManager.Instance.DeferredSwitchScene(rawName, transTypeVariant);
122+
SceneManager.DeferredSwitchScene(rawName, transTypeVariant);
124123
}
125124

126-
public async Task QuitAndCleanup()
125+
public async Task ExitGame()
127126
{
128127
GetTree().AutoAcceptQuit = false;
129128

@@ -132,10 +131,16 @@ public async Task QuitAndCleanup()
132131
{
133132
// Since the PreQuit event contains a Task only the first subscriber will be invoked
134133
// with await PreQuit?.Invoke(); so need to ensure all subs are invoked.
135-
Delegate[] invocationList = PreQuit.GetInvocationList();
136-
foreach (Func<Task> subscriber in invocationList)
134+
foreach (Func<Task> subscriber in PreQuit.GetInvocationList())
137135
{
138-
await subscriber();
136+
try
137+
{
138+
await subscriber();
139+
}
140+
catch (Exception ex)
141+
{
142+
GD.PrintErr($"PreQuit subscriber failed: {ex}");
143+
}
139144
}
140145
}
141146

Framework/Autoloads/Autoloads.tscn

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ Options = ExtResource("5_vm4c5")
1717
Credits = ExtResource("2_xdmsn")
1818
metadata/_custom_type_script = "uid://17yxgcswri77"
1919

20-
[node name="Autoloads" type="Node"]
20+
[node name="Autoloads" type="Node" node_paths=PackedStringArray("GameConsole", "ComponentManager")]
2121
process_mode = 3
2222
script = ExtResource("1_00yjk")
2323
_scenes = SubResource("Resource_xdmsn")
24+
GameConsole = NodePath("Debug/Console")
25+
ComponentManager = NodePath("ComponentManager")
2426

2527
[node name="ComponentManager" type="Node" parent="."]
2628
process_mode = 1

0 commit comments

Comments
 (0)