Best practice for calling methods from base node when a script is attached

Godot Version

.net v4.2.1

Context

While working on an ability framework, I’m creating a test-ability that damages and knocks back all colliders (let’s call these collider) that are hit by a capsule (using ShapeCasting).

To damage entities, I determine whether the collider has a child node that is of type Health (my script). This is implemented and works as expected. However…

…to knock back entities, I have to perform a dynamic function call to apply_central_impulse instead of casting the collider as a Rigidbody3D and calling ApplyCentralImpulse(). The reason I have to do this is because some of the Rigidbody3Ds in my scene has a script attached that inherits from Node3D - not RigidBody3D. Because of this, the Rigidbody3D is not present in the static context.

The described code (health, and knockback):

	var result = owner.Shapecast(sh, pos, pos);
	
	// Damage and push all colliders hit
	for (int i = 0; i < result.Count; i++)
	{
		Node3D n = (Node3D)result[i]["collider"];
	
		// Node3D operations
		if (n.IsValid())
		{
			// Damage the node's health
			n.Dmg(damage);
	
			// NOTE: I guess this is how you call a "script-node"'s methods?
			if (n.HasMethod("apply_central_impulse"))
			{
				Vector3 toColl = n.GlobalPosition - owner.GlobalPosition;
				float distFactor = (radius - toColl.Length()) / radius;
				Vector3 imp = (toColl.XZ().Normalized() * knockbackDirection.X + Vector3.Up * knockbackDirection.Y) * knockbackForce * distFactor;
				n.Call("apply_central_impulse", imp);
			}
		}
	}

Below you can find additional code if you want the full code context:

ShapeCast Extension
	public static Godot.Collections.Array<Godot.Collections.Dictionary> Shapecast(this Node3D n, Shape3D shape, Vector3 start, Vector3 end)
	{
		// Create a raycast query
		var spaceState = n.GetWorld3D().DirectSpaceState;
		var query = new PhysicsShapeQueryParameters3D()
		{
			Shape = shape,
			CollideWithBodies = true,
			Transform = new Transform3D(Basis.Identity, start),
		};

		// Exclude this object from the raycast
		if (n is CollisionObject3D)
			query.Exclude = new Godot.Collections.Array<Rid> { (n as CollisionObject3D).GetRid() };

		var result = spaceState.IntersectShape(query);

		return result;
	}
Dmg Extension
	public static void Dmg(this Node n, float amount)
	{
		if (n.TryGetNode(out Health h))
		{
			h.Damage(amount);
		}
	}
TryGetNode Extension
	public static bool TryGetNode<T>(this Node n, out T node) where T : Node
	{
		for (int i = 0; i < n.GetChildCount(); i++)
		{
			if (n.GetChild(i) is T)
			{
				node = (T)n.GetChild(i);
				return true;
			}
		}

		node = null;
		return false;
	}
IsValid Extension
	public static bool IsValid(this GodotObject go)
	{
		return Godot.GodotObject.IsInstanceValid(go);
	}

Question

What is the preferred method, or best practice, for coding an interaction with a Rigidbody3D that has a script attached to it?

I’ve got some ideas of my own (see below), but I want to hear the thoughts of people with more Godot experience than me.

My ideas

  • Avoid attaching scripts to Rigidbody3Ds (not viable)
  • Nest the Rigidbody3D inside a Node3D and attach the script to the new parent (annoying…)
  • Implement an interface on all scripts, that is assumed to be attached to RigidBody3Ds, that provides access to the RigidBody3D methods in the static context. (not viable)