1. Main Scene
  2. The Paddles
  3. The Ball
  4. Sounds
  5. The World
  6. The UI
  7. Takeaways
  8. Problems
  9. What’s on the horizon
    1. Release
    2. Visuals
    3. Sound
  10. Special Thanks

Equipped with the basics, I wanted to create a small game as a finger exercise on my own. In programming languages, it’s a common classic to output “Hello, World!” as a starting point. I think creating Pong is a suitable equivalent, like a “Hello, Gaming World!”. The code of the first version can be found in my repository. Just as the current version.

This is the first version of my attempt of creating a Pong game on my own: Battle Pong

For my initial attempt, my goal was to create a playable version of Pong with a minimum feature set of:

  • a human player can play against a computer enemy
  • a minimal menu
  • a few game sounds
  • score would be tracked
  • game ends when a certain score was reached, with the option to start a new game

Main Scene

The main scene’s current structure uses a simple Node to contain the world, UI, sounds, ball, and both the player and enemy paddles. This Node serves as a central container for all the game elements. With the exception of the SoundService, all other child nodes themselves serve as containers for additional nodes. However, as these were saved as Packed Scenes and added here as child nodes, they function as black boxes within the context of the main node. By concealing the internal contents of Packed Scenes when adding them as child nodes, it encourages giving the Packed Scenes a clear and well-defined API that can be accessed externally. This approach greatly facilitates the development of a modular software design.

Main scene node structure.

The Paddles

During development, I started with the PlayerPaddle because it was already clear to me which nodes it had to be composed of:

  • CharacterBody2D for the built-in move_and_collide method
  • Sprite2D to make the paddle visible
  • CollisionShape2D to detect collisions with the ceiling/floor and the ball automatically.

To standardize the movement of both PlayerPaddle and EnemyPaddle, I created a parent class called Paddle that inherits from CharacterBody2D, and manages the movement based on an input vector. Initially, the controls felt very clunky to me. After a brief research, I found that adding acceleration instead of immediate maximum speed feels much more natural. The complete code for the Paddle parent class is:

extends CharacterBody2D

class_name Paddle

const MAX_SPEED = 700
const ACCELERATION = 4000

func move(input: Vector2, delta: float):
    velocity = velocity.move_toward(input * MAX_SPEED, ACCELERATION * delta)
    move_and_collide(velocity * delta)

The move function requires a two-dimensional input vector and a delta, which represents the time since the last physics frame. Multiplying them ensures that approximately the same distance is covered per unit time in case of frame drops. For the PlayerPaddle, extending the Paddle class, the two parameters are determined as follows:

func _physics_process(delta):
    var input_vector: Vector2

    if Input.is_action_pressed("ui_up"):
        input_vector = Vector2.UP
    elif Input.is_action_pressed("ui_down"):
        input_vector = Vector2.DOWN
    else:
        input_vector = Vector2.ZERO
    
    move(input_vector, delta)

The EnemyPaddle, which also extends Paddle, adjusts its position based on a target: the Ball. The further the ball is above or below the paddle, the faster the paddle moves in that direction. When the distance between the centers of the paddle and the ball is 50 pixels or more, the input vector is at its maximum.

func _physics_process(delta):
    var distance = target.position.y -position.y
    var speed = clampf(distance / 50, -1, 1)
    move(Vector2.DOWN * speed, delta)

The EnemyPaddle provides its parent node with the ability to set a target via API.

var target: Node2D

func set_target(newTarget: Node2D):
    target = newTarget

This offers the advantage that the EnemyPaddle and the Ball do not need to know anything about each other. Instead, the Main node links the two after their own instantiation.

func _ready():
    $EnemyPaddle.set_target($Ball)

The Ball

The node structure of the ball is identical to that of the paddles: CharacterBody2D, Sprite2D, and CollisionShape2D for the same reasons. However, here I use a special feature of the move_and_collide method that I have only used so far for positioning along a predefined vector:

var collision: KinematicCollision2D  = move_and_collide(direction * speed * delta)

The function returns a KinematicCollision2D if a collision occurs in this frame, otherwise null. In the case of a collision with the ceiling or floor, the y-component of the ball must be inverted. In the case of a collision with the paddles, the x-component must be inverted. However, instead of having to do this manually, the KinematicCollision2D offers a much more elegant solution:

if collision:
	direction = direction.bounce(collision.get_normal())

