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)

Based on my understanding of your question, I’ll share my thoughts. I’m not very familiar with C#, so I’ll describe the code in GDScript; the logic should be similar.

From your initial code, it seems like your ability framework is responsible for finding targets hit by the ability and handling damage and knockback effects within the framework itself. However, since the scripts for the targets are based on Node3D rather than RigidBody3D, it’s challenging to uniformly call the knockback function.

I think having the attacker handle the logic for the target might be overstepping its role, which is causing the issue here. Instead, you only need to notify the target that the skill includes a knockback effect without actually calling the function to apply knockback. This is helpful because your skills might later expand to include effects like burning, freezing, stunning, etc. Obviously, the way these effects are handled will vary for different targets. For example, a stone might not be affected by these effects. If you handle all the logic in the ability framework, it will become overly complex and bloated. Knockback is similar in this respect.

Here’s an outline of my current approach:

First, I’d make damage into an event that includes skill damage and any other potential effects.

class_name DamageEvent
extends Object

var damage: float = 0.0
var knocks_back: bool = false

and so on

Once the skill finds a target with a damageable component, it sends a damage event message to that target.

var damageable_com = target.get_child(“DamageableCom”)
if damageable_com:
var event = DamageEvent.new()
event.damage = 10.0
event.knocks_back = true
damageable_com.take_damage(event)

Within the damage component, any purely data-based calculations (e.g., damage calculations) are handled within the component itself (or can be sent to an attribute component for processing). If there are additional special effects, they’re emitted as events and handled by either the base class or other components.

For example, an object whose base class is Node3D wouldn’t need to handle these events, while an object of type RigidBody3D could listen and respond to these effects.

This way, all logic remains decoupled and clear.

Coincidentally, I produced a similar framework with Damage Events that contained: damage, contact point, and other things.

However, I never expanded it to include add-on effects such as knockback. You do a good job of explaining why my approach, at the time, was lacking:


Thanks for the precise, albeit late, response!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.