From 5c4519ed26462aea1ed80171e052c473f432c000 Mon Sep 17 00:00:00 2001 From: redglow Date: Wed, 29 Jul 2026 11:05:27 +0200 Subject: [PATCH] chore: cleanup --- Game/Game/GameServiceSource.cs | 9 ++++++-- Game/Level/LevelServiceSource.cs | 13 ++++++----- Game/Level/LevelServices.cs | 12 +++-------- Game/Score/Score.cs | 16 ++++++++++---- Game/Score/TotalScore.cs | 21 +++++++++--------- Game/Target/Target.cs | 30 +++++++++++--------------- Game/Target/TargetSpawner.cs | 21 +++++++++++------- Game/Turret/Turret.cs | 1 - Helpers/ObservableHelpers.cs | 7 ++++++ Interfaces/ICurrentLevel.cs | 10 +++++++++ Interfaces/ILevel.cs | 3 +++ Interfaces/ILevelProvider.cs | 8 +++++++ Interfaces/IScore.cs | 2 +- Interfaces/ITarget.cs | 7 +++--- Interfaces/ITargetCollector.cs | 2 +- Interfaces/ITargetEventBus.cs | 7 ++++-- Interfaces/ITargetSpawner.cs | 2 +- addons/AetherBind/BuilderExtensions.cs | 6 ++++-- godot-host-test.csproj | 1 + godot-host-test.sln.DotSettings.user | 2 ++ 20 files changed, 110 insertions(+), 70 deletions(-) diff --git a/Game/Game/GameServiceSource.cs b/Game/Game/GameServiceSource.cs index 2fa4cd3..f81b614 100644 --- a/Game/Game/GameServiceSource.cs +++ b/Game/Game/GameServiceSource.cs @@ -1,4 +1,5 @@ using Godot; +using GodotHostTest.AetherBind; using GodotHostTest.Game.Level; using GodotHostTest.Game.Score; using GodotHostTest.Game.Target; @@ -6,7 +7,6 @@ using GodotHostTest.Interfaces; using Microsoft.Extensions.DependencyInjection; using OwofGames.GodotHost.Observability; using OwofGames.GodotLume; -using RootServiceSource = GodotHostTest.AetherBind.RootServiceSource; namespace GodotHostTest.Game.Game; @@ -17,9 +17,14 @@ public partial class GameServiceSource : RootServiceSource protected override void Configure(Builder builder) { builder + // root scope services .AddSingleton() .AddSingleton() - .AddLevelServices(_levelsData); + // current level / level data management + .AddInstance(_levelsData) + .AddSingleton() + // level scope services + .AddLevelServices(); } protected override void ConfigureServices(IServiceCollection serviceCollection) diff --git a/Game/Level/LevelServiceSource.cs b/Game/Level/LevelServiceSource.cs index 153be07..de3b1f2 100644 --- a/Game/Level/LevelServiceSource.cs +++ b/Game/Level/LevelServiceSource.cs @@ -13,7 +13,8 @@ namespace GodotHostTest.Game.Level; public partial class LevelServiceSource : ServiceSource { - private Activity? _activityValue; + private Activity? _levelActivity; + [Export] private TargetSpawner _targetSpawner = null!; protected override void OnScopeCreated(IProvider serviceProvider) @@ -22,15 +23,13 @@ public partial class LevelServiceSource : ServiceSource } [WithProvider] - private void OnScopeCreated(IValueSetter targetCollector, - IValueSetter targetSpawner, IActivitySourceFactory activitySourceFactory, - IValueSetter activity) + private void OnScopeCreated(IActivitySourceFactory activitySourceFactory, + IValueSetter targetCollector, IValueSetter targetSpawner) { // ReSharper disable once ExplicitCallerInfoArgument - the caller name is not significant in this case - _activityValue = activitySourceFactory.ActivitySource.StartActivity("Level") ?? + _levelActivity = activitySourceFactory.ActivitySource.StartActivity("Level") ?? throw new InvalidOperationException( "No listeners for the activity - is OpenTelemetry running?"); - activity.Set(_activityValue); targetCollector.Set(_targetSpawner); targetSpawner.Set(_targetSpawner); } @@ -38,6 +37,6 @@ public partial class LevelServiceSource : ServiceSource public override void _Ready() { // after 3 seconds, dispose the activity for simulation purposes - GetTree().CreateTimer(3).Timeout += _activityValue!.Dispose; + GetTree().CreateTimer(3).Timeout += _levelActivity!.Dispose; } } \ No newline at end of file diff --git a/Game/Level/LevelServices.cs b/Game/Level/LevelServices.cs index e6b83b5..362460e 100644 --- a/Game/Level/LevelServices.cs +++ b/Game/Level/LevelServices.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using GodotHostTest.AetherBind; using GodotHostTest.Interfaces; using OwofGames.GodotLume; @@ -7,17 +6,12 @@ namespace GodotHostTest.Game.Level; public static class LevelServices { - public static void AddLevelServices(this Builder builder, LevelsData levelsData) + public static void AddLevelServices(this Builder builder) { builder - // current level / level data management - .AddSingleton() - .AddInstance(levelsData) - // value provider for activity - .AddValueProvider() - // value providers to send data into level scope + // value providers to send level data from root to level scope .AddValueProvider() - // value provider for level scope setup + // value providers for level scope setup .AddValueProvider() .AddValueProvider(); } diff --git a/Game/Score/Score.cs b/Game/Score/Score.cs index 41d64b5..28dc755 100644 --- a/Game/Score/Score.cs +++ b/Game/Score/Score.cs @@ -9,16 +9,24 @@ namespace GodotHostTest.Game.Score; public partial class Score : Control { [Export] private AnimationPlayer _animationPlayer = null!; + [Inject] private ICurrentLevel _currentLevel = null!; [Export] private Label _label = null!; - [Inject] private IScore _score = null!; public override void _Ready() { - _score.Score.Subscribe(UpdateScore).AddTo(this); - _score.Score.Chunk(2, 1).Where(values => values.Length == 2 && values[1] < values[0]) - .Subscribe(OnScoreDecreasing).AddTo(this); + // update the score label whenever the score changes + _score.Score + .Subscribe(UpdateScore) + .AddTo(this); + + // run an animation whenever the score decreases + _score.Score + .Chunk(2, 1) + .Where(values => values.Length == 2 && values[1] < values[0]) + .Subscribe(OnScoreDecreasing) + .AddTo(this); } private void OnScoreDecreasing(T _) diff --git a/Game/Score/TotalScore.cs b/Game/Score/TotalScore.cs index 3d56c9a..cae52f8 100644 --- a/Game/Score/TotalScore.cs +++ b/Game/Score/TotalScore.cs @@ -6,24 +6,23 @@ namespace GodotHostTest.Game.Score; public class TotalScore : IScore, IDisposable { - private readonly IDisposable _connectedObservableDisposable; - public TotalScore(ITargetEventBus targetEventBus) { - var connectedObservable = targetEventBus.Hit.Select(_ => 20) - .Merge(targetEventBus.TimedOut.Select(_ => -5)) - .Scan(0, (x1, x2) => x1 + x2) - .Prepend(0) - .Replay(1); - _connectedObservableDisposable = connectedObservable.Connect(); - Score = connectedObservable.AsObservable(); + Score = + // map every hit target to +20 points + targetEventBus.Hit.Select(_ => 20) + // map every missed target to -5 points + .Merge(targetEventBus.TimedOut.Select(_ => -5)) + // sum them all (starting with 0) + .Scan(0, (x1, x2) => x1 + x2) + .ToReadOnlyReactiveProperty(); } public void Dispose() { - _connectedObservableDisposable.Dispose(); + Score.Dispose(); GC.SuppressFinalize(this); } - public Observable Score { get; } + public ReadOnlyReactiveProperty Score { get; } } \ No newline at end of file diff --git a/Game/Target/Target.cs b/Game/Target/Target.cs index ed7b527..34028b1 100644 --- a/Game/Target/Target.cs +++ b/Game/Target/Target.cs @@ -8,14 +8,14 @@ namespace GodotHostTest.Game.Target; public partial class Target : Node2D, ITarget { private readonly Subject _hit = new(); - private readonly Subject _timedOut = new(); + [Export] public required Area2D CollisionArea; [Export] public required Sprite2D Image; [Export] public required Timer TimeOutTimer; [Export] private PackedScene _explosionPackedScene = null!; - [Inject] private ILevel _level = null!; + [Inject] private ILevel _level = null!; [Inject] private ITargetCollector _targetCollector = null!; public Observable TimedOut => _timedOut.AsObservable(); @@ -30,26 +30,30 @@ public partial class Target : Node2D, ITarget TimeOutTimer.Start(_level.SpawnDuration.TotalSeconds); } + private void Disable() + { + Image.Visible = false; + // can't set monitoring/monitorable when inside collision code, like during OnHit, so let's call them deferred + CollisionArea.SetDeferred(Area2D.PropertyName.Monitoring, false); + CollisionArea.SetDeferred(Area2D.PropertyName.Monitorable, false); + TimeOutTimer.Stop(); + } + public override void _Ready() { _targetCollector.NewTarget(this); } - public override void _ExitTree() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); _timedOut.Dispose(); _hit.Dispose(); } private void OnAreaEntered(Area2D _) - { - OnHit(); - } - - private void OnHit() { Disable(); - TimeOutTimer.Stop(); _hit.OnNext(Unit.Default); var explosion = (GpuParticles2D)_explosionPackedScene.Instantiate(); @@ -58,14 +62,6 @@ public partial class Target : Node2D, ITarget GetTree().CurrentScene.AddChild(explosion); } - private void Disable() - { - Image.Visible = false; - // can't se monitoring/monitorable when inside the collision code, like during OnHit - CollisionArea.SetDeferred(Area2D.PropertyName.Monitoring, false); - CollisionArea.SetDeferred(Area2D.PropertyName.Monitorable, false); - } - private void OnTimeout() { Disable(); diff --git a/Game/Target/TargetSpawner.cs b/Game/Target/TargetSpawner.cs index 4957c3b..9586faf 100644 --- a/Game/Target/TargetSpawner.cs +++ b/Game/Target/TargetSpawner.cs @@ -10,31 +10,36 @@ namespace GodotHostTest.Game.Target; public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner { private readonly List _availableTargets = []; - private readonly Subject _targetSpawned = new(); + [Export] public required Timer Timer; [Inject] private ILevel _level = null!; [Inject] private ITargetEventBus _targetEventBus = null!; + /// public void NewTarget(ITarget target) { + // record the target as available _availableTargets.Add(target); - target.Hit.Merge(target.TimedOut).Select(_ => target).Subscribe(MakeTargetAvailable).AddTo(this); + + // whenever the target gets disabled, mark it as available again + target.Hit.Merge(target.TimedOut) + .Select(_ => target) + .Subscribe(_availableTargets.Add) + .AddTo(this); } public Observable TargetSpawned => _targetSpawned.AsObservable(); public override void _Ready() { + // inform the target event bus that there's a new target spawner _targetEventBus.NewTargetSpawner(this); - Spawn(); - Timer.Start(_level.TimeBetweenSpawns.TotalSeconds); - } - private void MakeTargetAvailable(ITarget target) - { - _availableTargets.Add(target); + // spawn every total seconds (and immediately spawn something at ready) + Timer.Start(_level.TimeBetweenSpawns.TotalSeconds); + Spawn(); } private void OnTimer() diff --git a/Game/Turret/Turret.cs b/Game/Turret/Turret.cs index 7beb5a2..ac4ef2f 100644 --- a/Game/Turret/Turret.cs +++ b/Game/Turret/Turret.cs @@ -11,7 +11,6 @@ public partial class Turret : Sprite2D [Inject] private ILogger _logger = null!; [Export] private PackedScene _projectilePackedScene = null!; - private double _rotationDirection; [Export] private double _rotationSpeed = 6; [Inject] private ISceneInstantiator _sceneInstantiator = null!; diff --git a/Helpers/ObservableHelpers.cs b/Helpers/ObservableHelpers.cs index 440e9b1..235f1f1 100644 --- a/Helpers/ObservableHelpers.cs +++ b/Helpers/ObservableHelpers.cs @@ -16,4 +16,11 @@ public static class ObservableHelpers }); return disposable; } + + public static ConnectableObservable Connect(this ConnectableObservable connectableObservable, + out IDisposable disposable) + { + disposable = connectableObservable.Connect(); + return connectableObservable; + } } \ No newline at end of file diff --git a/Interfaces/ICurrentLevel.cs b/Interfaces/ICurrentLevel.cs index 63ec1a3..cc837fb 100644 --- a/Interfaces/ICurrentLevel.cs +++ b/Interfaces/ICurrentLevel.cs @@ -2,8 +2,18 @@ using R3; namespace GodotHostTest.Interfaces; +/// +/// The object that handles the current level. +/// public interface ICurrentLevel { + /// + /// The current level. + /// public ReadOnlyReactiveProperty LevelNumber { get; } + + /// + /// Move to the next level. + /// public void NextLevel(); } \ No newline at end of file diff --git a/Interfaces/ILevel.cs b/Interfaces/ILevel.cs index c1bf0a0..11e5f2f 100644 --- a/Interfaces/ILevel.cs +++ b/Interfaces/ILevel.cs @@ -2,6 +2,9 @@ using System; namespace GodotHostTest.Interfaces; +/// +/// Description of a single level. +/// public interface ILevel { /// diff --git a/Interfaces/ILevelProvider.cs b/Interfaces/ILevelProvider.cs index fcfdc51..13b8318 100644 --- a/Interfaces/ILevelProvider.cs +++ b/Interfaces/ILevelProvider.cs @@ -1,6 +1,14 @@ namespace GodotHostTest.Interfaces; +/// +/// An object that provides the information about a certain level. +/// public interface ILevelDataProvider { + /// + /// Return the level data for a specific level. + /// + /// Level number. + /// The information about a certain level. public ILevel GetLevelData(int levelNumber); } \ No newline at end of file diff --git a/Interfaces/IScore.cs b/Interfaces/IScore.cs index 5465945..867c71d 100644 --- a/Interfaces/IScore.cs +++ b/Interfaces/IScore.cs @@ -10,5 +10,5 @@ public interface IScore /// /// The current score. /// - public Observable Score { get; } + public ReadOnlyReactiveProperty Score { get; } } \ No newline at end of file diff --git a/Interfaces/ITarget.cs b/Interfaces/ITarget.cs index abb82de..57fc48b 100644 --- a/Interfaces/ITarget.cs +++ b/Interfaces/ITarget.cs @@ -3,18 +3,17 @@ using R3; namespace GodotHostTest.Interfaces; /// -/// A target for our projectiles. +/// A target for our projectiles. Targets can be reused multiple times. /// public interface ITarget { /// - /// The target hasn't been hit in the allotted time and has timed out; once this observable emits an event, both - /// it and complete. + /// An observable that emits a value when the target hasn't been hit in the allotted time and has timed out. Every time this observable emits, the target becomes disabled. /// Observable TimedOut { get; } /// - /// The target has been hit; once this observable emits an event, both it and complete. + /// An observable that emits a value when the target has been hit in the allotted time. Every time this observable emits, the target becomes disabled. /// Observable Hit { get; } diff --git a/Interfaces/ITargetCollector.cs b/Interfaces/ITargetCollector.cs index 03f7743..1cff133 100644 --- a/Interfaces/ITargetCollector.cs +++ b/Interfaces/ITargetCollector.cs @@ -1,7 +1,7 @@ namespace GodotHostTest.Interfaces; /// -/// An object which collects targets as soon as they are created. +/// An object which receives targets as soon as they are created. /// public interface ITargetCollector { diff --git a/Interfaces/ITargetEventBus.cs b/Interfaces/ITargetEventBus.cs index f99130d..8d1d0f1 100644 --- a/Interfaces/ITargetEventBus.cs +++ b/Interfaces/ITargetEventBus.cs @@ -2,6 +2,9 @@ using R3; namespace GodotHostTest.Interfaces; +/// +/// An event bus that summarizes information from all 's targets. +/// public interface ITargetEventBus { /// @@ -15,8 +18,8 @@ public interface ITargetEventBus Observable Hit { get; } /// - /// A new target spawner has been added. + /// Callback method to invoke when a new target spawner has been added. /// - /// + /// The new target spawner. void NewTargetSpawner(ITargetSpawner targetSpawner); } \ No newline at end of file diff --git a/Interfaces/ITargetSpawner.cs b/Interfaces/ITargetSpawner.cs index cd7bf8e..042a77b 100644 --- a/Interfaces/ITargetSpawner.cs +++ b/Interfaces/ITargetSpawner.cs @@ -3,7 +3,7 @@ using R3; namespace GodotHostTest.Interfaces; /// -/// An object that spawns targets. +/// An object that spawns targets. A target is considered spawned whenever it's enabled, even if it was already present in the scene. /// public interface ITargetSpawner { diff --git a/addons/AetherBind/BuilderExtensions.cs b/addons/AetherBind/BuilderExtensions.cs index 1012243..4e996f1 100644 --- a/addons/AetherBind/BuilderExtensions.cs +++ b/addons/AetherBind/BuilderExtensions.cs @@ -1,10 +1,12 @@ +using JetBrains.Annotations; using OwofGames.GodotLume; namespace GodotHostTest.AetherBind; public static class BuilderExtensions { - public static Builder AddValueProvider(this Builder builder) + public static Builder AddValueProvider(this Builder builder) where TValueType : class where TSourceScope : RootScope where TDestinationScope : TSourceScope @@ -22,7 +24,7 @@ public static class BuilderExtensions provider => provider.Get>().Value); } - public static Builder AddValueProvider(this Builder builder) + public static Builder AddValueProvider(this Builder builder) where TValueType : class where TScope : RootScope { diff --git a/godot-host-test.csproj b/godot-host-test.csproj index e9bb39b..a11ccfa 100644 --- a/godot-host-test.csproj +++ b/godot-host-test.csproj @@ -7,6 +7,7 @@ enable + diff --git a/godot-host-test.sln.DotSettings.user b/godot-host-test.sln.DotSettings.user index b5cc359..c66414f 100644 --- a/godot-host-test.sln.DotSettings.user +++ b/godot-host-test.sln.DotSettings.user @@ -21,8 +21,10 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded