Godot Version
4.5.1 Stable
Question
Rather than having all my player code in the root CharacterBody3D, I decided to try and implement modular components. I am just having a difficult time understanding how to avoid dependencies.
As an example, say I have a “MovementComponent”:
class_name MovementComponent
extends Node
@export var move_speed: float = 5.0
func set_movement_velocity(character: CharacterBody3D, dir: float) -> void:
#character.velocity = some new velocity using dir and move_speed
Then in my player scene I add the MovementComponent node as a child then write:
extends CharacterBody3D
@export input_component: InputComponent
@export movement_component: MovementComponent
func _physics_process(delta: float) -> void:
movement_component.set_movement_velocity(self, input_component.get_input_dir())
move_and_slide()
All the above is pseudocode written on my phone so please excuse errors.
Is there a better way to have child nodes able to affect the root node without causing them to “know” about their parent?
I am not sure this is a good idea. What else would actually be using this component? Usually a component is made into a component in the first place because it needs to be shared. But this is for your player, and usually you will only have one.
A more usual example might be that your player has multiple movements. Then your movement components might be run, walk, jump, hover, fly, teleport or whatever. But even then these are more like states than components, so a state controller with various states, but again you are unlikely to be sharing your player states around.
When you say “all my player code” what do you mean exactly? Perhaps you need a state_controller rather than components.
For instance my player, since it is a space ship, has these ‘managers’:
The state manager has these states:
And the root CharacterBody3D Player has this in the _physics_process:
func _physics_process(delta):
# Velocity set in states
velocity = state_manager.current_state_controller.do_physics_process(delta)
move_and_slide()
# Deal with collisions
for i in range(get_slide_collision_count()):
var collision: KinematicCollision2D = get_slide_collision(i)
collision_manager.handle_collision(collision)
Now I have not refactored my player code in some time so I might do the collision bit slightly differently, but it all works very smoothly at the moment.
I would add here though there is no right way to do this, so if this suggestion feels unintuitive to you, then don’t take it of course. But there are no real ‘components’ to any of these as they are not shared with anything else.
Perhaps I have misunderstood your intent?
1 Like
I guess the motive behind making separated components was to eventually make it so:
Player
- player_input_component
- movement_component
- gravity_component
Enemy
- ai_input_component
- movement_component
- gravity_component
So it would sort of be reusing the components in other scenes except maybe the player_input component.
I like the state machine idea, I will definitely need to implement states. I think I would probably need to do this in tandem with modularized components
Ok, so your movement_component just needs an intial velocity of the player sent to it (probably not even needed as usually players start at Vector2.ZERO) in an initialisation process in the player root node after loading all the components.
So in your _physics_process just set your velocity something like;
func _physics_process(delta: float) -> void:
velocity = movement_component.set_movement_velocity(input_component.get_input_dir())
The actual current_velocity would be in your movement component at all times. No passing of self, no get_parent() stuff in the components either. Input component returns an input_vector. Movement component returns a velocity.
Does that solve the conundrum?
The only difference here between my original example is that you are now going to be troubled by if the player can actually move or not? Is it active or alive? What type of movements are allowed? This is why you have states for your player, and the movement for each state is set in the states themselves. This component at the moment seems to be acting like a single fixed state - so yes, I think you really need a state manager. You can then play different animations, sounds, depending on the state as well etc etc.
1 Like
That makes sense. So just a side-question to that, is it not a good idea to send self as a function argument? I didn’t think that would be considered as a hard reference / dependency
Yes, that is fine. It is very lightweight, but since you are asking for a return value, what do you need the ‘self’ reference for. It knows where to return to. (Unless you need to do something else with the self but that seems a bit circular, just send it whatever it needs without having to refer back to the ‘self’.
Just noticed you said:
No, that is what I meant by I doubt any enemies are going to be using the player movement. They need to do path finding and all sorts of other things that your player does not have to do.
EDIT: As a final note, I would not try to optimise too early. Let your player have its code and cut/paste it if you need to into the enemy, and let your enemies have their own components. They rarely share well between player and enemy.
1 Like
It’s a lot to think about, thank you for the discussion!
Do you actually need components?
1 Like