I can simply calculate the direction after a reflection off the collision object using the bounce method. Even the required normal is supplied in the KinematicCollision2D object. This allows for physically correct scattering even with complex shapes, without any additional effort.

However, using this approach, the ball could not be controlled, and could only be reflected at a predetermined angle. If two perfect players were playing, the trajectory of the ball would be determined from the start to infinity. As I am not a big fan of determinism, I have added a bit of chaos:

func adjust_direction_y_based_on_hit_area(paddle: Paddle):
    direction.y += (position.y - paddle.position.y) / 200

func _physics_process(delta):
    (...)
    var collider = collision.get_collider()
    	if collider is Paddle:
	    	adjust_direction_y_based_on_hit_area(collider)

The further above the center of the paddle the ball hits, the stronger it will be accelerated upwards, and vice versa if it hits below the center of the paddle. This not only allows one to potentially confuse the opponent (if they were to think), but also allows one to accelerate the ball and win the game. The magnitude of the x-component of the ball’s velocity remains constant, but by positioning the paddle appropriately, one can increase the magnitude of the y-component of the ball’s velocity at each collision, and eventually win against the AI.

Ich bin mir nicht sicher, ob der Code für die Beschleunigung hier an der richtigen Stelle ist. Der Ball muss für diese Interaktion wissen, dass Paddel existieren und das erscheint mir reichlich unsauber. Vermutlich wäre es sauberer, ein Signal auszusenden, das den collider als Payload hat und die Main node verknüpft dann erst Paddle und Ball, indem sie den collider type prüft und dann ein accelerate oder ähnlich auf dem Ball aufruft. Aber wie gesagt, ich bin mir hier nicht sicher und struggle sowieso regelmäßig, wie hier eine vernünftige Architektur aussehen könnte.

I’m not sure if the acceleration code is in the right place here. The ball needs to know that paddles exist for this interaction, which seems rather messy to me. It would probably be cleaner to emit a signal that has the collider as a payload, and then have the Main node link Paddle and Ball by checking the collider type and calling accelerate or something similar on the Ball. But as I said, I’m not sure here and I regularly struggle with how to design a reasonable architecture here.

The ball also detects and signals when it hits one of the paddles, and even resolves it depending on which paddle it hits:

func _physics_process(delta):
    (...)
    if collider is Paddle:
        (...)
        if collider is PlayerPaddle:
            emit_signal("hit_player")
        elif collider is EnemyPaddle:
            emit_signal("hit_enemy")

Sounds

These signals are linked to the SoundService by the Main node to play alternating “Ping” and “Pong” sounds. The SoundService is a single AudioStreamPlayer2D node, where I load and play the appropriate signal depending on the sound.

func play_sound(sound: String):
    stream = load(sound)
    play()

Practically, a green arrow appears next to the method linked with the signal in the editor.

Clicking on this arrow opens a window that documents the connection in a very understandable way:

Overall, there are 4 sounds in the game: “Ping”, “Pong”, a sad melody when the player loses, and a happy melody when they win. For the first release, all the sounds are my voice recorded with Audacity. I just wanted to test how to place sounds in the right positions, and I find that it’s straightforward to do in Godot, as seen above. The only thing that makes me a bit uneasy is that I feel like the sounds are played with a slight delay, and this was already the case before I had to load each sound before playing it. I will need to investigate this further to see how to improve it.

The World

The World scene is kept quite simple with a green background, a CollisionShape2D at the top and bottom (representing the ceiling and the floor), as well as to the left and right (representing the player’s goal and the enemy’s goal).

World scene as seen in the 2D editor.

There are no big surprises with the nodes:

Node structure of the the World scene.

The background is implemented with a ColorRect node, which can easily be extended to cover the entire visible area and colored in Godot.

The ceiling and floor are StaticBody2D. Using a StaticBody2D, the Ball can automatically collide with them using its move_and_collide method.

The goals are Area2D that can be penetrated by the Ball. However, they register when other bodies enter their area and emit a body_entered(body) signal, which also transmits the information of the penetrated body. This signal is used in the Main node to register a scored goal, update the corresponding score label, and start a new round.

It’s clear that I was inconsistent with regard to abstraction level. The ceiling and floor are themselves packed scenes, in which StaticBody2D and CollisionShape2D are combined. With the goals, I added both as parent and child nodes visibly to the World. As I mentioned, I am unsure about architecture in Godot. However, I believe that the packed scene is more appropriate. The ceiling and floor serve the exact same purpose, as do the two goals. So, if functionality or additional child nodes are ever added, they will almost certainly be added to both objects. The DRY principle therefore demands encapsulation into a packed scene.

