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;
}
}