That statement makes me unsure of whether you understand the difference between inheritance and composition – they are two different things. While you can combine the two, composition is not a prerequisite for making inheritance “chains” work.
Alright, that makes more sense. Yes, definitely make generic code whenever possible.
Questions for the example
You have a few questions for the example I provided. I’ll try and answer those here.
Question #1| Damage Source
What does the Damage Source do; and why is its relation to other nodes “reversed”?
The DamageSource
script I have created will, in theory, allow any collision object to deal damage to another node; damage occurs when a collision happens between its parent and another collision object.
As for how the DamageSource
communicates with the rest of the system (and how it differs from my Health
node), the following pseudocode applies.
Note: I use C# which is not always directly comparable to GDScript.
Note-note: The full scripts that are part of my previous example can be seen at the end of this post.
func _enter_tree():
# Get the parent
# If parent is an Area3D or a RigidBody3D
# Subscribe to the body_entered signal
func _exit_tree():
# If parent is an Area3D or a RigidBody3D
# Un-subscribe from the body_entered signal
func on_body_entered(n: Node):
# Calculate damage based on collision data
# Damage any health nodes present in the node's children
Note how this script subscribes to another node’s signal. It responds to a signal. In contrast, my Health
node is the one emitting the signal; it is the transmitter instead of the receiver. That is what I meant by them being opposites. The way they use signals to interact with the system is opposite.
Health
→ transmits a signal when damaged
DamageSource
→ receives a signal and “performs” damage
Question #2
Why find the health node at run-time instead of saving a reference; is this not bad for performance?
You’re right. In the optimal case, you would save a reference to a node so you don’t have to search for it. However, I find my alternative to be so much easier to work with.
Let’s say you were to save a reference to the health node in a variable. This reference would likely be stored in the script of the scene’s root. That is where the problem occurs.
Now you have to cast the node you want to damage into a specific type: the script you added the health reference to. But okay, fine – we can just do that. The real problem with this approach is that you would have to add a health reference to every script located on a scene root that is damageable. Suddenly, you need to test against an ever-growing set of types on the root of an object in order to damage it.
So, you may increase performance by avoiding the need to find a specific node, but performance is then subsequently decreased due to the need for type-testing on the root inhabiting the reference.
With the approach I’m using for my Health
node, you do have to test the type of a set of children, but at least it’s only testing for one type. Honestly though, the best part is the fact that you don’t need to add health-related variables to a non-health related script. Everything just works as soon as you add the node (but again, you have to connect any relevant signals in the editor). It makes for much cleaner, clearer, and error-free code.
Now that I think about it, you could probably optimize the approach I’m using by making a cache that stores all health nodes and their related parents. That would basically serve as a lookup table and reduce it to one node search per object per session.
Question #3
As far as I understand the HP is stored in the Health script.
What if you want to create multiple Vehicles in your main scene, with them not all having the same hp?
Well, for my particular case, I likely wouldn’t want one type of vehicle to differ in maximum health between instances. Different vehicle types will have their own prefab and thus their own configurable health node.
Though, if I would want different health amounts, I think that is when you would either make a new prefab, or actually expand the children of the prefab and modify the instance itself. Yup, that’s right – you can actually modify the children. But treat it as a last resort!

