How to Make a 2D Character Controller | SOLID Principles


Following up from our previous tutorial, I want to talk about how you can create reusable components for your games.

The whole idea is to encapsulate everything that may change. For instance, we have our Moving Character in place. Now we are going to allow players to control it.

Our movement logic has nothing to do with our controls, so why bundle them in the same class? We may want to reuse the Moving Character in our enemies. In this case, allowing the player to control them doesn't make sense for our little platformer.

But then, why not just inherit the Moving Character class and make a "Playable Moving Character" like we did in the first iteration of the book?

The Inheritance Secret

Well...because I learned what inheritance is made for and when to use it. The idea of favoring composition over inheritance boosts all your production once you understand the power of breaking your game logic into smaller, reusable logic pieces.

Just so you understand how powerful I find this approach to be, nowadays I have a class just to make for loops and execute something on each iteration. Combining with the Spawner recipe from the Top 7 Godot Recipes, I can instantiate groups of enemies, asteroids, loot, and more. Not a single extra line of code was added to the Spawner recipe, but new functionality emerged.

To understand the main issue of inheritance, you should think about it as the hardest form of coupling. Any change in the superclass may break the subclass. And we don’t want that. But if not inheritance, how can we make reusable code?

Favoring Composition over Inheritance

Breaking our code into components and making them work together! This has the double benefit of allowing you to solve one problem at a time and improve your solution over time, making a specialized solution for your problems. But it sounds easier said than done, right? Well, let me give you a nice framework to decide when to make a component and when to inherit a class.

You should only use inheritance when these two conditions are met: your class will use the superclass code AND it will override the code with new functionalities.

If your class will only use the superclass code, then it can pretty much be a component. If your class will only override its interface with new functionalities, then it’s better to make an abstract class.

In that sense, our character controller will only use the Moving Character code, but it won’t override it with new functionalities. So, it will be a new component.

The Character Controller

With all that technical background in place, let’s dive into action. Create a new scene, add a basic Node as root node, and follow the steps below:

  1. Attach a new GDScript name MovingCharacterController.

  2. Export a variable to refer to the Moving Character it will control.

    class_name MovingCharacterController
    extends Node
    @export var moving_character: MovingCharacter  
  3. Next, export three variables that will each connect an input action to a specific movement.

    class_name MovingCharacterController
    extends Node
    @export var moving_character: MovingCharacter
    @export var move_left_input_action := "move_left"
    @export var move_right_input_action := "move_right"
    @export var jump_input_action := "jump" 
  4. After that, we are going to override the _unhandled_input() callback to add the input handling logic. Let’s start with the jump logic

    func _undhandled_input(event: InputEvent) -> void:
        if event.is_action_pressed(jump_input_action):
            moving_character.jump()
        elif event.is_action_released(jump_input_action):
        moving_character.cancel_jump() 
    📝 Note:

    Here we can see our inheritance framework making its job. We extend a Node so we have access to its main feature, which is to be processed by the SceneTree’s main loop, AND we are overriding its _unhandled_input() method to add a new functionality to the Node.
  5. Now, let’s add the horizontal movement logic. I like to take as a design premise the fact that if the player is pressing a key, something should be happening. Therefore, we won’t make those axis math to sum the value of the InputAction axis and use it as the direction. Instead, we will deal with each use case. The character will only stop moving if both the move left and move right input actions are released
func _undhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed(jump_input_action):
        moving_character.jump()
    elif event.is_action_released(jump_input_action):
        moving_character.cancel_jump()
    if event.is_action_pressed(move_left_input_action):
        moving_character.move_left()
    elif event.is_action_pressed(move_right_input_action):
        moving_character.move_right()
    if event.is_action_released(move_left_input_action) and not Input.is_action_pressed(move_right_input_action):
        moving_character.stop()
    elif event.is_action_released(move_right_input_action) and not Input.is_action_pressed(move_left_input_action):
        moving_character.stop()
🔓 Secret Unlocked:
Did you notice we used a mixed approach using the InputEvent and the Input singleton classes?

Some people use the Input singleton to read the status of a given input method in the _physics_process()for example. This may bloat the physics process loop with unnecessary logic. We don’t need to constantly check the status of the input. We just need to handle it when the player presses or releases a button. Which definitely doesn’t happen 60 times per second.

As a rule of thumb, use the _input() and the _undhandled_input() callbacks every time you want to handle…well…input events. The Input singleton is meant to be used to read the state of input methods. So it’s great to use it when you want to check the state of a given input method and handle it accordingly.

In our implementation, we need to check for the state of the move_left/move_right input actions at the very time the player released the opposite input action.

Next steps

Now that we have both a Moving Character and a controller, we can finally have a playable character. We are going to see in the next tutorial that there’s a small tweak we have to do in our mindset in order to create a good code base. Naturally, you might be thinking “well I will just add the Moving Character Controller as the child of the Moving Character”. Please, don’t. And you will understand why in the next tutorial.

But for now, that’s it. Thanks for reading!

~Henrique Campos

Get Platformer Essentials Cookbook

Buy Now
On Sale!
25% Off
$9.99 $7.49 USD or more

Leave a comment

Log in with itch.io to leave a comment.