Shoud you use scenes instead of handing down code / How to access children of instanciated scenes

Godot Version

4.3

Question

`
I am learning Godot and GDScript, and I want to avoid both duplicate code and duplicated work in the editor. Thus, I split my movement system in 3 parts. A hittableObject, a movableObject and a player. The hittableObject script contains an implementation that applies to anything that can take damage (In my game, the same as everything that can get hit) I originally just handed down the implementation from the scripts. But then I noticed stuff like every hittableObject having a Hitbox (CollisionShape2D) with its signal connected to the according function in hittableObject. (Connecting from Code requires the name of the CollisionShape, so I would either have to always name them the same which does not feel like best practice or somehow get the name of the CollisionShape and the only way I can think of that would work without assuming implementation from other scenes would be getParent() which feels inefficient performance wise)
I also noticed the painful abundance of abstract classes or interfaces and then learned more about the importance of scenes in Godot, and I wondered if this is what should be used to hand down stuff like this. It sounds convenient to just always have the hitbox for the hittableObject in the right place. It also allows me to hand down functionality without having access to a lot of unnecessary functions because of the lack of a true private keyword.(yes I know that _ exists) It just seems odd to me that I can access the entirety of the inner implementation of any parenting class even if I should have no business changing it.
So I tried making a Scene hittableObject with a CollisionShape in it and the script attached, a Scene for the movableObject that instantiates the hittableObject and a scene for the Player that instantiates The hittableObject and has its own spriteRenderer.
This approach did come with a few problems:

  1. Sometimes I do have to access the Interface of another class. Like for example, the Player has to access the function changeMovedObject() from movableObject to make sure that movableObject moves the Player and not just the movableObject which is an instantiated scene in the Player scene and thus a child. This can be solved by with getNode or an @export variable. The variable is probably better, but does put a lot of never changing variables into the inspector, which does not feel like a best practice. But it gets even worse if

  2. I have to access the interface of a child of an instantiated variable. Like if I wanted to change a property of the hitbox of the player. In this case, I would have to create a getter in movableObject (and any other script in a scene that implements a scene and gets implemented by another scene) and call that from player. But what is the first real roadblock I have encountered is the inability to access these children from the inspector. The inspector just shows the properties of the main node of the instantiated scene. (At least as far as I can tell) This does not allow me to resize the hitbox of the player in the player scene. But this is where the SpriteRenderer is handled. So my only way would be to do this from the script, which feels horrible because

  3. I have to guess the required boundaries of the player.

  4. It changes the hitbox at runtime.
    I don’t know if GDScript has alternatives for this, but the ready function would be incredibly inefficient, as, as far as I understand, it is run every time I instantiate the related object. Not that bad for the player object, but horrible for say the enemies if they recalculate the size of their always equally sized hitbox every time I spawn one.

Right now, I am unsure what to do. Should I go back to using scenes like Unity Prefabs? Just handing down implementations from scripts, tolerating all the disadvantages I mentioned earlier? Is there something else I can do? What is the best practice here?

As far as I can tell, you seem to have a problem deciding how you want to structure your code/nodes. From what I’ve read, using composition through the use of multiple nodes is, for the most part, the way to go in Godot (See Godot’s Design Philosophy).

As for your particular case, I’m having a hard time deciphering how your system functions – something that would be good to know if I am to provide any meaningful suggestions for your system. Perhaps you could describe, in more clear terms, how the parts in your system relates to one another – not through paragraphs with small tangents, but with formatted text that conveys your system at a glance. You may consider supplying the following information:

  • Node tree example (Image or indented list)
  • UML class diagram; showcases the relationship between classes/scripts
  • Code snippets from relevant scripts
  • Other illustrative imagery that aids in the understanding of your system

I want to help but it’s very hard to read and understand what you’ve written here.

1 Like

First of all: Thank you for helping me
I tried to put my project into a UML:

Here would be my node tree:

player
	AnimatedSprite2d
	MovableObj
		HittableObj
			CollisionShape2d

Or to make inheritance really clear just the first 2 lines of every relevant script:

class_name _hittableObj
extends Area2D
class_name _movableObj
extends Node2D
class_name _playerScript
extends _movableObj

But just from the fact that this seems to be very hard to understand for somebody that does not know my system, I can tell that this is probably not best practice.
How would you in your games make sure that you don’t have to copy and paste the same code into every object with a hitbox? (Like players and enemies both using the same hp and hit detection system?)

I noticed that it might be difficult to understand my problem with the second post:
The CollisionShape2d from the HittableObject does not match the size of the SpriteRenderer on the Player scene. (Which i forgot to include in the UML ooops)
Full UML here:

(Using links to avoid the media limit)

Thus, I want to change the hitbox boundaries in the Player Node, but I cannot do so in the editor, because it only shows the main Node of instantiated scenes. So in this case the Main Node from MovableObject. But not its child, the HittableObject. Or the child of that, the Collisionshape2d which I want to change.

Secondly, I frequently encountered the problem that I want to access interfaces from “lower” scripts.
For Example:
_player needs to access:

## Sets the moved object
func setMoved(_newMoved) -> void:
	_toBeMoved = _newMoved

from _movableObj.
This can be done like this:

## Called when the node enters the scene tree for the first time.
func _ready() -> void:
	getMovableObj()
	_movableObjVar.setMoved(self)
func getMovableObj() -> void:
	_movableObjVar = get_node("MovableObj")

Or alternatively, the way I did it, by simply extending _movableObj. This has the negative side effect of having access to all the other functions of _movableObj.

Or I could use

@export var _movableObjVar : _movableObj

and applying it in the editor.

And there might be other options I’m not aware of.
With the design philosophy I used (Always use Scenes instead of extending scripts whenever possible), this problem appears every time I want to access any method from any other script.
What option should I use / not use? Or should I generally not create Node structures like this?

Thanks a ton for the extensive information. I now understand your problem a lot better. The following sections will tackle the problems that you’ve outlined in your recent posts.

Using and configuring prefabs (scenes)

For software such as games, it is convenient to have access to a blueprint for a complex prefabricated object (a.k.a prefab); an object whose configuration is predetermined ahead of time. With prefabs, you can instantiate complex objects on a whim without needing to build the object through code.

Prefabs also benefit your workflow as they can be easily copied and reused in the game engine’s editor. This is also nice for the maintainability of your project as any changes to the prefab will be automatically replicated to any instances of that prefab.

In Godot, the complexity within a prefab instance is hidden away; you don’t have access to children of the prefab. As you have noted, this is an issue if you want to configure each instance in a unique way.

However, these restrictions are entirely by design. As an example, scripts will often pack away the complexity of their functionality and only expose the parameters, variables, and functions needed to control its behaviour – it’s called encapsulation. The same principle should also apply to your packed scenes.

With the way you have built your player prefab, you have created a harsh semantic line between the _player, _moveableObject, and _hittableObject nodes. Similarly, your main scene will only have direct editor access to the root of your Player prefab. As far as the main scene is concerned, the player object is one holistic object.

I hope it’s clear that the nodes are only hidden in the editor. The nodes are just as accessible through code as any other. It’s purely to communicate the prefabricated/encapsulated nature of prefabs.

Inheritance vs composition

You mention that there are a few ways to go about using a script’s variables and methods:

  • Through a reference to the node/script
    • Exported variable
    • Through a reference retrieved at runtime (e.g. via get_node())
  • Through inheritance (i.e. extending a specific class)

The first two options listed here use composition. Using composition means creating a system that functions on a set of sub-systems i.e. a class that makes use of other classes via reference. On the other hand, inheritance allows you to create classes that are derived from a base class i.e. a class on top of another class.

The two concepts may sound similar but they each have contexts in which one is better than the other. As an example, a character controller for a specific game will likely inherit from a pre-existing controller to make itself function within the surrounding ecosystem while adding (extending) functionality that makes this new specialized class capable of new things.

Inheritance example:
PlayerController -inherits- CharacterBody3D

The key principle to follow when using inheritance is: the specialized class should never offer functionality beyond the nature of the base class. The CharacterBody3D class provides functionality for easy-to-use 3D platformer movement. Similarly, the PlayerController class should provide movement-related functionality – or more precisely, it should add this functionality.

If you wanted to add health, you should add it with composition instead of inheritance as a “Player Controller” should not manage health-related systems. An example of how health can be added with composition is described in the section below.

A prefab example (with health)

Here is a concrete example of a prefab that makes use of composition to create a vehicle object that has: health, damage, and physics joints.

Vehicle node tree example
Vehicle in the viewport

Pay special attention to the root node, Vehicle (4-wheeled), and the grey nodes in the bottom, Health and DamageSource.

The root node has a VehicleController script attached to it which controls the wheel joints named Spring (__). The VehicleController script uses inheritance as it is derived from the RigidBody3D native class. This is important since the vehicle should be able to interface with Godot’s physics system. The child nodes also rely on the root being a physics object.

The grey nodes are incorporated into the object through composition. The health, damage, and vehicle controller node are not directly aware of one another but will still function through the use of signals.

When the object is hit (the rigidbody), the health node is found at run-time and damage is applied. When the health node is damaged, it send out a “health changed” signal. Any nodes that are connected to that signal with respond accordingly and can do so uniquely.

The opposite setup is used for the damage node. The damage node will dynamically get a reference to the parent, determine its type, and connect to any relevant signals (e.g. _on_body_entered).

Crucially, this use of composition will allow me to add/remove health and damage nodes to any object that I desire – the nodes themselves will do the work for me (although I do have to connect nodes to the health’s signal).

Final remarks

I am still wondering why you felt the need to make a prefab for the MoveableObject and HittableObject. Unless you intend for other prefabs (e.g. enemy) to share the same configuration for these two nodes, there is no need for nesting prefabs like you’ve done.

The reason you should use prefabs is to make your workflow easier. Therefore, ask yourself why you’re making them. Is it because:

  • An object shares a 3D model configuration with another object?
  • An object is too complex to be created step-by-step at runtime?
  • The object functions as a modular piece for a more complicated object?

I guess my specific question is: why are the MoveableObject and HittableObject nodes not a direct part of the Player prefab? Surely your player will be different from your enemy objects – at least in terms of graphics and collision boxes.

You can do it, but you have to edit the packed scene, not the instance of it.

This is not necessarily an issue and it depends on the system you’re building. Hopefully the example I provided gave you one way of avoiding excessive referencing.

I’m not sure what you mean by avoiding copying and pasting code into an object. Do you mean attaching a script to a node, or?


I hope I covered most of the worries and doubts you have regarding this issue. Feel free to ask any further questions.

1 Like

First of all thank you for this very helpful extensive explanation.
You have already cleared up a lot of misunderstandings I had. However, there are a few questions left.
Let me start by answering the questions you asked.

As you assumed, I am planning to use the same parts for other stuff. The HittableObject is supposed to be a base both for the player and for the enemy.

Changing the hitbox in the packed scene would not allow me to use the same base scene for the player and the enemy.
My original idea was creating the hitbox there to make sure the signal is connected to the scripts.
Now I see that scenes seem to not be intended to be used this way, as I am not supposed to change the properties from much lower scenes in much higher ones, like changing the hitbox in the Player or the Enemy.
To be honest, the MovableObject has far less reason to exist than the HittableObject. It could be useful if I ever want the player to be able to move other things than their character. However, this is unlikely to happen.
My project exists for learning how to set up a good base System in Godot, and I just wanted to see if I can make inheritance chains work trough composition.
And it did make me learn a lot about the way this works.

I want to (for Example) create one script for connecting a hitbox to the hp system. I then want to reuse that script with inheritance or composition. I want to avoid copying its code into another script, having 2 different scripts that almost have the same functionality. I do not want to avoid attaching a script to a node. Shouldnt have used the term Object here. Still thinking in java…

I also have a few questions regarding your (very helpful) example:

Why do you use a different way here? Just to demonstrate the possibility? Or is there a reason for it? I also do not really understand what the DamageSource does in your example. (Perhaps that is not required)

Why do this at runtime? Wouldn’t it be better to use something like @export for performance?

Could you possibly show the code for that?
I am not sure how to do this without a lot of code that is only made for one specific case. (e.g. a specific type of parent node or a specific signal)

Also, as far as I understand the hp are 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?

1 Like

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!

image

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.

Thank you for this very helpful answer. The following statement is what I would conclude for my project:

  1. I should use composition, but children should be hidden inner implementation. Thus, stuff like a hittableObject with a hitbox that always has to be changed later should not be used.

  2. You can use composition instead of inheritance. (You can always make a field variable in a scene store the reference to a script in the base node and use that to access the script instead of inheriting its code.) But it should only be done in specific scenarios.
    Composition should only be used if you want to give more than just a scripts implementation to other scenes.

  3. Connecting both signals or references to or from instantiated scenes is a common problem, and there are multiple solution to it. (References via @export, getNode(), avoiding unnecessary reference’s trough signals) None of them are clearly superior.

  4. The easiest solution is not necessarily the most performative one in this case.

So for my project this means:

  1. Remove the HittableObject Scene and inherit the script in the _movableObj script instead.
  2. Add the hitbox directly to the player and the enemy and connect the signals individually or write a script like your damageSource to connect automatically.

Thanks a lot for your help and sorry for taking so much of your time. I just really want to get this right because this could save me a lot of work during later development.

1 Like

No worries. I enjoy curios people more than the counterpart. Besides, you have to ask questions if there is something you don’t understand – no need to be sorry about that.


Good luck with your project, and keep on keeping on!

2 Likes

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