feat: automatic injection of elements in scene.

This commit is contained in:
redglow
2026-07-17 15:14:40 +02:00
parent 659baff30c
commit 5ad3af84b2
15 changed files with 86 additions and 219 deletions
@@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace GodotHostTest.Game; namespace GodotHostTest.Game;
public partial class GameRootServiceSource : RootServiceSource public partial class LevelServiceSource : RootServiceSource
{ {
[Export] private TargetSpawner _targetSpawner = null!; [Export] private TargetSpawner _targetSpawner = null!;
+5
View File
@@ -0,0 +1,5 @@
namespace GodotHostTest.Interfaces;
public interface ILevel
{
}
@@ -1,11 +1,13 @@
#if TOOLS
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using Godot; using Godot;
namespace GodotHostTest.addons.godot_di; namespace GodotHostTest.GodotDI.Editor;
/// <summary> /// <summary>
/// Taken from https://github.com/godotengine/godot-proposals/issues/12294 /// Taken from https://github.com/godotengine/godot-proposals/issues/12294
@@ -23,7 +25,7 @@ public static class GodotScriptPathCache
{ {
if (_assemblyLoadEventHandler != null) return; if (_assemblyLoadEventHandler != null) return;
Initialize(); Initialize();
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly())! AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly())!
.Unloading += _ => { Deinitialize(); }; .Unloading += _ => { Deinitialize(); };
} }
@@ -87,4 +89,21 @@ public static class GodotScriptPathCache
InitializeIfNeeded(); InitializeIfNeeded();
return PathToType.TryGetValue(path, out type); return PathToType.TryGetValue(path, out type);
} }
public static Type? GetCSharpScriptType(this GodotObject node)
{
if (node.GetScript().AsGodotObject() is CSharpScript script &&
TryGetTypeFromPath(script.ResourcePath, out var type))
return type;
return null;
}
public static bool IsServiceSource(this GodotObject node)
{
return node.GetScript().AsGodotObject() is CSharpScript script &&
TryGetTypeFromPath(script.ResourcePath, out var type) &&
type.IsAssignableTo(typeof(ServiceSource));
}
} }
#endif
@@ -1,16 +1,12 @@
#if TOOLS
using System; using System;
using System.Linq; using System.Linq;
using Godot; using Godot;
namespace GodotHostTest.GodotDI; namespace GodotHostTest.GodotDI.Editor;
public partial class ScopePickerEditor : EditorProperty public partial class ScopePickerEditor : EditorProperty
{ {
/// <summary>
/// The "combo box" that picks a choice from the scopes.
/// </summary>
private OptionButton _optionButton = new();
/// <summary> /// <summary>
/// The currently chosen type. /// The currently chosen type.
/// </summary> /// </summary>
@@ -21,6 +17,11 @@ public partial class ScopePickerEditor : EditorProperty
/// </summary> /// </summary>
private string[] _fullyQualifiedNames; private string[] _fullyQualifiedNames;
/// <summary>
/// The "combo box" that picks a choice from the scopes.
/// </summary>
private OptionButton _optionButton = new();
/// <summary> /// <summary>
/// A guard against internal changes while the property is being updated. /// A guard against internal changes while the property is being updated.
/// </summary> /// </summary>
@@ -93,3 +94,4 @@ public partial class ScopePickerEditor : EditorProperty
_optionButton.Selected = Array.IndexOf(_fullyQualifiedNames, _currentType); _optionButton.Selected = Array.IndexOf(_fullyQualifiedNames, _currentType);
} }
} }
#endif
+33
View File
@@ -0,0 +1,33 @@
using System.Collections.Generic;
namespace GodotHostTest.GodotDI;
public static class EnumerableExtensions
{
public enum SingleFailureReason
{
LessThanOne,
MoreThanOne
}
public static T? Single<T>(this IEnumerable<T> enumerable, out SingleFailureReason? failureReason)
where T : class
{
using var enumerator = enumerable.GetEnumerator();
if (!enumerator.MoveNext())
{
failureReason = SingleFailureReason.LessThanOne;
return null;
}
var value = enumerator.Current;
if (enumerator.MoveNext())
{
failureReason = SingleFailureReason.MoreThanOne;
return null;
}
failureReason = null;
return value;
}
}
-64
View File
@@ -1,64 +0,0 @@
namespace GodotHostTest.addons.godot_di;
// RandomIntEditor.cs
#if TOOLS
using Godot;
public partial class RandomIntEditor : EditorProperty
{
// The main control for editing the property.
private Button _propertyControl = new Button();
// An internal value of the property.
private int _currentValue = 0;
// A guard against internal changes when the property is updated.
private bool _updating = false;
public RandomIntEditor()
{
// Add the control as a direct child of EditorProperty node.
AddChild(_propertyControl);
// Make sure the control is able to retain the focus.
AddFocusable(_propertyControl);
// Setup the initial state and connect to the signal to track changes.
RefreshControlText();
_propertyControl.Pressed += OnButtonPressed;
}
private void OnButtonPressed()
{
// Ignore the signal if the property is currently being updated.
if (_updating)
{
return;
}
// Generate a new random integer between 0 and 99.
_currentValue = (int)GD.Randi() % 100;
RefreshControlText();
EmitChanged(GetEditedProperty(), _currentValue);
}
public override void _UpdateProperty()
{
// Read the current value from the property.
var newValue = (int)GetEditedObject().Get(GetEditedProperty());
if (newValue == _currentValue)
{
return;
}
// Update the control with the new value.
_updating = true;
_currentValue = newValue;
RefreshControlText();
_updating = false;
}
private void RefreshControlText()
{
_propertyControl.Text = $"Value: {_currentValue}";
}
}
#endif
-1
View File
@@ -1 +0,0 @@
uid://db526cjmnd4q7
+10 -5
View File
@@ -9,17 +9,22 @@ namespace GodotHostTest.GodotDI;
[Icon("res://addons/GodotDI/ServiceSource.svg")] [Icon("res://addons/GodotDI/ServiceSource.svg")]
public partial class ServiceSource : Node public partial class ServiceSource : Node
{ {
[Export] private string _serviceScope = typeof(RootScope).AssemblyQualifiedName!; private static readonly string RootScopeQualifiedName = typeof(RootScope).AssemblyQualifiedName!;
[Export] private string _serviceScope = RootScopeQualifiedName;
[Export] protected Node?[] InjectedNodes = []; [Export] protected Node?[] InjectedNodes = [];
internal void ResolveInjectedNodes(IServiceProvider serviceProvider) internal void ResolveInjectedNodes(IServiceProvider serviceProvider)
{ {
// if this service source must also act as a scope, create the scope and use its service provider // if this service source must also act as a scope, create the scope and use its service provider
if (_serviceScope != "") if (_serviceScope != RootScopeQualifiedName)
{ {
serviceProvider = serviceProvider var serviceProviderCreator = serviceProvider.GetRequiredService<Func<IProvider, IServiceProvider>>();
.CreateScope() var scopedProvider = serviceProvider.GetRequiredService<IProvider>()
.ServiceProvider; .GetScopedProvider(ScopeTypesAndNames.GetType(_serviceScope));
serviceProvider = serviceProviderCreator(scopedProvider);
// serviceProvider = serviceProvider
// .CreateScope()
// .ServiceProvider;
} }
// resolve the injected nodes against the chosen service provider // resolve the injected nodes against the chosen service provider
+1 -1
View File
@@ -4,4 +4,4 @@ name="GodotDI"
description="" description=""
author="owof games" author="owof games"
version="0.1" version="0.1"
script="GodotDI.cs" script="Editor/Plugin.cs"
+5 -137
View File
@@ -1,142 +1,10 @@
[gd_scene format=3 uid="uid://vsburk37p4u"] [gd_scene format=3 uid="uid://vsburk37p4u"]
[ext_resource type="PackedScene" uid="uid://3v8kq1rfwhmr" path="res://Game/target.tscn" id="2_c77vg"] [ext_resource type="Script" uid="uid://bvjvojer872c8" path="res://Game/Root.cs" id="1_iqet6"]
[ext_resource type="Script" uid="uid://cxfrtglku2gfa" path="res://Game/TargetSpawner.cs" id="3_iqet6"] [ext_resource type="PackedScene" uid="uid://xfd48ep5qi1k" path="res://Game/level.tscn" id="3_bbjqm"]
[ext_resource type="Script" uid="uid://dk578xnlxjwrr" path="res://Game/GameRootScope.cs" id="3_t2jh2"]
[ext_resource type="Script" uid="uid://bgrjeadxhgmvi" path="res://Game/Score.cs" id="4_t2jh2"]
[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="5_4ty2a"]
[ext_resource type="Script" uid="uid://ce7gnp1lcrrdv" path="res://Game/Turret.cs" id="6_rckqj"]
[ext_resource type="PackedScene" uid="uid://brbufibn7hi3o" path="res://Game/projectile.tscn" id="7_rckqj"]
[sub_resource type="Animation" id="Animation_bbjqm"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Score:offset_transform_position")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Vector2(0, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Score:offset_transform_rotation")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [0.0]
}
[sub_resource type="Animation" id="Animation_4ty2a"]
resource_name = "wiggle"
length = 0.3
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Score:offset_transform_position")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.049999997, 0.099999994, 0.15, 0.2, 0.24999999),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
"update": 0,
"values": [Vector2(0, 0), Vector2(10, -10), Vector2(-10, -10), Vector2(10, 10), Vector2(-10, 10), Vector2(0, 0)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Score:offset_transform_rotation")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0, 0.049999997, 0.099999994, 0.15, 0.2, 0.24999999),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
"update": 0,
"values": [0.0, 0.08726646, -0.08726646, 0.08726646, -0.08726646, 0.0]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_rckqj"]
_data = {
&"RESET": SubResource("Animation_bbjqm"),
&"wiggle": SubResource("Animation_4ty2a")
}
[node name="Node2D" type="Node2D" unique_id=920849082] [node name="Node2D" type="Node2D" unique_id=920849082]
script = ExtResource("1_iqet6")
_levelScene = ExtResource("3_bbjqm")
[node name="RootScope" type="Node" parent="." unique_id=1453141084 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")] [node name="Level" parent="." unique_id=1331632726 instance=ExtResource("3_bbjqm")]
script = ExtResource("3_t2jh2")
_targetSpawner = NodePath("../Spawner")
InjectedNodes = [NodePath("../Turret"), NodePath("../Target"), NodePath("../Target2"), NodePath("../Target3"), NodePath("../Target4"), NodePath("../Target5"), NodePath("../Target6"), NodePath("../Spawner"), NodePath("../Score")]
[node name="Target" parent="." unique_id=2101558052 instance=ExtResource("2_c77vg")]
position = Vector2(198, 143)
TimeOutDurationInSeconds = 2.0
[node name="Target2" parent="." unique_id=1480287846 instance=ExtResource("2_c77vg")]
position = Vector2(669, 64)
TimeOutDurationInSeconds = 2.0
[node name="Target3" parent="." unique_id=242951997 instance=ExtResource("2_c77vg")]
position = Vector2(1060, 263)
TimeOutDurationInSeconds = 2.0
[node name="Target4" parent="." unique_id=1190782819 instance=ExtResource("2_c77vg")]
position = Vector2(961, 620)
TimeOutDurationInSeconds = 2.0
[node name="Target5" parent="." unique_id=1183803617 instance=ExtResource("2_c77vg")]
position = Vector2(469, 591)
TimeOutDurationInSeconds = 2.0
[node name="Target6" parent="." unique_id=2017833482 instance=ExtResource("2_c77vg")]
position = Vector2(53, 417)
TimeOutDurationInSeconds = 2.0
[node name="Spawner" type="Node2D" parent="." unique_id=1331368036 node_paths=PackedStringArray("Timer")]
script = ExtResource("3_iqet6")
TimeBetweenSpawns = 1.5
Timer = NodePath("Timer")
[node name="Timer" type="Timer" parent="Spawner" unique_id=1142098929]
[node name="Score" type="ColorRect" parent="." unique_id=1990204205 node_paths=PackedStringArray("_animationPlayer", "_label")]
offset_right = 200.0
offset_bottom = 40.0
offset_transform_enabled = true
color = Color(1, 1, 1, 0.2784314)
script = ExtResource("4_t2jh2")
_animationPlayer = NodePath("../AnimationPlayer")
_label = NodePath("Label")
[node name="Label" type="Label" parent="Score" unique_id=1929317028]
layout_mode = 1
anchors_preset = -1
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 10.0
offset_right = -10.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(0, 0, 0, 1)
text = "Score: 0"
vertical_alignment = 1
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1749711689]
libraries/ = SubResource("AnimationLibrary_rckqj")
[node name="Turret" type="Sprite2D" parent="." unique_id=1932581411]
modulate = Color(1.92523e-07, 0.88004553, 0.8802661, 1)
position = Vector2(539, 307)
texture = ExtResource("5_4ty2a")
script = ExtResource("6_rckqj")
_projectilePackedScene = ExtResource("7_rckqj")
[connection signal="timeout" from="Spawner/Timer" to="Spawner" method="OnTimer"]