From 404e26f5edf0f26e18b940355f81fc8c7546d4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henri=20Hyyryl=C3=A4inen?= Date: Sat, 20 Nov 2021 13:57:50 +0200 Subject: [PATCH] DamageNumbers is now implemented --- DamageNumbers/Damage Numbers.csproj | 1 + DamageNumbers/Damage Numbers.sln.DotSettings | 2 + DamageNumbers/DamageNumbers/DamageNumbers.cs | 38 ++++- .../DamageNumbers/FloatingDamageNumbers.cs | 130 +++++++++++++++++- 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 DamageNumbers/Damage Numbers.sln.DotSettings diff --git a/DamageNumbers/Damage Numbers.csproj b/DamageNumbers/Damage Numbers.csproj index 1fa2a37..ea437e4 100644 --- a/DamageNumbers/Damage Numbers.csproj +++ b/DamageNumbers/Damage Numbers.csproj @@ -2,6 +2,7 @@ net472 DamageNumbers + 9.0 diff --git a/DamageNumbers/Damage Numbers.sln.DotSettings b/DamageNumbers/Damage Numbers.sln.DotSettings new file mode 100644 index 0000000..d872259 --- /dev/null +++ b/DamageNumbers/Damage Numbers.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/DamageNumbers/DamageNumbers/DamageNumbers.cs b/DamageNumbers/DamageNumbers/DamageNumbers.cs index d26b7f4..3f5a8fd 100644 --- a/DamageNumbers/DamageNumbers/DamageNumbers.cs +++ b/DamageNumbers/DamageNumbers/DamageNumbers.cs @@ -7,6 +7,8 @@ public class DamageNumbers : IMod { private FloatingDamageNumbers damageNumbers; + private IModInterface storedInterface; + /// /// Called when the mod is being loaded /// @@ -19,8 +21,15 @@ public bool Initialize(IModInterface modInterface, ModInfo currentModInfo) { GD.Print("DamageNumbers mod is initializing"); + // Store the mod interface for use later + storedInterface = modInterface; + + // Setup our GUI control damageNumbers = new FloatingDamageNumbers(); + // Subscribe to the events we are interested in + modInterface.OnDamageReceived += OnDamageReceived; + // Success return true; } @@ -33,6 +42,17 @@ public bool Unload() { GD.Print("DamageNumbers mod is unloading"); + // Remember to unsubscribe from all the events we subscribed to in Initialize, + // otherwise mod unloading won't work correctly + storedInterface.OnDamageReceived -= OnDamageReceived; + + // And release our mod interface reference + storedInterface = null; + + // Release our other resources we created + damageNumbers.QueueFree(); + damageNumbers = null; + // Success return true; } @@ -40,11 +60,25 @@ public bool Unload() /// /// Called once initial node setup has finished and it is possible to add children to the root node /// - /// The scene we want to attach to, could also get these from the mod interface + /// + /// The scene we want might want to attach to, could also get these from the mod interface + /// + /// + /// + /// As this mod wants to be always active we directly attach to the scene tree root to stay attached even when + /// game scenes are changed. + /// + /// public void CanAttachNodes(Node currentScene) { GD.Print("DamageNumbers mod is attaching nodes"); - currentScene.GetParent().AddChild(damageNumbers); + currentScene.GetTree().Root.AddChild(damageNumbers); + } + + private void OnDamageReceived(Node damageReceiver, float amount, bool isPlayer) + { + if (damageReceiver is Spatial spatial) + damageNumbers.AddNumber(amount, spatial.GlobalTransform.origin); } } diff --git a/DamageNumbers/DamageNumbers/FloatingDamageNumbers.cs b/DamageNumbers/DamageNumbers/FloatingDamageNumbers.cs index ab7fdcd..b1f1eff 100644 --- a/DamageNumbers/DamageNumbers/FloatingDamageNumbers.cs +++ b/DamageNumbers/DamageNumbers/FloatingDamageNumbers.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using Godot; /// @@ -11,19 +15,141 @@ /// public class FloatingDamageNumbers : Control { + private const float NumberLifespan = 1.5f; + + private static readonly Vector3 NumberVelocity = new(0, 10.0f, -0.5f); + private static readonly Vector3 VelocityRandomNess = new(0.05f, 5.0f, -1.0f); + + private readonly List activeNumbers = new(); + private readonly Random random = new(); + + private Camera camera; + public override void _Ready() { GD.Print("FloatingDamageNumbers class loaded"); + // Fullscreen + AnchorRight = 1; + AnchorBottom = 1; + + // But don't block mouse input + MouseFilter = MouseFilterEnum.Ignore; + + // For more easily seeing this Node in the Godot debugger we give us a name (needs to be unique in the parent + // node we attach to) + Name = nameof(FloatingDamageNumbers); + } + + public override void _Process(float delta) + { + // When cameras are detached from the main scene they don't have Current set to false even if + // they aren't active + if (camera == null || (!camera.Current || !camera.IsInsideTree())) + { + TryGetCamera(); + + // No camera, we can't do anything + // TODO: would be good to still remove the existing numbers + if (camera == null) + return; + + GD.Print("DamageNumbers found a camera"); + } + + var cameraTransform = camera.GlobalTransform; + var cameraTranslation = cameraTransform.origin; + + // If we wanted to constraint the labels to the screen area we would need this variable + // var screenArea = GetViewport().GetVisibleRect(); + + foreach (var number in activeNumbers) + { + number.LifespanRemaining -= delta; + number.CurrentPosition += number.Velocity * delta; + + var numberTranslation = number.CurrentPosition; + + bool isBehindCamera = cameraTransform.basis.z.Dot(numberTranslation - cameraTranslation) > 0; + + // Fade if close to camera + var distance = cameraTranslation.DistanceTo(numberTranslation); + + float alpha = Mathf.Clamp(RangeLerp(distance, 0, 2, 0, 1), 0, 1); + + // TODO: make it not start to fade immediately + alpha = Mathf.Min(alpha, number.LifespanRemaining / number.TotalLifespan); + + var unprojectedPosition = camera.UnprojectPosition(numberTranslation); + + // In the example project there's some fancy logic for keeping stuff on-screen edges if it would otherwise + // go off-screen + if (isBehindCamera) + { + number.AssociatedLabel.Visible = false; + } + else + { + number.AssociatedLabel.RectPosition = unprojectedPosition; + number.AssociatedLabel.Visible = true; + number.AssociatedLabel.SelfModulate = new Color(1, 1, 1, alpha); + } + } + + var toRemove = activeNumbers.Where(n => n.LifespanRemaining < 0).ToList(); + + foreach (var number in toRemove) + { + number.AssociatedLabel.Free(); + activeNumbers.Remove(number); + } + } + + public void AddNumber(float damage, Vector3 position) + { var label = new Label() { - Text = "Test from a mod", + Text = Math.Round(damage, 1).ToString(CultureInfo.CurrentCulture), }; + // TODO: make the text label center better on the position instead of having the left edge of the number there + + // TODO: change text colour based on the damage + AddChild(label); + + activeNumbers.Add(new FloatingNumber + { + AssociatedLabel = label, + CurrentPosition = position, + LifespanRemaining = NumberLifespan, + TotalLifespan = NumberLifespan, + Velocity = NumberVelocity + VelocityRandomNess * (float)random.NextDouble(), + }); } - public override void _Process(float delta) + /// + /// Seems to be missing from Godot C#, helpfully provided on Godot Q & A site: + /// https://godotengine.org/qa/91310/where-is-range_lerp-in-c%23 by AlexTheRegent + /// Modified to conform to naming style. + /// + private static float RangeLerp(float value, float iStart, float iStop, float oStart, float oStop) + { + return oStart + (oStop - oStart) * value / (iStop - iStart); + } + + private void TryGetCamera() { + camera = GetViewport().GetCamera(); + } + + private class FloatingNumber + { + public Vector3 CurrentPosition; + public Vector3 Velocity; + public float LifespanRemaining; + public float TotalLifespan; + + public Label AssociatedLabel; } }