10
\$\begingroup\$

How?

I think I still don't properly understand how composition in Godot works. So, as far as I know, you 'compose' stuff in Godot, right? Like, if you want to make a rock you create a mesh node with your rock 3D model. You want the rock to have collisions? oh ok, then add a body node and specify its collision shape (another node). Now you want the rock to talk...for some reason (?), well you can! add a 'talk' node! oh want it to shoot lasers? oh boy, add a big and juicy 'shoot' node! And so on, you add and add...and add nodes. You compose your rock through aggregation I guess?

Now, I'm trying to make the controllable character the player is going to use, but to be honest I'm still not sure about its full extent of abilities. It may be able to run, to jump, to fly, or it may not. Through aggregation, this problem is likely solved, isn't it? since I could just make the 'base' character, with some basic functionality, and then add more functionality as needed.

I've seen some code that does something like this:

class PlayerController() 
{
    private Movement _movement;
    private Jump _jump;
    private Attack _attack;

    private DoStuff()
    {
       // Let's imagine we're handling input here
       _movement.DoMovememnt();
       if(_jump != null) _jump.DoJump();
       if(_attack != null) _ attack.DoAttack();
    }
}

But the problem here is that the PlayerController class is expecting, to some extent, the other classes to exists. I don't think that's how it should work...

If I understood this article, that would some sort of a Component Container.

Then, I've also seen stuff like this:

class Movement() { // Do stuff }
class Jump() { // Do stuff, and if neccesary, modify properties from `Movement()`}
class Attack() { //Do stuff, and if necesary, modify properties from `Movement()` or `Jump`}

Basically, the PlayerController doesn't actually exists, and its more of just a concept within the system (in Godot, the player would be just the name of the node, which holds the Movement, Jump and Attack nodes). If the player attacks and an enemy, and you've decided that the player can't move and attack at the same time, then the Attack node could access some properties from the Movement node to block actual movement.

Though I'm not sure if that last bit could be considered aggregation, since in that particular case, wouldn't accessing other class properties create dependency? I don't see much of a problem in here because the 'base' doesn't need any of the classes above to work, nor isn't expecting any class to exists.

Although to be honest, I can't see any realistic scenario were, for example, a Jump node doesn't need to know anything about the Movement node, since usually jumping affects somehow movement. Like, it would be physically impossible to entirely isolate both one from each other.

Buuuuut I'm probably not getting the idea behind composition. Or did I?

Keep in mind that I'm using C# and Godot 3.5.2 (unfortunately I can't go 4.x due to my requirements).

\$\endgroup\$

3 Answers 3

13
\$\begingroup\$

I have gone from a monolith player controller, to an state machine, to a behavior tree, to something closer to what you describe.

My current character controller looks something like this in the scene tree:

character
├ collider
├ model
├ animation_tree
├ character_jump
├ character_run
├ character_auto_walk
├ character_edge_guard
└ character_rotate

The character node handles gravity, keeps some common properties and has helper methods that character_jump, character_run, character_auto_walk, character_edge_guard, and character_rotate use.

Now, character does not call them. Godot call them. By virtue of being Nodes on the scene tree I can use _ready, _enter_tree, _exit_tree, _input, _process, _physics_process, etc. They can also use Input on their own.

Instead they call into the character. So character do not depend on them, they depend on character.


And they do not depend on each other... And for that their order is key.

For, you see, character_jump and character_run modify the velocity property of the character, but if character_auto_walk is active, by virtue of character_auto_walk being after character_jump and character_run in the scene tree, it runs afterwards and would overwrite whatever modification to velocity that character_jump and character_run might have done.

Similarly, if character_edge_guard is active, it can overwrite velocity to prevent the character from walking off a cliff, regardless if it was previously set by character_jump, character_run or character_auto_walk.

And character_rotate is at the end, because I want the character to rotate to face in the direction it is moving (according to velocity) when it is all said and done.

In my current project I do not have means of attack, but my current thinking is to place a character_attack after character_rotate so the attack can use the updated orientation. And yes, it can also overwrite velocity to make sure the character does not move while attacking, for example.


Picking animations is handled in the animation_tree, and it is done by reading the properties of the character. It does not have to be an AnimationTree.

\$\endgroup\$
1
\$\begingroup\$

Basically, the PlayerController doesn't actually exists, and its more of just a concept within the system (in Godot, the player would be just the name of the node, which holds the Movement, Jump and Attack nodes).

It does exist in that a KinematicBody has a built-in connection to the physics engine, it has some assumptions that any physics object will want to display something and have some collision information.

I think it can be dangerous to over-engineer the architecture of a game that might not even have a formal design document yet. I think it's better to start with a real simple prototype and not get sidetracked trying to solve problems you might not ever encounter.

But the problem here is that the PlayerController class is expecting, to some extent, the other classes to exists. I don't think that's how it should work...

This is an advantage of a game engine though, that it gives you a recommended set of patterns of how things fit together.

The way I organize things is to make a Player scene (Player.tscn) with a KinematicBody "Player" node, whose children are a Sprite or Mesh node, collider nodes, etc. Main player logic is in a single state machine script attached to the parent KinematicBody. The _physics_process() loop is just an if statement that cycles every frame to fire off bits of the state machine.

I usually have weapons or items as separate nodes, instanced as children of the Player node, then triggered by state machine. All of the weapon logic, animations, etc. are off in a separate .tscn scene. Other parts of the state machine, like jumping or blocking, can usually happen directly in the Player.cs (or Player.gd) script, or in a Jump.cs that gets preloaded and instanced by the state machine. But not every behavior needs a whole separate .tscn scene the way weapons and items do. I go with the flow of the Godot hierarchy when it makes sense to do so, but I don't pre-build any more complexity into the interface than I need to.

\$\endgroup\$
0
\$\begingroup\$

But the problem here is that the PlayerController class is expecting, to some extent, the other classes to exists. I don't think that's how it should work...

The PlayerController should be expecting the "base" classes to exist. Each new controller would be setup to be able to be composed of all the bases classes and it is these bases classes that you then extend with new specialized children.

I might also recommend bringing in these base classes via dependency injection within the constructor of your controller. That way you can either supply the specialized children of those bases or not.

\$\endgroup\$
1
  • \$\begingroup\$ Huh, so, generic/abstract implementations via interfaces, inheritance, but mixed with composition? \$\endgroup\$
    – YoshGJ
    Commented Jul 14, 2023 at 21:13

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .