This commit is contained in:
redglow
2026-07-17 15:14:51 +02:00
parent 5ad3af84b2
commit d689e9ea1d
9 changed files with 362 additions and 62 deletions
+15
View File
@@ -0,0 +1,15 @@
using Godot;
using GodotHostTest.GodotDI;
namespace GodotHostTest.Game;
public partial class Root : Node2D
{
[Inject] private readonly ISceneInstantiator _sceneInstantiator = null!;
[Export] private PackedScene _levelScene = null!;
public override void _Ready()
{
AddChild(_sceneInstantiator.Instantiate(_levelScene));
}
}
+1
View File
@@ -0,0 +1 @@
uid://bvjvojer872c8
+144
View File
@@ -0,0 +1,144 @@
[gd_scene format=3 uid="uid://xfd48ep5qi1k"]
[ext_resource type="Script" uid="uid://dk578xnlxjwrr" path="res://Game/LevelServiceSource.cs" id="1_5saw1"]
[ext_resource type="PackedScene" uid="uid://3v8kq1rfwhmr" path="res://Game/target.tscn" id="1_y7rhs"]
[ext_resource type="Script" uid="uid://cxfrtglku2gfa" path="res://Game/TargetSpawner.cs" id="2_5saw1"]
[ext_resource type="Script" uid="uid://bgrjeadxhgmvi" path="res://Game/Score.cs" id="3_spj1u"]
[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="4_esqs7"]
[ext_resource type="Script" uid="uid://ce7gnp1lcrrdv" path="res://Game/Turret.cs" id="5_himod"]
[ext_resource type="PackedScene" uid="uid://brbufibn7hi3o" path="res://Game/projectile.tscn" id="6_sin03"]
[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("Level/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("Level/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("Level/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("Level/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="Level" type="Node2D" unique_id=1331632726]
[node name="RootScope" type="Node" parent="." unique_id=1267156771 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")]
script = ExtResource("1_5saw1")
_targetSpawner = NodePath("../Spawner")
_serviceScope = "GodotHostTest.Game.LevelScope, godot-host-test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
InjectedNodes = [NodePath("../Target"), NodePath("../Target2"), NodePath("../Target3"), NodePath("../Target4"), NodePath("../Target5"), NodePath("../Target6"), NodePath("../Score"), NodePath("../Turret")]
[node name="Target" parent="." unique_id=2101558052 instance=ExtResource("1_y7rhs")]
position = Vector2(198, 143)
TimeOutDurationInSeconds = 2.0
[node name="Target2" parent="." unique_id=1009901052 instance=ExtResource("1_y7rhs")]
position = Vector2(669, 64)
TimeOutDurationInSeconds = 2.0
[node name="Target3" parent="." unique_id=1433382188 instance=ExtResource("1_y7rhs")]
position = Vector2(1060, 263)
TimeOutDurationInSeconds = 2.0
[node name="Target4" parent="." unique_id=1553596281 instance=ExtResource("1_y7rhs")]
position = Vector2(961, 620)
TimeOutDurationInSeconds = 2.0
[node name="Target5" parent="." unique_id=46923989 instance=ExtResource("1_y7rhs")]
position = Vector2(469, 591)
TimeOutDurationInSeconds = 2.0
[node name="Target6" parent="." unique_id=2092179937 instance=ExtResource("1_y7rhs")]
position = Vector2(53, 417)
TimeOutDurationInSeconds = 2.0
[node name="Spawner" type="Node2D" parent="." unique_id=251404460 node_paths=PackedStringArray("Timer")]
script = ExtResource("2_5saw1")
TimeBetweenSpawns = 1.5
Timer = NodePath("Timer")
[node name="Timer" type="Timer" parent="Spawner" unique_id=1066078427]
[node name="Score" type="ColorRect" parent="." unique_id=1267746809 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("3_spj1u")
_animationPlayer = NodePath("../AnimationPlayer")
_label = NodePath("Label")
[node name="Label" type="Label" parent="Score" unique_id=558063655]
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=1215951318]
root_node = NodePath("../..")
libraries/ = SubResource("AnimationLibrary_rckqj")
[node name="Turret" type="Sprite2D" parent="." unique_id=152356196]
modulate = Color(1.92523e-07, 0.88004553, 0.8802661, 1)
position = Vector2(539, 307)
texture = ExtResource("4_esqs7")
script = ExtResource("5_himod")
_projectilePackedScene = ExtResource("6_sin03")
[connection signal="timeout" from="Spawner/Timer" to="Spawner" method="OnTimer"]
+1
View File
@@ -0,0 +1 @@
uid://bahq074tnev8g
+170
View File
@@ -0,0 +1,170 @@
#if TOOLS
using System.Linq;
using System.Reflection;
using Godot;
using Godot.Collections;
namespace GodotHostTest.GodotDI.Editor;
[Tool]
public partial class Plugin : EditorPlugin
{
/// <summary>
/// Amount of debounce before updating the injections
/// </summary>
private const double DebounceDelay = 0.1;
private const BindingFlags InjectBindingFlags =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private ScopeInspectorPlugin? _plugin;
private SceneTreeTimer? _sceneTreeTimer;
private bool _treeChangesRegistered;
public override void _EnterTree()
{
GD.Print("Initializing Godot DI");
// add the inspector plugin to provide a better interface for the "scope" property
_plugin = new ScopeInspectorPlugin();
AddInspectorPlugin(_plugin);
// update injected nodes when the scene changes
SceneChanged += OnSceneChanged;
}
public override void _ExitTree()
{
GD.Print("Deinitializing Godot DI");
SceneChanged -= OnSceneChanged;
UnregisterTreeChanges();
RemoveInspectorPlugin(_plugin);
_plugin?.Dispose();
_plugin = null;
}
private void OnTreeChanged()
{
var root = EditorInterface.Singleton.GetEditedSceneRoot();
OnSceneChanged(root);
}
private void OnSceneChanged(Node? sceneRoot)
{
// register to tree changes, in case the registration wasn't possible before
RegisterTreeChanges();
// don't do anything if there's no scene root
if (sceneRoot == null) return;
// apply debounce
if (_sceneTreeTimer != null)
{
_sceneTreeTimer.SetTimeLeft(DebounceDelay);
return;
}
_sceneTreeTimer =
EditorInterface.Singleton.GetEditedSceneRoot().GetTree().CreateTimer(DebounceDelay);
_sceneTreeTimer.Timeout += () => ActualOnSceneChanged(sceneRoot);
}
private void ActualOnSceneChanged(Node sceneRoot)
{
// mark debounce as completed
_sceneTreeTimer = null;
var sceneNodes = sceneRoot.FindChildren("*");
GD.Print($"Updating injection for root {sceneRoot.Name}");
// look for the service source
var serviceSource = sceneNodes.Where(child => child.IsServiceSource())
.Single(out var failureReason);
switch (failureReason)
{
case EnumerableExtensions.SingleFailureReason.LessThanOne:
return;
case EnumerableExtensions.SingleFailureReason.MoreThanOne:
GD.PushWarning("Found more than one service source owned by the scene.");
return;
default:
{
// find all nodes that need injection
var nodesNeedingInjection = sceneNodes.Append(sceneRoot).Where(node =>
{
var type = node.GetCSharpScriptType();
return type != null && (
type.GetFields(InjectBindingFlags).Any(field =>
field.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0) ||
type.GetMethods(InjectBindingFlags).Any(field =>
field.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0)
);
}).ToList();
// add all nodes needed to the scope
var injectedNodes = serviceSource!.Get(ServiceSource.PropertyName.InjectedNodes).AsGodotArray();
Array? newArray = null;
foreach (var node in nodesNeedingInjection
.Where(node => !injectedNodes.Contains(node)))
{
newArray ??= new Array(injectedNodes);
newArray.Add(node);
GD.Print($"Added node {node.Name} to injection.");
}
// remove all nodes that are no longer needing injection
foreach (var node in injectedNodes
.Select(n => (Node?)n.AsGodotObject())
.Where(node => !nodesNeedingInjection.Contains(node)))
{
if (node == null) continue;
newArray ??= new Array(injectedNodes);
newArray.Remove(node);
GD.Print($"Removed node {node?.Name} from injection.");
}
// apply changes to the scene if necessary
if (newArray != null)
{
GD.Print("Apply changes.");
var undoRedo = GetUndoRedo();
undoRedo.CreateAction("Update injected nodes");
undoRedo.AddDoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes, newArray);
undoRedo.AddUndoProperty(serviceSource, ServiceSource.PropertyName.InjectedNodes, injectedNodes);
undoRedo.CommitAction();
}
break;
}
}
}
/// <summary>
/// Attach to the TreeChanged event of the SceneTree if it's not already registered and if there's a scene tree to
/// attach to from the currently edited scene.
/// </summary>
private void RegisterTreeChanges()
{
if (_treeChangesRegistered) return;
var sceneRoot = EditorInterface.Singleton.GetEditedSceneRoot();
if (sceneRoot == null) return;
var tree = sceneRoot.GetTree();
tree.TreeChanged += OnTreeChanged;
_treeChangesRegistered = true;
}
private void UnregisterTreeChanges()
{
var sceneRoot = EditorInterface.Singleton.GetEditedSceneRoot();
if (!_treeChangesRegistered || sceneRoot == null) return;
var tree = sceneRoot.GetTree();
tree.TreeChanged -= OnTreeChanged;
SceneChanged -= OnSceneChanged;
}
}
#endif
@@ -0,0 +1,30 @@
#if TOOLS
using Godot;
namespace GodotHostTest.GodotDI.Editor;
// it's inside ServiceSource so that it can safely access nameof(_serviceScope)
public partial class ScopeInspectorPlugin : EditorInspectorPlugin
{
public override bool _CanHandle(GodotObject @object)
{
var result = @object.IsServiceSource();
return result;
}
public override bool _ParseProperty(GodotObject @object, Variant.Type type, string name, PropertyHint hintType,
string hintString,
PropertyUsageFlags usageFlags, bool wide)
{
// We handle properties of type integer.
if (name != nameof(ServiceSource.PropertyName._serviceScope)) return false;
// Create an instance of the custom property editor and register
// it to a specific property path.
AddPropertyEditor(name, new ScopePickerEditor());
// Inform the editor to remove the default property editor for
// this property type.
return true;
}
}
#endif
@@ -0,0 +1 @@
uid://ca5k0qqats56p
-26
View File
@@ -1,26 +0,0 @@
#if TOOLS
using Godot;
namespace GodotHostTest.GodotDI;
[Tool]
public partial class GodotDI : EditorPlugin
{
private ServiceSource.ScopeInspectorPlugin? _plugin;
public override void _EnterTree()
{
GD.Print("Initializing Godot DI");
_plugin = new ServiceSource.ScopeInspectorPlugin();
AddInspectorPlugin(_plugin);
}
public override void _ExitTree()
{
GD.Print("Deinitializing Godot DI");
RemoveInspectorPlugin(_plugin);
_plugin?.Dispose();
_plugin = null;
}
}
#endif
-36
View File
@@ -1,36 +0,0 @@
#if TOOLS
using Godot;
using GodotHostTest.addons.godot_di;
namespace GodotHostTest.GodotDI;
public partial class ServiceSource
{
// it's inside ServiceSource so that it can safely access nameof(_serviceScope)
public partial class ScopeInspectorPlugin : EditorInspectorPlugin
{
public override bool _CanHandle(GodotObject @object)
{
var result = @object.GetScript().AsGodotObject() is CSharpScript script &&
GodotScriptPathCache.TryGetTypeFromPath(script.ResourcePath, out var type) &&
type.IsAssignableTo(typeof(ServiceSource));
return result;
}
public override bool _ParseProperty(GodotObject @object, Variant.Type type, string name, PropertyHint hintType,
string hintString,
PropertyUsageFlags usageFlags, bool wide)
{
// We handle properties of type integer.
if (name != nameof(_serviceScope)) return false;
// Create an instance of the custom property editor and register
// it to a specific property path.
AddPropertyEditor(name, new ScopePickerEditor());
// Inform the editor to remove the default property editor for
// this property type.
return true;
}
}
}
#endif