From e560a743fb7c0d61909c6ee880a995b5c6ffd8af Mon Sep 17 00:00:00 2001 From: redglow Date: Sat, 11 Jul 2026 10:54:02 +0200 Subject: [PATCH] feat: initial import --- .editorconfig | 4 + .gitattributes | 2 + .gitignore | 3 + .idea/.idea.godot-host-test/.idea/.gitignore | 13 ++ .../.idea.godot-host-test/.idea/encodings.xml | 4 + .../.idea/indexLayout.xml | 8 + .../.idea/libraries/GdSdk.xml | 9 ++ .idea/.idea.godot-host-test/.idea/vcs.xml | 12 ++ Game/GameRootScope.cs | 18 +++ Game/GameRootScope.cs.uid | 1 + Game/Projectile.cs | 21 +++ Game/Projectile.cs.uid | 1 + Game/Score.cs | 37 +++++ Game/Score.cs.uid | 1 + Game/Target.cs | 61 ++++++++ Game/Target.cs.uid | 1 + Game/TargetSpawner.cs | 61 ++++++++ Game/TargetSpawner.cs.uid | 1 + Game/Turret.cs | 15 ++ Game/Turret.cs.uid | 1 + Game/projectile.tscn | 18 +++ Game/target.tscn | 31 ++++ GodotDI/ISceneInstantiator.cs | 20 +++ GodotDI/ISceneInstantiator.cs.uid | 1 + GodotDI/InjectAttribute.cs | 6 + GodotDI/InjectAttribute.cs.uid | 1 + GodotDI/RootScope.cs | 77 ++++++++++ GodotDI/RootScope.cs.uid | 1 + GodotDI/Scope.cs | 29 ++++ GodotDI/Scope.cs.uid | 1 + Helpers/NodeExtensions.cs | 19 +++ Helpers/NodeExtensions.cs.uid | 1 + Helpers/ObservableHelpers.cs | 19 +++ Helpers/ObservableHelpers.cs.uid | 1 + Helpers/Pipe.cs | 27 ++++ Helpers/Pipe.cs.uid | 1 + Interfaces/IScore.cs | 22 +++ Interfaces/IScore.cs.uid | 1 + Interfaces/ITarget.cs | 24 +++ Interfaces/ITarget.cs.uid | 1 + Interfaces/ITargetCollector.cs | 13 ++ Interfaces/ITargetCollector.cs.uid | 1 + Interfaces/ITargetSpawner.cs | 14 ++ Interfaces/ITargetSpawner.cs.uid | 1 + PrintOrder.cs | 31 ++++ PrintOrder.cs.uid | 1 + TestOrder.tscn | 79 ++++++++++ global.json | 7 + godot-host-test.csproj | 28 ++++ godot-host-test.sln | 19 +++ godot-host-test.sln.DotSettings.user | 5 + icon.svg | 1 + icon.svg.import | 43 ++++++ project.godot | 33 ++++ root_scene.tscn | 142 ++++++++++++++++++ 55 files changed, 993 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .idea/.idea.godot-host-test/.idea/.gitignore create mode 100644 .idea/.idea.godot-host-test/.idea/encodings.xml create mode 100644 .idea/.idea.godot-host-test/.idea/indexLayout.xml create mode 100644 .idea/.idea.godot-host-test/.idea/libraries/GdSdk.xml create mode 100644 .idea/.idea.godot-host-test/.idea/vcs.xml create mode 100644 Game/GameRootScope.cs create mode 100644 Game/GameRootScope.cs.uid create mode 100644 Game/Projectile.cs create mode 100644 Game/Projectile.cs.uid create mode 100644 Game/Score.cs create mode 100644 Game/Score.cs.uid create mode 100644 Game/Target.cs create mode 100644 Game/Target.cs.uid create mode 100644 Game/TargetSpawner.cs create mode 100644 Game/TargetSpawner.cs.uid create mode 100644 Game/Turret.cs create mode 100644 Game/Turret.cs.uid create mode 100644 Game/projectile.tscn create mode 100644 Game/target.tscn create mode 100644 GodotDI/ISceneInstantiator.cs create mode 100644 GodotDI/ISceneInstantiator.cs.uid create mode 100644 GodotDI/InjectAttribute.cs create mode 100644 GodotDI/InjectAttribute.cs.uid create mode 100644 GodotDI/RootScope.cs create mode 100644 GodotDI/RootScope.cs.uid create mode 100644 GodotDI/Scope.cs create mode 100644 GodotDI/Scope.cs.uid create mode 100644 Helpers/NodeExtensions.cs create mode 100644 Helpers/NodeExtensions.cs.uid create mode 100644 Helpers/ObservableHelpers.cs create mode 100644 Helpers/ObservableHelpers.cs.uid create mode 100644 Helpers/Pipe.cs create mode 100644 Helpers/Pipe.cs.uid create mode 100644 Interfaces/IScore.cs create mode 100644 Interfaces/IScore.cs.uid create mode 100644 Interfaces/ITarget.cs create mode 100644 Interfaces/ITarget.cs.uid create mode 100644 Interfaces/ITargetCollector.cs create mode 100644 Interfaces/ITargetCollector.cs.uid create mode 100644 Interfaces/ITargetSpawner.cs create mode 100644 Interfaces/ITargetSpawner.cs.uid create mode 100644 PrintOrder.cs create mode 100644 PrintOrder.cs.uid create mode 100644 TestOrder.tscn create mode 100644 global.json create mode 100644 godot-host-test.csproj create mode 100644 godot-host-test.sln create mode 100644 godot-host-test.sln.DotSettings.user create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 project.godot create mode 100644 root_scene.tscn diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcc84f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ \ No newline at end of file diff --git a/.idea/.idea.godot-host-test/.idea/.gitignore b/.idea/.idea.godot-host-test/.idea/.gitignore new file mode 100644 index 0000000..b6d58ee --- /dev/null +++ b/.idea/.idea.godot-host-test/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/.idea.godot-host-test.iml +/projectSettingsUpdater.xml +/contentModel.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.godot-host-test/.idea/encodings.xml b/.idea/.idea.godot-host-test/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.godot-host-test/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.godot-host-test/.idea/indexLayout.xml b/.idea/.idea.godot-host-test/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.godot-host-test/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.godot-host-test/.idea/libraries/GdSdk.xml b/.idea/.idea.godot-host-test/.idea/libraries/GdSdk.xml new file mode 100644 index 0000000..0059883 --- /dev/null +++ b/.idea/.idea.godot-host-test/.idea/libraries/GdSdk.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.godot-host-test/.idea/vcs.xml b/.idea/.idea.godot-host-test/.idea/vcs.xml new file mode 100644 index 0000000..4c6280e --- /dev/null +++ b/.idea/.idea.godot-host-test/.idea/vcs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Game/GameRootScope.cs b/Game/GameRootScope.cs new file mode 100644 index 0000000..a61378b --- /dev/null +++ b/Game/GameRootScope.cs @@ -0,0 +1,18 @@ +using Godot; +using GodotHostTest.GodotDI; +using GodotHostTest.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace GodotHostTest.Game; + +public partial class GameRootScope : RootScope +{ + [Export] private TargetSpawner _targetSpawner = null!; + + protected override void Configure(IServiceCollection serviceCollection) + { + serviceCollection + .AddSingleton(_targetSpawner) + .AddSingleton(_targetSpawner); + } +} \ No newline at end of file diff --git a/Game/GameRootScope.cs.uid b/Game/GameRootScope.cs.uid new file mode 100644 index 0000000..a370e6f --- /dev/null +++ b/Game/GameRootScope.cs.uid @@ -0,0 +1 @@ +uid://dk578xnlxjwrr diff --git a/Game/Projectile.cs b/Game/Projectile.cs new file mode 100644 index 0000000..d2315b4 --- /dev/null +++ b/Game/Projectile.cs @@ -0,0 +1,21 @@ +using Godot; +using GodotHostTest.GodotDI; +using GodotHostTest.Helpers; +using GodotHostTest.Interfaces; +using Microsoft.Extensions.Logging; +using R3; + +namespace GodotHostTest.Game; + +public partial class Projectile : Node2D +{ + [Inject] private readonly ILogger _logger = null!; + [Inject] private readonly ITargetSpawner _targetSpawner = null!; + + // Called when the node enters the scene tree for the first time. + public override void _Ready() + { + _targetSpawner.TargetSpawned + .Subscribe(_ => _logger.LogInformation("Projectile got informed of target spawning")).AddTo(this); + } +} \ No newline at end of file diff --git a/Game/Projectile.cs.uid b/Game/Projectile.cs.uid new file mode 100644 index 0000000..998f870 --- /dev/null +++ b/Game/Projectile.cs.uid @@ -0,0 +1 @@ +uid://dc5mvle8lc40j diff --git a/Game/Score.cs b/Game/Score.cs new file mode 100644 index 0000000..adff9b6 --- /dev/null +++ b/Game/Score.cs @@ -0,0 +1,37 @@ +using R3; +using Godot; +using GodotHostTest.GodotDI; +using GodotHostTest.Helpers; +using GodotHostTest.Interfaces; + +namespace GodotHostTest.Game; + +public partial class Score : Control +{ + [Export] private AnimationPlayer _animationPlayer = null!; + [Export] private Label _label = null!; + [Inject] private readonly ITargetSpawner _targetSpawner = null!; + + private int _score; + + public override void _Ready() + { + _targetSpawner.TargetSpawned.Subscribe(OnTargetSpawned).AddTo(this); + base._Ready(); + } + + private void OnTargetSpawned(ITarget target) + { + _animationPlayer.Play("wiggle"); + target.Hit.Select(_ => 20).Merge(target.TimedOut.Select(_ => -5)).SubscribeOnce(delta => + { + _score += delta; + UpdateScore(); + }).AddTo(this); + } + + private void UpdateScore() + { + _label.Text = $"Score: {_score}"; + } +} \ No newline at end of file diff --git a/Game/Score.cs.uid b/Game/Score.cs.uid new file mode 100644 index 0000000..62efd91 --- /dev/null +++ b/Game/Score.cs.uid @@ -0,0 +1 @@ +uid://bgrjeadxhgmvi diff --git a/Game/Target.cs b/Game/Target.cs new file mode 100644 index 0000000..7cecfa8 --- /dev/null +++ b/Game/Target.cs @@ -0,0 +1,61 @@ +using Godot; +using GodotHostTest.GodotDI; +using GodotHostTest.Interfaces; +using R3; + +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!; + + public override void _Ready() + { + _targetCollector.NewTarget(this); + } + + public override void _ExitTree() + { + _timedOut.Dispose(); + _hit.Dispose(); + } + + public Observable TimedOut => _timedOut.AsObservable(); + + public Observable Hit => _hit.AsObservable(); + + // todo: hook to collision + private void OnHit() + { + Disable(); + TimeOutTimer.Stop(); + _hit.OnNext(Unit.Default); + } + + public void Enable() + { + Image.Visible = true; + CollisionArea.Monitoring = true; + TimeOutTimer.Start(TimeOutDurationInSeconds); + } + + private void Disable() + { + Image.Visible = false; + CollisionArea.Monitoring = false; + } + + private void OnTimeout() + { + Disable(); + _timedOut.OnNext(Unit.Default); + } +} \ No newline at end of file diff --git a/Game/Target.cs.uid b/Game/Target.cs.uid new file mode 100644 index 0000000..2dfee40 --- /dev/null +++ b/Game/Target.cs.uid @@ -0,0 +1 @@ +uid://bv2hat41dyxiv diff --git a/Game/TargetSpawner.cs b/Game/TargetSpawner.cs new file mode 100644 index 0000000..71f9ce9 --- /dev/null +++ b/Game/TargetSpawner.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Godot; +using GodotHostTest.Helpers; +using GodotHostTest.Interfaces; +using R3; + +namespace GodotHostTest.Game; + +public partial class TargetSpawner : Node2D, ITargetCollector, ITargetSpawner +{ + [Export] public double TimeBetweenSpawns; + + [Export] public required Timer Timer; + + private readonly List _availableTargets = []; + + public override void _Ready() + { + Spawn(); + Timer.Start(TimeBetweenSpawns); + } + + private void MakeTargetAvailable(ITarget target) + { + _availableTargets.Add(target); + } + + private void OnTimer() + { + Spawn(); + } + + private void Spawn() + { + // pick a random target + var randomIndex = GD.RandRange(0, _availableTargets.Count - 1); + var target = _availableTargets[randomIndex]; + _availableTargets.RemoveAt(randomIndex); + + // enable it + target.Enable(); + + // notify that a target has been spawned + _targetSpawned.OnNext(target); + } + + public void NewTarget(ITarget target) + { + _availableTargets.Add(target); + target.Hit.Merge(target.TimedOut).Select(_ => target).Subscribe(MakeTargetAvailable).AddTo(this); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _targetSpawned.Dispose(); + } + + private readonly Subject _targetSpawned = new(); + public Observable TargetSpawned => _targetSpawned.AsObservable(); +} \ No newline at end of file diff --git a/Game/TargetSpawner.cs.uid b/Game/TargetSpawner.cs.uid new file mode 100644 index 0000000..7cfa927 --- /dev/null +++ b/Game/TargetSpawner.cs.uid @@ -0,0 +1 @@ +uid://cxfrtglku2gfa diff --git a/Game/Turret.cs b/Game/Turret.cs new file mode 100644 index 0000000..370af4d --- /dev/null +++ b/Game/Turret.cs @@ -0,0 +1,15 @@ +using Godot; +using GodotHostTest.GodotDI; + +namespace GodotHostTest.Game; + +public partial class Turret : Sprite2D +{ + [Inject] private readonly ISceneInstantiator _sceneInstantiator = null!; + [Export] private PackedScene _projectilePackedScene = null!; + + public override void _Ready() + { + GetTree().CreateTimer(1).Timeout += () => AddChild(_sceneInstantiator.Instantiate(_projectilePackedScene)); + } +} \ No newline at end of file diff --git a/Game/Turret.cs.uid b/Game/Turret.cs.uid new file mode 100644 index 0000000..cb7fd66 --- /dev/null +++ b/Game/Turret.cs.uid @@ -0,0 +1 @@ +uid://ce7gnp1lcrrdv diff --git a/Game/projectile.tscn b/Game/projectile.tscn new file mode 100644 index 0000000..38959d4 --- /dev/null +++ b/Game/projectile.tscn @@ -0,0 +1,18 @@ +[gd_scene format=3 uid="uid://brbufibn7hi3o"] + +[ext_resource type="Script" uid="uid://dc5mvle8lc40j" path="res://Game/Projectile.cs" id="1_80yud"] +[ext_resource type="Script" uid="uid://qjonl8stia46" path="res://GodotDI/Scope.cs" id="2_3kmdq"] +[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="3_1i54g"] + +[node name="Projectile" type="Node2D" unique_id=1686616504] +script = ExtResource("1_80yud") + +[node name="Scope" type="Node" parent="." unique_id=513224818 node_paths=PackedStringArray("InjectedNodes")] +script = ExtResource("2_3kmdq") +InjectedNodes = [NodePath("..")] + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1021740171] +modulate = Color(1, 1, 0, 1) +rotation = 0.7853982 +scale = Vector2(0.3, 0.3) +texture = ExtResource("3_1i54g") diff --git a/Game/target.tscn b/Game/target.tscn new file mode 100644 index 0000000..c3f9212 --- /dev/null +++ b/Game/target.tscn @@ -0,0 +1,31 @@ +[gd_scene format=3 uid="uid://3v8kq1rfwhmr"] + +[ext_resource type="Texture2D" uid="uid://y1ros3y7hag2" path="res://icon.svg" id="1_032qf"] +[ext_resource type="Script" uid="uid://bv2hat41dyxiv" path="res://Game/Target.cs" id="1_c0a8e"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_u8d0v"] +size = Vector2(128, 128) + +[node name="Target" type="Node2D" unique_id=2101558052 node_paths=PackedStringArray("TimeOutTimer", "Image", "CollisionArea")] +script = ExtResource("1_c0a8e") +TimeOutDurationInSeconds = 7.0 +TimeOutTimer = NodePath("TimeOutTimer") +Image = NodePath("Sprite2D") +CollisionArea = NodePath("CollisionArea") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=31059524] +visible = false +modulate = Color(1, 0.20392157, 0.015686275, 1) +texture = ExtResource("1_032qf") + +[node name="CollisionArea" type="Area2D" parent="." unique_id=116681968] +monitoring = false +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="CollisionArea" unique_id=1558252366] +shape = SubResource("RectangleShape2D_u8d0v") + +[node name="TimeOutTimer" type="Timer" parent="." unique_id=1079777109] +one_shot = true + +[connection signal="timeout" from="TimeOutTimer" to="." method="OnTimeout"] diff --git a/GodotDI/ISceneInstantiator.cs b/GodotDI/ISceneInstantiator.cs new file mode 100644 index 0000000..972e046 --- /dev/null +++ b/GodotDI/ISceneInstantiator.cs @@ -0,0 +1,20 @@ +using Godot; + +namespace GodotHostTest.GodotDI; + +public interface ISceneInstantiator +{ + /// + /// 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. + /// The created node, with its dependencies satisfied. + Node Instantiate(PackedScene packedScene); + + /// + /// 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. + /// The created node, with its dependencies satisfied. + T Instantiate(PackedScene packedScene) where T : Node; +} \ No newline at end of file diff --git a/GodotDI/ISceneInstantiator.cs.uid b/GodotDI/ISceneInstantiator.cs.uid new file mode 100644 index 0000000..ffc838f --- /dev/null +++ b/GodotDI/ISceneInstantiator.cs.uid @@ -0,0 +1 @@ +uid://bv7aut87rpm5u diff --git a/GodotDI/InjectAttribute.cs b/GodotDI/InjectAttribute.cs new file mode 100644 index 0000000..6d3c159 --- /dev/null +++ b/GodotDI/InjectAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace GodotHostTest.GodotDI; + +[AttributeUsage(AttributeTargets.Field)] +public class InjectAttribute: Attribute; \ No newline at end of file diff --git a/GodotDI/InjectAttribute.cs.uid b/GodotDI/InjectAttribute.cs.uid new file mode 100644 index 0000000..d63b7fb --- /dev/null +++ b/GodotDI/InjectAttribute.cs.uid @@ -0,0 +1 @@ +uid://mshffj1jlgi7 diff --git a/GodotDI/RootScope.cs b/GodotDI/RootScope.cs new file mode 100644 index 0000000..3aef231 --- /dev/null +++ b/GodotDI/RootScope.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Godot; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OwofGames.GodotHost; + +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 RootScope : Scope, ISceneInstantiator +{ + private Host? _host; + + private Host Host + { + get => _host ?? + throw new InvalidOperationException( + "Some method of RootScope has been invoked before it entered the tree"); + set => _host = value; + } + + private ILogger? _logger; + + private ILogger Logger => _logger ??= Host.GetLogger(); + + public override void _EnterTree() + { + // create the host + Host = new HostBuilder() + .SetServiceCollectionBuilder(InnerConfigure) + .Build(); + + // resolve nodes + ResolveInjectedNodes(Host.ServiceProvider); + } + + private void InnerConfigure(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(this); + Configure(serviceCollection); + } + + /// + /// Configure the service collection by adding the registrations your game needs. + /// + /// The service collection to enrich. + protected abstract void Configure(IServiceCollection serviceCollection); + + /// + public Node Instantiate(PackedScene packedScene) + { + 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); + } + + return node; + } + + /// + public T Instantiate(PackedScene packedScene) + where T : Node + { + return (T)Instantiate(packedScene); + } +} \ No newline at end of file diff --git a/GodotDI/RootScope.cs.uid b/GodotDI/RootScope.cs.uid new file mode 100644 index 0000000..9713826 --- /dev/null +++ b/GodotDI/RootScope.cs.uid @@ -0,0 +1 @@ +uid://fr0c6wwras8c diff --git a/GodotDI/Scope.cs b/GodotDI/Scope.cs new file mode 100644 index 0000000..102f460 --- /dev/null +++ b/GodotDI/Scope.cs @@ -0,0 +1,29 @@ +using System; +using System.Reflection; +using Godot; +using Microsoft.Extensions.DependencyInjection; + +namespace GodotHostTest.GodotDI; + +public partial class Scope : Node +{ + [Export] protected Node?[] InjectedNodes = []; + + internal void ResolveInjectedNodes(IServiceProvider serviceProvider) + { + // resolve the injected nodes + // TODO: move to source generator, make it the only way for injection + foreach (var node in InjectedNodes) + { + if (node == null) continue; + foreach (var field in node.GetType() + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (field.GetCustomAttribute() == null) continue; + var fieldType = field.FieldType; + var service = serviceProvider.GetRequiredService(fieldType); + field.SetValue(node, service); + } + } + } +} \ No newline at end of file diff --git a/GodotDI/Scope.cs.uid b/GodotDI/Scope.cs.uid new file mode 100644 index 0000000..9c7953f --- /dev/null +++ b/GodotDI/Scope.cs.uid @@ -0,0 +1 @@ +uid://qjonl8stia46 diff --git a/Helpers/NodeExtensions.cs b/Helpers/NodeExtensions.cs new file mode 100644 index 0000000..068cd89 --- /dev/null +++ b/Helpers/NodeExtensions.cs @@ -0,0 +1,19 @@ +using System; +using Godot; + +namespace GodotHostTest.Helpers; + +public static class NodeExtensions +{ + public static void AddTo(this IDisposable disposable, Node node) + { + node.TreeExited += OnTreeExited; + return; + + void OnTreeExited() + { + disposable.Dispose(); + node.TreeExited -= OnTreeExited; + } + } +} \ No newline at end of file diff --git a/Helpers/NodeExtensions.cs.uid b/Helpers/NodeExtensions.cs.uid new file mode 100644 index 0000000..f70b80a --- /dev/null +++ b/Helpers/NodeExtensions.cs.uid @@ -0,0 +1 @@ +uid://lbkratfmlq5e diff --git a/Helpers/ObservableHelpers.cs b/Helpers/ObservableHelpers.cs new file mode 100644 index 0000000..440e9b1 --- /dev/null +++ b/Helpers/ObservableHelpers.cs @@ -0,0 +1,19 @@ +using System; +using R3; + +namespace GodotHostTest.Helpers; + +public static class ObservableHelpers +{ + public static IDisposable SubscribeOnce(this Observable observable, Action action) + { + IDisposable? disposable = null; + disposable = observable.Subscribe(x => + { + // ReSharper disable once AccessToModifiedClosure + disposable?.Dispose(); + action(x); + }); + return disposable; + } +} \ No newline at end of file diff --git a/Helpers/ObservableHelpers.cs.uid b/Helpers/ObservableHelpers.cs.uid new file mode 100644 index 0000000..435c214 --- /dev/null +++ b/Helpers/ObservableHelpers.cs.uid @@ -0,0 +1 @@ +uid://kfb8ooqmhoh0 diff --git a/Helpers/Pipe.cs b/Helpers/Pipe.cs new file mode 100644 index 0000000..7534c89 --- /dev/null +++ b/Helpers/Pipe.cs @@ -0,0 +1,27 @@ +using System; +using R3; + +namespace GodotHostTest.Helpers; + +public class Pipe : IDisposable +{ + private readonly ReplaySubject> _subject = new(1); + + public Pipe() + { + Observable = _subject.Switch(); + } + + public Observable Observable { get; } + + public void Dispose() + { + _subject.Dispose(); + GC.SuppressFinalize(this); + } + + public void PipeIn(Observable observable) + { + _subject.OnNext(observable); + } +} \ No newline at end of file diff --git a/Helpers/Pipe.cs.uid b/Helpers/Pipe.cs.uid new file mode 100644 index 0000000..c963e22 --- /dev/null +++ b/Helpers/Pipe.cs.uid @@ -0,0 +1 @@ +uid://crh20vve83rew diff --git a/Interfaces/IScore.cs b/Interfaces/IScore.cs new file mode 100644 index 0000000..0883176 --- /dev/null +++ b/Interfaces/IScore.cs @@ -0,0 +1,22 @@ +namespace GodotHostTest.Interfaces; + +/// +/// An object that keeps track of the score. +/// +public interface IScore +{ + /// + /// A target has been hit. + /// + void TargetHit(); + + /// + /// A target has timed out. + /// + void TargetTimedOut(); + + /// + /// The current score. + /// + public int Score { get; } +} \ No newline at end of file diff --git a/Interfaces/IScore.cs.uid b/Interfaces/IScore.cs.uid new file mode 100644 index 0000000..3e4be78 --- /dev/null +++ b/Interfaces/IScore.cs.uid @@ -0,0 +1 @@ +uid://wf8obkyi1rod diff --git a/Interfaces/ITarget.cs b/Interfaces/ITarget.cs new file mode 100644 index 0000000..7c94547 --- /dev/null +++ b/Interfaces/ITarget.cs @@ -0,0 +1,24 @@ +using R3; + +namespace GodotHostTest.Interfaces; + +/// +/// A target for our projectiles. +/// +public interface ITarget +{ + /// + /// The target hasn't been hit in the allotted time and has timed out. + /// + Observable TimedOut { get; } + + /// + /// The target has been hit. + /// + Observable Hit { get; } + + /// + /// Enable the target. Targets start disabled and gets disabled when they're hit or time out. + /// + void Enable(); +} \ No newline at end of file diff --git a/Interfaces/ITarget.cs.uid b/Interfaces/ITarget.cs.uid new file mode 100644 index 0000000..4ec6c83 --- /dev/null +++ b/Interfaces/ITarget.cs.uid @@ -0,0 +1 @@ +uid://bgkbpxwire4qb diff --git a/Interfaces/ITargetCollector.cs b/Interfaces/ITargetCollector.cs new file mode 100644 index 0000000..03f7743 --- /dev/null +++ b/Interfaces/ITargetCollector.cs @@ -0,0 +1,13 @@ +namespace GodotHostTest.Interfaces; + +/// +/// An object which collects targets as soon as they are created. +/// +public interface ITargetCollector +{ + /// + /// Register a target. + /// + /// The new target. + void NewTarget(ITarget target); +} \ No newline at end of file diff --git a/Interfaces/ITargetCollector.cs.uid b/Interfaces/ITargetCollector.cs.uid new file mode 100644 index 0000000..ebde607 --- /dev/null +++ b/Interfaces/ITargetCollector.cs.uid @@ -0,0 +1 @@ +uid://cyuavtr6x72uj diff --git a/Interfaces/ITargetSpawner.cs b/Interfaces/ITargetSpawner.cs new file mode 100644 index 0000000..cd7bf8e --- /dev/null +++ b/Interfaces/ITargetSpawner.cs @@ -0,0 +1,14 @@ +using R3; + +namespace GodotHostTest.Interfaces; + +/// +/// An object that spawns targets. +/// +public interface ITargetSpawner +{ + /// + /// Observable that produces a value whenever a target gets spawned. + /// + Observable TargetSpawned { get; } +} \ No newline at end of file diff --git a/Interfaces/ITargetSpawner.cs.uid b/Interfaces/ITargetSpawner.cs.uid new file mode 100644 index 0000000..962c1c4 --- /dev/null +++ b/Interfaces/ITargetSpawner.cs.uid @@ -0,0 +1 @@ +uid://b8ksms66cbg3 diff --git a/PrintOrder.cs b/PrintOrder.cs new file mode 100644 index 0000000..ec1ba74 --- /dev/null +++ b/PrintOrder.cs @@ -0,0 +1,31 @@ +using Godot; + +namespace GodotHostTest; + +public partial class PrintOrder : Node +{ + [Export] private Node? _otherNode; + + public PrintOrder() + { + PrintStatus("constructor"); + } + + public override void _EnterTree() + { + PrintStatus("_enter_tree"); + } + + public override void _Ready() + { + PrintStatus("_ready"); + } + + private void PrintStatus(string moment) + { + var hasExport = _otherNode != null; + var hasParent = GetParent() != null; + var numChildren = GetChildren().Count - 1; // remove the non-script child + GD.Print($"[{moment,11}] Child {Name,-8} Has export? {hasExport} Has parent? {hasParent} Script children? {numChildren}"); + } +} \ No newline at end of file diff --git a/PrintOrder.cs.uid b/PrintOrder.cs.uid new file mode 100644 index 0000000..4154a3f --- /dev/null +++ b/PrintOrder.cs.uid @@ -0,0 +1 @@ +uid://c30r02q21wx33 diff --git a/TestOrder.tscn b/TestOrder.tscn new file mode 100644 index 0000000..718eef9 --- /dev/null +++ b/TestOrder.tscn @@ -0,0 +1,79 @@ +[gd_scene format=3 uid="uid://cfobsqrtg2n2a"] + +[ext_resource type="Script" uid="uid://c30r02q21wx33" path="res://PrintOrder.cs" id="1_f88u3"] + +[node name="TestOrder" type="Node2D" unique_id=2018587797] + +[node name="Root1" type="Node2D" parent="." unique_id=1113529794 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../ImportedNode") + +[node name="Child" type="Node" parent="Root1" unique_id=1210473518] + +[node name="Child1" type="Node2D" parent="Root1" unique_id=1613779589 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../ImportedNode") + +[node name="Child" type="Node" parent="Root1/Child1" unique_id=87552907] + +[node name="Descendant1" type="Node2D" parent="Root1/Child1" unique_id=669587585 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../../ImportedNode") + +[node name="Child" type="Node" parent="Root1/Child1/Descendant1" unique_id=12685308] + +[node name="Descendant2" type="Node2D" parent="Root1/Child1" unique_id=109086010 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../../ImportedNode") + +[node name="Child" type="Node" parent="Root1/Child1/Descendant2" unique_id=1161770344] + +[node name="Child2" type="Node2D" parent="Root1" unique_id=806378838 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../ImportedNode") + +[node name="Child" type="Node" parent="Root1/Child2" unique_id=1260588174] + +[node name="Child3" type="Node2D" parent="Root1" unique_id=2084092809 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../ImportedNode") + +[node name="Child" type="Node" parent="Root1/Child3" unique_id=1306245672] + +[node name="Root2" type="Node2D" parent="." unique_id=1070917045 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../ImportedNode") + +[node name="Child" type="Node" parent="Root2" unique_id=564938069] + +[node name="Child1" type="Node2D" parent="Root2" unique_id=1331804694 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../ImportedNode") + +[node name="Child" type="Node" parent="Root2/Child1" unique_id=358417729] + +[node name="Descendant1" type="Node2D" parent="Root2/Child1" unique_id=765114105 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../../ImportedNode") + +[node name="Child" type="Node" parent="Root2/Child1/Descendant1" unique_id=119960501] + +[node name="Descendant2" type="Node2D" parent="Root2/Child1" unique_id=1849254856 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../../ImportedNode") + +[node name="Child" type="Node" parent="Root2/Child1/Descendant2" unique_id=439293261] + +[node name="Child2" type="Node2D" parent="Root2" unique_id=690813149 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../ImportedNode") + +[node name="Child" type="Node" parent="Root2/Child2" unique_id=1594270861] + +[node name="Child3" type="Node2D" parent="Root2" unique_id=497541522 node_paths=PackedStringArray("_otherNode")] +script = ExtResource("1_f88u3") +_otherNode = NodePath("../../ImportedNode") + +[node name="Child" type="Node" parent="Root2/Child3" unique_id=1948966308] + +[node name="ImportedNode" type="Node" parent="." unique_id=2077860292] diff --git a/global.json b/global.json new file mode 100644 index 0000000..f4fd385 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/godot-host-test.csproj b/godot-host-test.csproj new file mode 100644 index 0000000..8c0ba22 --- /dev/null +++ b/godot-host-test.csproj @@ -0,0 +1,28 @@ + + + net8.0 + net9.0 + true + GodotHostTest + enable + + + + + + + ..\OwofGames.Godot\OwofGames.GodotHost\bin\Debug\net8.0\OwofGames.GodotHost.dll + + + ..\OwofGames.Godot\OwofGames.GodotHost\bin\Debug\net8.0\OwofGames.GodotLogger.dll + + + + + + + + + + + \ No newline at end of file diff --git a/godot-host-test.sln b/godot-host-test.sln new file mode 100644 index 0000000..7888ff9 --- /dev/null +++ b/godot-host-test.sln @@ -0,0 +1,19 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "godot-host-test", "godot-host-test.csproj", "{ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + ExportDebug|Any CPU = ExportDebug|Any CPU + ExportRelease|Any CPU = ExportRelease|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU + {ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU + {ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU + {ACB2B98F-A7C5-4486-BC24-2B85B7990C5B}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU + EndGlobalSection +EndGlobal diff --git a/godot-host-test.sln.DotSettings.user b/godot-host-test.sln.DotSettings.user new file mode 100644 index 0000000..404c6c6 --- /dev/null +++ b/godot-host-test.sln.DotSettings.user @@ -0,0 +1,5 @@ + + True + True + /usr/lib/dotnet/dotnet + /usr/lib/dotnet/sdk/10.0.109/MSBuild.dll \ No newline at end of file diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..6669d72 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://y1ros3y7hag2" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..2a29ae4 --- /dev/null +++ b/project.godot @@ -0,0 +1,33 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="godot-host-test" +run/main_scene="uid://vsburk37p4u" +config/features=PackedStringArray("4.7", "C#", "Forward Plus") +config/icon="res://icon.svg" + +[display] + +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[dotnet] + +project/assembly_name="godot-host-test" + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" diff --git a/root_scene.tscn b/root_scene.tscn new file mode 100644 index 0000000..ddd7f09 --- /dev/null +++ b/root_scene.tscn @@ -0,0 +1,142 @@ +[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://cxfrtglku2gfa" path="res://Game/TargetSpawner.cs" id="3_iqet6"] +[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="RootScope" type="Node" parent="." unique_id=1453141084 node_paths=PackedStringArray("_targetSpawner", "InjectedNodes")] +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"]