Question #4
Could you possibly show the code for how you connect the signals dynamically?
Yes. I’ll provide a brief sidenote but the script can be seen below.
Sidenote
From my perspective, I see a reasonable amount of people that are overly concerned with producing generic, and performance-perfect code. Sure, generic code is great, but if the goal of god-like code halts your progress that’s bad. I tend to view one’s code architecture as being more important. Only worry about performance if you need to.
Scripts
Health script (C#)
Note: This is from a networking sample I’m working on. Stuff that mentions peer IDs or RPC is strictly for the networking.
using Godot;
using System;
[GlobalClass]
public partial class Health : Node
{
public long PeerID { get; set; }
/// <summary>Used as keys (casted to int) for the data received from one or more of Health's signals.</summary>
public enum SignalValue
{
Health,
Max,
Damage,
Position,
Normal
}
public struct DamageData
{
public long peerID;
public float damage;
public Vector3 position;
public Vector3 normal;
public DamageData(long peerID, float damage, Vector3 position, Vector3? normal = null)
{
this.peerID = peerID;
this.damage = damage;
this.position = position;
this.normal = normal.HasValue ? normal.Value : Vector3.Zero;
}
/// <summary>
/// Makes a Godot-dictionary containing all relevant data.<br />
/// This method is strictly used to interface with Godot's native functionality (e.g. signals).
/// </summary>
public Godot.Collections.Dictionary MakeDictionary()
{
var dict = new Godot.Collections.Dictionary()
{
{ (int)SignalValue.Damage, damage },
{ (int)SignalValue.Position, position }
};
if (normal != Vector3.Zero)
dict.Add((int)SignalValue.Normal, normal);
return dict;
}
}
[Signal]
public delegate void OnDeathEventHandler();
[Signal]
public delegate void OnHealthChangedEventHandler(Godot.Collections.Dictionary data); // Should contain: health, maxHealth, damage, position, and normal.
[Export] protected int startHealth { get; private set; } = 10;
[Export] protected float minTimeBetweenDamage { get; private set; } = 1f;
[Export] protected bool invulnerable = false;
private ulong lastDamageStamp;
protected float TimeSinceDamage => (Time.GetTicksMsec() - lastDamageStamp) / 1000f;
protected float Value { get; set; }
public override void _Ready()
{
Reset();
}
/// <summary>Resets all state-related variables</summary>
public virtual void Reset()
{
Value = startHealth;
}
/// <summary>
/// Alters the health value by the amount given.
/// </summary>
/// <param name="data">Data about the damage event (damage, position, and normal)</param>
public virtual void Damage(DamageData data)
{
if (Mathf.IsZeroApprox(data.damage))
{
return;
}
// DEBUGGING
float time = 3f;
this.ShowSphere(time, data.position, 0.2f, c: Colors.OrangeRed);
if (!invulnerable)
{
// Damage Callback
if (data.damage != 0f && TimeSinceDamage >= minTimeBetweenDamage)
{
lastDamageStamp = Time.GetTicksMsec();
var signalData = data.MakeDictionary();
signalData.Add((int)SignalValue.Health, Value - data.damage);
signalData.Add((int)SignalValue.Max, startHealth);
Rpc(MethodName.SetHealth, signalData);
}
// Death Callback
if (Value <= 0)
{
EmitSignal(SignalName.OnDeath);
}
}
}
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void SetHealth(Godot.Collections.Dictionary data)
{
// DEBUGGING
GD.Print($"{this.TreeMP().GetUniqueId()} => {GetParent().Name} health: {Value}");
Value = (float)data[(int)SignalValue.Health];
EmitSignal(SignalName.OnHealthChanged, data);
}
}
DamageSource script (C#)
Note: This is from a networking sample I’m working on. Stuff that mentions peer IDs or RPC is strictly for the networking.
using Godot;
using System;
[GlobalClass]
public partial class DamageSource : Node
{
public long PeerID { get; set; } = -1;
[Export] private float baseDamage = 1f;
[Export] private float variance = 0f;
[ExportGroup("Physics Settings")]
[Export(PropertyHint.Range, "0,1")] private float linearVelocityInfluence = 1f;
[Export] private Vector2 linearVelocityRange = new Vector2(5f, 10f);
[Export(PropertyHint.Range, "0,1")] private float angularVelocityInfluence = 0f;
[Export] private Vector2 angularVelocityRange = Vector2.Down;
private CollisionObject3D co;
private bool isArea;
private bool isRigidbody;
public override void _EnterTree()
{
co = GetParent<CollisionObject3D>();
// Determine type of collision object
if (co.IsValid())
{
if (co is Area3D)
{
isArea = true;
}
else if (co is RigidBody3D)
{
isRigidbody = true;
}
else
{
GD.PushWarning($"{co.Name} is not a valid collision object!");
}
}
// Subscribe to relevant collision signals
if (isArea)
{
Area3D a = (Area3D)co;
a.BodyEntered += OnBodyEntered;
}
if (isRigidbody)
{
RigidBody3D rb = (RigidBody3D)co;
rb.BodyEntered += OnBodyEntered;
}
}
public override void _ExitTree()
{
// Unsubscribe to relevant collision signals
if (isArea)
{
Area3D a = (Area3D)co;
a.BodyEntered -= OnBodyEntered;
}
if (isRigidbody)
{
RigidBody3D rb = (RigidBody3D)co;
rb.BodyEntered -= OnBodyEntered;
}
}
private void OnBodyEntered(Node n)
{
float damage = baseDamage;
// Variance
float v = 0f;
if (variance > 0f)
{
v = (float)GD.Randfn(0f, variance);
}
// Physics influence
float linVelMod = 1f;
float angVelMod = 0f;
// ==============================================================================
// TODO: Velocity is post contact. Make the code below get the previous velocity.
// NOTE: Use a state history (similar to link below).
// https://forum.godotengine.org/t/determining-the-exact-global-position-of-a-collision-with-rigidbody2d-body-shape-entered/77007/2?u=sweatix
// ==============================================================================
if (isRigidbody)
{
RigidBody3D rb = (RigidBody3D)co;
PhysicsDirectBodyState3D rbState = PhysicsServer3D.BodyGetDirectState(rb.GetRid());
int contactIdx = rbState.GetContactIndexFromNode(n.GetInstanceId());
Vector3 contactPosition = contactIdx != -1 ? rbState.GetContactColliderPosition(contactIdx) : Vector3.Zero;
Vector3 contactNormal = contactIdx != -1 ? rbState.GetContactLocalNormal(contactIdx) : Vector3.Zero;
if (linearVelocityInfluence > 0f)
{
// Modulate the damage based on this source's motion direction relative to the direction towards the impact point, and the speed of the damage receiver.
// TODO: Reformulate.
Vector3 toContactPoint = contactPosition - rbState.Transform.Origin;
Vector3 linVelocityDifference = rbState.LinearVelocity - DetermineVelocity(n).Project(rbState.LinearVelocity);
float toPointDotVelocity = toContactPoint.Normalized().Dot(linVelocityDifference); // NOTE: Is intentionally not normalized.
toPointDotVelocity = Mathf.Max(0f, toPointDotVelocity); // Clamp off negative values (no negative damage allowed!)
linVelMod = (toPointDotVelocity - linearVelocityRange[0]) / (linearVelocityRange[1] - linearVelocityRange[0]);
linVelMod = Mathf.Clamp(linVelMod, 0f, 1f);
// DEBUGGING
if (linVelocityDifference.LengthSquared() > 0.01f)
this.ShowLine(3f, contactPosition, contactPosition + linVelocityDifference * (toPointDotVelocity / linVelocityDifference.Length()), 0.05f, Colors.OrangeRed);
}
if (angularVelocityInfluence > 0f)
{
// TODO: Implement angular velocity influence.
// ...You may need to base it on tangent-velocity.
Vector3 contactVelocity = rbState.GetContactLocalVelocityAtPosition(contactIdx);
Vector3 contactVelocityDiff = contactVelocity - DetermineVelocity(n).Project(contactVelocity);
angVelMod = (contactVelocityDiff.Length() - angularVelocityRange[0]) / (angularVelocityRange[1] - angularVelocityRange[0]);
angVelMod = Mathf.Clamp(angVelMod, 0f, 1f);
// DEBUGGING
if (contactVelocityDiff.LengthSquared() > 0.01f)
this.ShowLine(3f, contactPosition, contactPosition + contactVelocityDiff, 0.05f, Colors.GreenYellow);
}
// Apply physics modifier to damage amount
float maxMod = Mathf.Clamp(linVelMod + angVelMod, 0f, 1f);
damage *= maxMod;
// Apply damage to node (if this is the local client OR is a non-player object on the server)
// NOTE: Replication is handled by the affected health node(s)
bool isLocal = this.TreeMP().GetUniqueId() == PeerID;
bool isServerSideNonPlayer = this.TreeMP().GetUniqueId() == 1 && PeerID == -1;
if (isLocal || isServerSideNonPlayer)
{
n.Dmg(new Health.DamageData(
PeerID,
damage,
contactPosition,
contactNormal
));
// Add shake to the scene
ShakeObject.Create(this, 0.8f, 12f, 0.05f * maxMod, contactPosition, 10f);
}
}
}
private Vector3 DetermineVelocity(Node n)
{
if (n is RigidBody3D rb)
{
return rb.LinearVelocity;
}
else
{
return Vector3.Zero;
}
}
}
Utility function for dealing damage (C#)
/// <summary>
/// An extension function for damaging health nodes.<br />
/// </summary>
public static void Dmg(this Node n, Health.DamageData data)
{
// Forum-specific: The TryGetNode-method is another util function.
if (n.TryGetNode(out Health h))
{
h.Damage(data);
}
}
Did I miss anything? I hope not.