From 04f0b5ef168f1329137fc9e3dc439e035aa680b4 Mon Sep 17 00:00:00 2001 From: redglow Date: Sat, 18 Jul 2026 12:09:43 +0200 Subject: [PATCH] feat: working version with scopes. --- Game/GameServiceSource.cs | 4 +- Game/LevelServiceSource.cs | 13 ++++--- Game/LevelsData.cs | 2 +- Game/Projectile.cs | 4 +- Game/Score.cs | 10 ++--- Game/Target.cs | 38 +++++++++---------- addons/GodotDI/BuilderExtensions.cs | 6 +++ addons/GodotDI/ISceneInstantiator.cs | 8 ---- addons/GodotDI/IValueGetter.cs | 2 +- addons/GodotDI/RootServiceSource.cs | 30 ++------------- addons/GodotDI/SceneInstantiator.cs | 30 +++++++++++++++ addons/GodotDI/SceneInstantiatorExtensions.cs | 24 ++++++++++++ addons/GodotDI/ServiceSource.cs | 29 +++++++------- addons/GodotDI/ValueProvider.cs | 5 +-- godot-host-test.sln.DotSettings.user | 2 + 15 files changed, 120 insertions(+), 87 deletions(-) create mode 100644 addons/GodotDI/SceneInstantiator.cs create mode 100644 addons/GodotDI/SceneInstantiatorExtensions.cs diff --git a/Game/GameServiceSource.cs b/Game/GameServiceSource.cs index f3209cc..ea88273 100644 --- a/Game/GameServiceSource.cs +++ b/Game/GameServiceSource.cs @@ -14,6 +14,8 @@ public partial class GameServiceSource : RootServiceSource builder .AddSingleton() .AddInstance(_levelsData) - .AddValueProvider(); + .AddValueProvider() + .AddValueProvider() + .AddValueProvider(); } } \ No newline at end of file diff --git a/Game/LevelServiceSource.cs b/Game/LevelServiceSource.cs index c4d34ad..bcfbf58 100644 --- a/Game/LevelServiceSource.cs +++ b/Game/LevelServiceSource.cs @@ -1,5 +1,7 @@ using Godot; using GodotHostTest.GodotDI; +using GodotHostTest.Interfaces; +using OwofGames.GodotLume; namespace GodotHostTest.Game; @@ -7,10 +9,9 @@ public partial class LevelServiceSource : ServiceSource { [Export] private TargetSpawner _targetSpawner = null!; - // protected override void ConfigureServices(IServiceCollection serviceCollection) - // { - // serviceCollection - // .AddSingleton(_targetSpawner) - // .AddSingleton(_targetSpawner); - // } + protected override void OnScopeCreated(IProvider serviceProvider) + { + serviceProvider.Get>().Set(_targetSpawner); + serviceProvider.Get>().Set(_targetSpawner); + } } \ No newline at end of file diff --git a/Game/LevelsData.cs b/Game/LevelsData.cs index 1ce38e3..5cb5d53 100644 --- a/Game/LevelsData.cs +++ b/Game/LevelsData.cs @@ -10,7 +10,7 @@ public partial class LevelsData : Resource, ILevelDataProvider public ILevel GetLevelData(int levelNumber) { - var entry = _levelDataEntries[levelNumber]; + var entry = _levelDataEntries[levelNumber - 1]; return entry; } } \ No newline at end of file diff --git a/Game/Projectile.cs b/Game/Projectile.cs index d2315b4..80896a9 100644 --- a/Game/Projectile.cs +++ b/Game/Projectile.cs @@ -10,12 +10,12 @@ namespace GodotHostTest.Game; public partial class Projectile : Node2D { [Inject] private readonly ILogger _logger = null!; - [Inject] private readonly ITargetSpawner _targetSpawner = null!; + [Inject] private readonly IValueGetter _targetSpawner = null!; // Called when the node enters the scene tree for the first time. public override void _Ready() { - _targetSpawner.TargetSpawned + _targetSpawner.Value.TargetSpawned .Subscribe(_ => _logger.LogInformation("Projectile got informed of target spawning")).AddTo(this); } } \ No newline at end of file diff --git a/Game/Score.cs b/Game/Score.cs index 58e2522..9c7e2c0 100644 --- a/Game/Score.cs +++ b/Game/Score.cs @@ -1,22 +1,24 @@ -using R3; using Godot; using GodotHostTest.GodotDI; using GodotHostTest.Helpers; using GodotHostTest.Interfaces; +using R3; namespace GodotHostTest.Game; public partial class Score : Control, IScore { + [Inject] private readonly IValueGetter _targetSpawner = null!; [Export] private AnimationPlayer _animationPlayer = null!; [Export] private Label _label = null!; - [Inject] private readonly ITargetSpawner _targetSpawner = null!; private int _score; + int IScore.Score => _score; + public override void _Ready() { - _targetSpawner.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this); + _targetSpawner.Value.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this); base._Ready(); } @@ -34,6 +36,4 @@ public partial class Score : Control, IScore { _label.Text = $"Score: {_score}"; } - - int IScore.Score => _score; } \ No newline at end of file diff --git a/Game/Target.cs b/Game/Target.cs index 7cecfa8..03d09a3 100644 --- a/Game/Target.cs +++ b/Game/Target.cs @@ -7,19 +7,30 @@ namespace GodotHostTest.Game; public partial class Target : Node2D, ITarget { - [Export] public double TimeOutDurationInSeconds; - [Export] public required Timer TimeOutTimer; - [Export] public required Sprite2D Image; - [Export] public required Area2D CollisionArea; - - private readonly Subject _timedOut = new(); private readonly Subject _hit = new(); - [Inject] private readonly ITargetCollector _targetCollector = null!; + [Inject] private readonly IValueGetter _targetCollector = null!; + + private readonly Subject _timedOut = new(); + [Export] public required Area2D CollisionArea; + [Export] public required Sprite2D Image; + [Export] public double TimeOutDurationInSeconds; + [Export] public required Timer TimeOutTimer; + + public Observable TimedOut => _timedOut.AsObservable(); + + public Observable Hit => _hit.AsObservable(); + + public void Enable() + { + Image.Visible = true; + CollisionArea.Monitoring = true; + TimeOutTimer.Start(TimeOutDurationInSeconds); + } public override void _Ready() { - _targetCollector.NewTarget(this); + _targetCollector.Value.NewTarget(this); } public override void _ExitTree() @@ -28,10 +39,6 @@ public partial class Target : Node2D, ITarget _hit.Dispose(); } - public Observable TimedOut => _timedOut.AsObservable(); - - public Observable Hit => _hit.AsObservable(); - // todo: hook to collision private void OnHit() { @@ -40,13 +47,6 @@ public partial class Target : Node2D, ITarget _hit.OnNext(Unit.Default); } - public void Enable() - { - Image.Visible = true; - CollisionArea.Monitoring = true; - TimeOutTimer.Start(TimeOutDurationInSeconds); - } - private void Disable() { Image.Visible = false; diff --git a/addons/GodotDI/BuilderExtensions.cs b/addons/GodotDI/BuilderExtensions.cs index 7a4b6dd..87d672b 100644 --- a/addons/GodotDI/BuilderExtensions.cs +++ b/addons/GodotDI/BuilderExtensions.cs @@ -15,4 +15,10 @@ public static class BuilderExtensions .AddScoped, IValueGetter, TDestinationScope>(Builder.NoDependencies, _ => valueProvider.Getter); } + + public static Builder AddValueProvider(this Builder builder) + where TScope : RootScope + { + return builder.AddValueProvider(); + } } \ No newline at end of file diff --git a/addons/GodotDI/ISceneInstantiator.cs b/addons/GodotDI/ISceneInstantiator.cs index 7ca4f38..2a6a6ca 100644 --- a/addons/GodotDI/ISceneInstantiator.cs +++ b/addons/GodotDI/ISceneInstantiator.cs @@ -12,12 +12,4 @@ public interface ISceneInstantiator /// An optional callback invoked once the scope has been created, but before instantiating the scene. /// The created node, with its dependencies satisfied. Node Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null); - - /// - /// Instantiate a packed scene, just like , but also looks for a scope between the top level children and triggers the dependency injection mechanism when found. - /// - /// The packed scene to instantiate. - /// An optional callback invoked once the scope has been created, but before instantiating the scene. - /// The created node, with its dependencies satisfied. - T Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null) where T : Node; } \ No newline at end of file diff --git a/addons/GodotDI/IValueGetter.cs b/addons/GodotDI/IValueGetter.cs index 4787a01..2fd112c 100644 --- a/addons/GodotDI/IValueGetter.cs +++ b/addons/GodotDI/IValueGetter.cs @@ -2,5 +2,5 @@ namespace GodotHostTest.GodotDI; public interface IValueGetter { - public T Get(); + public T Value { get; } } \ No newline at end of file diff --git a/addons/GodotDI/RootServiceSource.cs b/addons/GodotDI/RootServiceSource.cs index aedea9d..b53aa86 100644 --- a/addons/GodotDI/RootServiceSource.cs +++ b/addons/GodotDI/RootServiceSource.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using Godot; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OwofGames.GodotHost; @@ -12,7 +10,7 @@ namespace GodotHostTest.GodotDI; /// /// The base class for a root scope. Implement this class and its abstract methods to kick off the DI system. /// -public abstract partial class RootServiceSource : ServiceSource, ISceneInstantiator +public abstract partial class RootServiceSource : ServiceSource { private Host? _host; @@ -28,28 +26,6 @@ public abstract partial class RootServiceSource : ServiceSource, ISceneInstantia private ILogger Logger => _logger ??= Host.GetLogger(); - /// - public Node Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null) - { - var node = packedScene.Instantiate(); - var scope = node.GetChildren().OfType().SingleOrDefault(); - if (scope == null) - Logger.LogDebug( - "Instantiated the scene {PackedSceneName} through ISceneInstantiator.Instantiate, but no scope found.", - packedScene.GetName()); - else - scope.ResolveInjectedNodes(Host.ServiceProvider, onScopeCreated); - - return node; - } - - /// - public T Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null) - where T : Node - { - return (T)Instantiate(packedScene, onScopeCreated); - } - public override void _EnterTree() { // create the host @@ -59,12 +35,12 @@ public abstract partial class RootServiceSource : ServiceSource, ISceneInstantia .Build(); // resolve nodes - ResolveInjectedNodes(Host.ServiceProvider, null); + ResolveInjectedNodes(Host.ServiceProvider.GetRequiredService(), null); } private void InnerConfigure(IServiceCollection serviceCollection) { - serviceCollection.AddSingleton(this); + serviceCollection.AddTransient(); ConfigureServices(serviceCollection); } diff --git a/addons/GodotDI/SceneInstantiator.cs b/addons/GodotDI/SceneInstantiator.cs new file mode 100644 index 0000000..26128d0 --- /dev/null +++ b/addons/GodotDI/SceneInstantiator.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; +using Godot; +using Microsoft.Extensions.Logging; +using OwofGames.GodotLume; + +namespace GodotHostTest.GodotDI; + +internal class SceneInstantiator(IProvider provider) : ISceneInstantiator +{ + /// + public Node Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null) + { + var node = packedScene.Instantiate(); + var scope = node.GetChildren().OfType().SingleOrDefault(); + if (scope == null) + provider.Get>().LogDebug( + "Instantiated the scene {PackedSceneName} through ISceneInstantiator.Instantiate, but no scope found.", + packedScene.GetName()); + else + scope.ResolveInjectedNodes(provider, onScopeCreated); + + return node; + } + + public T Instantiate(PackedScene packedScene, Delegate? onScopeCreated = null) where T : Node + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/addons/GodotDI/SceneInstantiatorExtensions.cs b/addons/GodotDI/SceneInstantiatorExtensions.cs new file mode 100644 index 0000000..9fe431d --- /dev/null +++ b/addons/GodotDI/SceneInstantiatorExtensions.cs @@ -0,0 +1,24 @@ +using System; +using Godot; + +namespace GodotHostTest.GodotDI; + +public static class SceneInstantiatorExtensions +{ + /// + /// Instantiate a packed scene, just like , but also looks for a scope between + /// the top level children and triggers the dependency injection mechanism when found. + /// + /// The scene instantiator to use. + /// The packed scene to instantiate. + /// + /// An optional callback invoked once the scope has been created, but before instantiating the + /// scene. + /// + /// The created node, with its dependencies satisfied. + private static T Instantiate(this ISceneInstantiator sceneInstantiator, PackedScene packedScene, + Delegate? onScopeCreated = null) where T : Node + { + return (T)sceneInstantiator.Instantiate(packedScene, onScopeCreated); + } +} \ No newline at end of file diff --git a/addons/GodotDI/ServiceSource.cs b/addons/GodotDI/ServiceSource.cs index 7be0c5f..b0fc86f 100644 --- a/addons/GodotDI/ServiceSource.cs +++ b/addons/GodotDI/ServiceSource.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Reflection; using Godot; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OwofGames.GodotLume; @@ -15,29 +14,28 @@ public partial class ServiceSource : Node [Export] private string _serviceScope = RootScopeQualifiedName; [Export] protected Node?[] InjectedNodes = []; - internal void ResolveInjectedNodes(IServiceProvider serviceProvider, Delegate? onScopeCreated) + internal void ResolveInjectedNodes(IProvider provider, Delegate? onScopeCreated) { - var logger = serviceProvider.GetRequiredService>(); + var logger = provider.Get>(); // if this service source must also act as a scope, create the scope and use its service provider if (_serviceScope != RootScopeQualifiedName) { // create the scoped provider var serviceScope = ScopeTypesAndNames.GetType(_serviceScope); - var scopedProvider = serviceProvider - .GetRequiredService() - .GetScopedProvider(serviceScope); - // convert it also to an IServiceProvider - var serviceProviderCreator = serviceProvider.GetRequiredService>(); - serviceProvider = serviceProviderCreator(scopedProvider); + provider = provider.GetScopedProvider(serviceScope); // invoke onScopeCreated, if present // TODO: replace with a source code generation by turning this method private partial with Action argument, and every invocation site calls the necessary provider.Get<...> to build the argument list if (onScopeCreated != null) { - var parameters = onScopeCreated.Method.GetParameters() - .Select(parameterInfo => scopedProvider.Get(parameterInfo.ParameterType)); + var parameters = onScopeCreated.Method + .GetParameters() + .Select(parameterInfo => provider.Get(parameterInfo.ParameterType)) + .ToArray(); onScopeCreated.DynamicInvoke(parameters); } + + OnScopeCreated(provider); } else if (onScopeCreated != null) { @@ -56,7 +54,7 @@ public partial class ServiceSource : Node { if (field.GetCustomAttribute() == null) continue; var fieldType = field.FieldType; - var service = serviceProvider.GetRequiredService(fieldType); + var service = provider.Get(fieldType); field.SetValue(node, service); } @@ -70,7 +68,7 @@ public partial class ServiceSource : Node foreach (var parameter in parameters) { var parameterType = parameter.ParameterType; - var service = serviceProvider.GetRequiredService(parameterType); + var service = provider.Get(parameterType); values[i++] = service; } @@ -78,4 +76,9 @@ public partial class ServiceSource : Node } } } + + // TODO: also add source generator to mark methods in derived classes with [OnScopeCreated] and perform parameter injection + protected virtual void OnScopeCreated(IProvider serviceProvider) + { + } } \ No newline at end of file diff --git a/addons/GodotDI/ValueProvider.cs b/addons/GodotDI/ValueProvider.cs index e6e5a05..9d49640 100644 --- a/addons/GodotDI/ValueProvider.cs +++ b/addons/GodotDI/ValueProvider.cs @@ -38,9 +38,6 @@ public class ValueProvider private class GetterImplementation(T value) : IValueGetter { - public T Get() - { - return value; - } + public T Value => value; } } \ No newline at end of file diff --git a/godot-host-test.sln.DotSettings.user b/godot-host-test.sln.DotSettings.user index 4d36f80..7fb3b90 100644 --- a/godot-host-test.sln.DotSettings.user +++ b/godot-host-test.sln.DotSettings.user @@ -4,6 +4,8 @@ True True ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded /usr/lib/dotnet/dotnet