The UI

The UI is straightforward. There are two labels to track the points of the player and the enemy, a menu, and a mostly invisible countdown:

The UI of the game.

The corresponding node structure is:

The node structure of the UI scene.

Das meistens unsichtbare CountdownLabel besitzt einen Timer, der bei jeder neuen Runde gestartet wird. Das Label zeigt die verbleibende Zeit an und das timeout Signal wird in der Main node mit dem Ball verknüpft und startet ihn.

The Menu is again a packed scene and consists of a semi-transparent ColorRect, a Label for the game title, and a Button. When the button is pressed, the entire menu disappears, the score is reset (if a round has already been played to the end), and the countdown, and thus a new round, is started.

Takeaways

The most important takeaway for me is that I enjoy using Godot, and it provides me with the exact solution I was looking for in my initial projects. It seems powerful enough to not limit my ideas for a long time. At the same time, the entry is pleasant with pre-built nodes that allow for collisions and movement with very little code and effort, as well as signals that can link any objects together, and packed scenes that make modular, maintainable, and expandable structure very easy.

I didn’t take longer for the project than to write the blog post about it (which makes me doubt the idea of blogging). The main obstacle was a proper structure. As you may have noticed in the text above, I became aware of some inconsistencies while writing. And especially when it came to figuring out how to modularize things, I was often uncertain.

What helped me the most here was the realization of defining the API for a packed scene in its main node. The main node then communicates with the child nodes, and someone implementing the packed scene doesn’t need to know anything about the structure of the packed scene. The child nodes throw signals upwards. Either these can be finally processed directly in the main node, or the main node receives the signal and throws a corresponding signal that can be processed by the implementer of the packed scene. This loosely reminds me of the structure of the only frontend framework I know: Vue.

Problems

Refactoring can be a tedious task, especially in Godot, as it is not a fully-fledged IDE like Intellij or other JetBrains IDEs. It lacks automated refactoring tools to extract a code block into a method or rename variables. Moreover, although you can convert an existing branch of a node to a new packed scene, all signal connections with objects outside the new packed scene are lost, requiring manual reconnection. This can be time-consuming and discourages cleaning up code. While a Ryder Plugin exists for Godot scripting, it does not work with the latest Ryder version at the time of writing.

Another hurdle to enjoy cleaning up is that I am not yet sure how unit or integration tests could work. A brief research has confused me more than enlightened me, as it only dealt with C++ code. The lack of any tests takes away my confidence in making changes while still guaranteeing some functionality. Before starting a new, larger project, I definitely need to invest more research in this area.

What’s on the horizon

I am not finished with the game yet and plan to put a lot more work into it.

Release

This game in its current state is nothing more than a mere exercise. However, my plan is to turn it into a real game that is fun to play for at least 15 minutes and to release it in some form. The easiest option is probably itch.io, a platform for indie games where games can be downloaded for free or for a fee. The next step would be to see if a Steam release is realistic and to get to know that world. Finally, I would like to revise the controls to release the game on the Google Play Store. The goal is simply to learn all the relevant options for me.

The main idea and reason why the game is called Battle Pong, is the implementation of power-ups. Players should be able to collect various weapons to shoot the enemy paddle, making it easier to score points. Additionally, I am considering making the enemy paddle faster, making it necessary to successfully shoot it in order to score. Implementing power-ups will be the next concrete step in the game’s development.

Visuals

Overall, I have absolutely no idea about graphic design. However, the menu is particularly ugly and uninspired. I definitely want to spice it up a bit. Instead of a simple font, we might need a logo. And instead of a semi-transparent overlay, we could display something dynamic.

This would also be a direct upgrade for the background. I would like to have it not static anymore. For example, a slowly changing starry sky would be great.

Lastly, the paddles and ball look very clumsy. I don’t have any ideas yet, but I definitely want to revamp them before releasing the game.

Sound

Here, I actually already have an idea. A loop of a few basic chords in a key as continuous background music. And when the paddles hit, they don’t play my voice anymore, but a tone from the pentatonic scale of the corresponding key. I imagine that you could then play something like an un-rhythmic solo with the paddles.

I’ve never really produced music before, but I’ve been wanting to do it for a while now :)

Special Thanks

Special thanks go to sschellhoff for input concerning the structure of the game code. He actually even had more ideas, that I did not include yet. But before his comments, the main scene was a mess.