My question is somewhat similar to this one I asked earlier: Other Post
But it is different enough that I decided to make a new Topic.
I have a simple hit system where both the Player and the Enemy can take damage. I have created a HealthNode Scene which handles Damage.
extends Node
## Handles the hp system
##
## If an Object with Hp gets hit it loses hp. It might die if it drops to 0. It both cases a signal
## Is invoked by this Script. The Hp are also properly incremented
## The max hp of the parent Node
@export var _max_hp = 100
var _current_hp
## The Object that is taking damage
var _damagedObj
signal health_changed(amount: int)
signal death
## Makes sure that the Hiting system is properly set up
##
## Connects the area_entered Signal to the appropiate function
## Sets the current Hp to the max hp
func _ready() -> void:
_current_hp = _max_hp
var _damagedObj = get_parent()
_damagedObj = get_parent()
if _damagedObj is CharacterBody2D:
_damagedObj.hit.connect(_onDamageTaken)
print(_damagedObj, " connected succesfully")
elif _damagedObj is Area2D:
_damagedObj.area_entered.connect(_onDamageTaken)
print(_damagedObj, " connected succesfully")
func _onDamageTaken(_damagingObj):
_updateHealth(_damagingObj.getDamage())
func _updateHealth(damage: int):
if(_current_hp - damage > 1):
_current_hp - (damage/_damagedObj.getArmour())
health_changed.emit(damage)
else:
death.emit()
This works.
The Problem is, that every _damagedObj needs to have a getArmour() method. (It should return the amount of armor the Object has as a number between 1 and 0) (I know I’m not handling the 0 case I will fix this later)
This function works the same no matter which _damagedObj it is attached to. However some of the _damagedObj will be enemies and some will be the Player.
The Player is a CharacterController2D and the Enemy is a Area2D.
I could change this and make the Enemy an CharacterController2D but I wonder if this is really the only solution to my problem.
I want to avoid duplicate Code and thus would like to write a script that has the getArmour() function and extend it in both the Player and the enemy.
But this would cause both of them to get the Type of this new script so eighter CharacterController2D or Area2D. No matter what I chose the other script breaks because it is missing funcitons.
How can I solve this without:
Copy Pasting the same function for both the Enemy and the Player
The root of your issue is that you have Player and Enemy extending different node types. By doing so, you have left yourself no other option than to duplicate your code in both classes.
I am aware of this. However, I want to have different kinds of CollisionObjects to be able to hit and get hit. For me, this does not seem like a very uncommon need.
I do not see how this solves my problem. My problem is not that I get crashes when calling a method that does not exist. My problem is that I want to have Armour on both the player and the enemy. Even if I use Metadata, I would still have to apply it both to the player and the enemy separately, right?
Using composition instead does sound appealing. In the other Forum topic I linked in my original post, composition was a central topic.
However, I don’t know how to use it in this case.
I tried to use composition in my previous project. But in the last topic I learned that the inner implementation of scenes (is not fully) but should normally be hidden for instantiating scenes. But even if I would ignore that and use scene chains instead of inheritance, I would:
Make a scene called _damagedObject
Attach a script with the _getArmourImpl() Method and its implementation
Instantiate this scene in the Enemy and Player scenes.
Reference that object in _player and _enemy scripts. (@export or getNode())
Define a _getArmour method in both the Player and the Enemy
Call _getArmourImpl from both those methods.
This would allow me to be able to change one method for both Player and Enemy. But:
I would still have to implement a simple getArmour() method for every _damagedObj.
I would have to worry about getting the reference every time I create a new _damageObj. (Say I want to add a projectile Scene or something similar) (yes getNode() solves doing this manually put it isn’t pretty because of its inefficiency. I can’t help but feel like I have missed something crucial. There is no way everybody who uses different kinds of CollisionObjects for Players and Enemys just goes ahead and dupes their code, right? And It’s not just that. Every time I use godot I feel like I encounter this problem so many times. What if I want to reuse the same methods for different types of Nodes. Is there really no good way?)
Sure this kind of composition is a solution and if it is the best there is I will take it.
I just wonder if there was something better than that.
The problem is really that the getArmour() method in my case should be located at the level of a CollisionObject2D.
I really appreciate you trying to help me. So again, thanks a lot.
I get that. Even if I would not apply anything, the code would just assume that nothing is there and apply the damage as if there would be no armor.
That avoids errors because the function is missing.
However, I do not want to avoid that error. It is even good that it is thrown because there should be armor on every _damagedObj. No matter whether it’s a Player or an enemy or anything else.
Thus, making an edge case for the case that there is none is counterproductive.
I am looking for a way to define a getArmour() function (or similar)
and attach it to every _damagedObj even if they are of different times.
(By the way, if I ever should sound rude in these post, I really don’t mean it. I am truly grateful that you are trying to help me.)
Create a common base class that extends Node2D (let’s call it Entity), and put all your common data there. Player and Enemy will have a huge amount of overlap, with the biggest difference being some nodes in the Entity subclasses (Player and Enemy), so they will both extend Entity. That will solve your damage and health issues, and solve a bunch of upcoming problems you don’t yet know you have.
Your Enemy is current a subclass of Area2D, but that is trivially handled by adding an Area2D to your Enemy class instead of inheriting from it.
It is very simple and probably the most appropriate here.
You create a scene with a collision body(area2d), internally it can be set to hit or hurt. Once it collides with another of the same class that is set to the opposite type, this will generate a signal and the player or enemy node that is composed with will interpret the signal as a hit, or hurt, linking it to a function that will adjust the health. ( You may not even need to distinguish a hit/hurt internally, since the signal can be interpreted in its own context by the connected function ) Although you may want to pass some information like a damage value. This could just be a generic value that is passed in the signal.
This way you can consolidate the collision objects to talk just about collisions, signaling that something appropriate to your game happened.
Then you don’t really need to adhere to a damage function that is shared, because it becomes a signal you can connect to any function.