We’re going to start with one of the earliest video game archetypes, the ball and paddle game. It’s a dead simple genre, allowing us to examine mechanics and game elements in near isolation while still making a complete game.
I’ll be building games to provide this series with practical illustrative examples as we go. The posts will focus on the lesson in question, but patreon supporters will have access to the godot source files and annotated code for use as reference.
Component Architecture
One of Godot’s strengths is its architecture. Nodes are very modular, so I’ll be leaning into that with a component-based design pattern. You should be able to take the components I present here and adapt them to future projects with little modification, or use them as the basis for your ow…
We’re going to start with one of the earliest video game archetypes, the ball and paddle game. It’s a dead simple genre, allowing us to examine mechanics and game elements in near isolation while still making a complete game.
I’ll be building games to provide this series with practical illustrative examples as we go. The posts will focus on the lesson in question, but patreon supporters will have access to the godot source files and annotated code for use as reference.
Component Architecture
One of Godot’s strengths is its architecture. Nodes are very modular, so I’ll be leaning into that with a component-based design pattern. You should be able to take the components I present here and adapt them to future projects with little modification, or use them as the basis for your own structures.
Handball
We’re opening up a new project which I’m going to call Handball. In Godot’s settings, under Display/Window, I’m setting the viewport's width to 320, its height to 180, and itsoverrides to 1280 and 720. These are good values to use with pixel art games, even though I won’t technically be using pixel art.
Also, change the Stretch Mode to canvas_item.

Let’s start with a scene for the paddle. Since our core gameplay is “bounce a ball off of a paddle” we’re going to want to use the CharacterBody2D node – designed for user-controlled bodies that interact with godot’s physics. Select it from the menu under Other Node.

Our CharacterBody2D needs a visual representation. We could whip up some pixel art with shading and highlights but for prototyping it’s fastest to go with simple geometric shapes represented by the ColorRectnode. This displays a rectangle filled with solid color, useful for UI purposes as well as placeholders. Add one by using the plus symbol in the Scene Tree to add a new node, then select ColorRect from the popup menu.


This gives us a white square. As you can see by the purple lines representing our screen’s dimensions when we run the game, it’s quite large. We need something more manageable, so with the ColorRect selected in the Scene Tree take a look over at the Inspector. As you can see there’s a Color heading you can use to change the shade of our paddle.

To change its size, expand the Layout header and then Transform. The Size property lets you set its X width and Y height to a number of pixels of your choosing. I’m going to go with 4 x 20 but you can pick whatever feels right to you. This turns our paddle from a large square into a more manageable rectangle.
Now it has appearance but no mass – it has no presence in the physics engine, because that’s not what ColorRectdoes. What it needs is a CollisionShape2D – that’s why there’s a warning symbol next to our CharacterBody2D node.
Select the CharacterBody2D so it’s highlighted, then click the plus again to add a new node – this time a CollisionShape2D.

Does this sound familiar? It should, if you read my last godot lesson post. We’ve already gone over all this, but repetition helps things stick, so I’m explaining it again. In future lessons – as things get more complicated – I’ll probably omit the step-by-step process to save space.
Our CollsionShape2D has a Shape property in the Inspector. Click on the field where it says <empty> and select RectangleShape2D from the dropdown.

This gives us a draggable rectangle over in the 2D window, which we can drag to shape in such a way that it maps to the ColorRect‘s dimensions.

Let’s rename the scene to Player and save it to a player directory before we forget.
MoveComponent
Next up we’ll give the player the power to make the paddle move. This will take the form of two components – one to take the player input, the other to actually move the paddle. Why two? Well, we want to make sure that each component does only one thing – this keeps them modular. We’ll be able to use the move component in CPU controlled paddles without having to trim away the player input elements.
Not that Handball is going to have CPU paddles! But this is the nice thing about components – if I wanted to make a game that was closer to table tennis, I could reuse a lot of what we’re building here. Or a brick breaking game. Or a fixed-screen shooter.
The movement component we’re building can be used with any game that uses 2d movement.
So let’s start with that, then work on the player input component.
First, let’s create a components directory in our FileSystem. It’ll keep us organized, and is a great habit to build for larger projects. Right click on the new directory and select Create New>Script. We’re creating a script that is not, initially, tied to a node. Call it move_component.gd.

Double-click it to bring it up in the Script editor. Not a lot in here now, so let’s start by adding a line at the very top (above extends Node) to give it the class name of MoveComponent.

This does not make our new component a class. All scripts are technically classes. And if you remember from lesson zero, a script is just a way of extending the current behavior of a node. So by giving MoveComponent a name, we are in effect creating a new node type, like ColorRect or CollisionShape2D.
What we want this component to do is simple – it will apply velocity to the node we point it at. There are a few ways we can make that work – for example, we could tell it to apply movement velocity to its immediate parent. That would force us to make this component a child of whatever node we were trying to move, and that might not always be desired.
Instead, we’re going to create an export variable that points to our subject node.

@exportis a keyword that makes the property that follows (here, a Node2D variable named* subject*) appear in the Inspector. This lets us, when we create one of our shiny new MoveComponentnodes, drag and drop a Node2D into the inspector to tell Godot that that node is the subject we’re going to try and move.
After the colon – where it says Node2D – we’re telling Godot not to accept any values that aren’t Node2D nodes (or that don’t inherit from Node2D nodes). So we can’t accidentally give it a value or node that is incompatible.
We create a second variable, velocity, to store our velocity as a 2-dimensional vector – our movement along the x and y coordinates, giving us an angled direction of a given length.

Finally we’re going to add a _physics_process() method. If you recall from lesson zero, _physics_process() is called every “tick” of the physics engine. So this code will run continuously.

First, we set the subject’s velocity to our velocity. This is necessary because our input component will be sending player input to us first, not *our* subject.
Second, it calls our subject’s move_and_slide() method. This is another built in method native to CharacterBody2D that takes its current velocity and updates its positioning while accounting for collisions and a whole bunch of other stuff. (There is also a move_and_collide() method that returns more information about those collisions but we won’t need it on our paddle).
But wait! What if we don’t have a move_and_slide() method? What if our subject isn’t a CharacterBody2D because we don’t need complex physics interactions? We’d need to translate our location manually. We could build a separate MoveComponentclass for those cases, or add a simple conditional instead.

This time, if we’re not a CharacterBody2D, we instead use the subject’s translate() method to multiply our velocity by delta.
(Delta is “how long since the last physics frame” in microseconds, so multiplying by delta keeps our action cpu speed independent; we don’t need to do this with move_and_slide() because it does that for us.)
This gives us the following script for our MoveComponent:
class_name MoveComponent
extends Node
var velocity : Vector2
@export var subject: Node2D
func _physics_process(_delta: float) ->void:
if subject.is_class("CharacterBody2D"):
subject.velocity = velocity
subject.move_and_slide()
return
else:
subject.translate(velocity * _delta)
We can now add our MoveComponent as a new node to our scene tree.

It’s pretty much plug n’ play – you simply need to drag the node it’ll be moving (Player, in this case) over into the Subject property of the inspector.

InputComponent
Next we’ll work on the MoveInput component. First step is going to be opening up our project settings and switching to the input map tab.
We only need to define two input actions – move_up and move_down. I’ll do so, allowing the player to use W, the up arrow, d-pad, or analog stick for up, and D or down or the pad or stick for down.

As an afterthought, we can add a third action – serve – with space or any of a game-pad action buttons.
Now we can add our input_component.gd script into the components directory. We’ll give it the class name InputComponent.
What we’re going to want to do is take the player’s input and pass that as a vector to the player’s MoveComponent node, so the first thing we’ll need is an @export variable pointing to the player node’s input component. We’ll add a second @export variable for our paddle’s movement speed.

Why is speed an @export? Well, this makes it easier to test. We can tweak the speed in the Inspector while testing the game and adjust the value and see the changes immediately.
Just to put it out there, we’re typing the speed variable as an integer so we can’t accidentally input a floating point number or string or something as our speed. It will always be a whole number, and we can only drag over a MoveComponent node into our move_component variable.
The body of our component is going to be the _input() method. Like _physics_process(), _input() is built in, and it stores the latest input from the user as an InputEvent.

To walk you through it, Input.get_axis() gets the relative axis between two input actions, here “move_up” and “move_down” as we set them in our Input Map. This results in a number between -1 and 1 – it’s -1 if we’re pressing up, and 1 for down, or a decimal between the two if we’re using our analog stick. This is stored in the direction variable, which gets a zero if we’re not moving.
We take this number and multiply it by our speed variable. If we’re not moving, we make sure the number is zeroed out. Whatever the result, we pass that along to our MoveComponent's velocity along the y axis, giving us vertical movement.
Once saved we can add an InputComponent as a node to our Player scene tree. Don’t forget to drag your MoveComponent from the scene tree into your Move Component property in the inspector.

Stage
In order to test this, we’re going to need to make a playfield. Create a new scene using the plus icon next to the player scene.

Choose 2D Scene to make the root aNode2D, and switch over to the 2D view in the viewport. We’re not doing much here, just creating an instance of our Player scene as a node. Click the chain icon in the upper left of our scene tree.

Choose player.tscn from the popup menu. This creates a copy – or an instance – of our entire Player node as a child of Node2D. While we’re at it, rename Node2D to Stage or World or something, then save it.
You can see that when we added our Player instance, it was created in the upper-right corner of the screen in the 2D view. Use the move tool and drag it away from the left side, and down a bit so we can see it clearly.

Let’s test to make sure our components are working. Click the Run Current Scene button in the upper right corner (or hit F6).

If you’re throwing errors here, make sure you dragged the Player node into your Move Component’s subject property, and your MoveComponent node into your Input Component’s Move Component property.
You should now be able to move the paddle up and down using the input keys we’ve specified. Nothing keeps us from floating off the screen, so let’s add some walls.
There are a few methods we could use for this, but the simplest will be to make each wall a StaticBody2D node. This is a physics body that won’t be moved by external forces. With Stage highlighted, add a new node and select StaticBody2D.
To give it a shape we’ll need to give it a CollisionShape2D child node, and to give it an appearance we’ll need a ColorRect node. The CollisionShape2D node and ColorRect will be siblings – that is, at the same level of hierarchy below StaticBody2D. Don’t forget to give theCollisionShape2D a RectangleShape.

Let’s shape both so that they form the top wall of our handball court.

Now do the same for the bottom and right wall. We won’t need a left wall.

Now, if we test the stage scene, our paddle will stop when we hit the top and bottom walls.
The Ball
Our third major moving component is going to be the ball. Let’s create it as a new scene – a CharacterBody2Droot node with a ColorRect andCollisionShape2D Rectangle shape of 4 pixels by 4 pixels in size.
We won’t be using our Move Component for the ball. Instead we’ll be adding a script to the root CharacterBody2D node using that button that looks like a scroll with a green plus sign.

This brings us to our script window, where we’re going to create a few export variables to govern how much we accelerate per bounce. That’s why we can’t just use godot’s built in physics – real world physics would demand that an object lose energy each bounce with friction and transference. Instead, to make a good ball-and-paddle game, we want to get faster and faster with each bounce.
So let’s create our variables – speed_factor and acceleration.

var speed_factor : float = 1.0var acceleration : float = 0.05
Note that these values are floats, not integers.
In our_physics_process() method – which, remember, is called every cycle of the physics engine – we want to both move the ball and check for collisions with our paddle or the walls. We can do both with the built-in move_and_collide() method.
What’s the difference? Well, move_and_slide() handles a lot of the physics for us – it’s a special version of move_and_collide() that calculates slide vectors. We’re going to use move_and_collidebecause in addition to moving our ball, it’ll also report information on the collision involved, which helps us to calculate our return angle.

We call move_and_collide() storing the data it returns in a variable called collision, and passing to the method the product of our current velocity, speed factor, and delta – and remember, delta is the time that’s passed since the last tick of the physics engine, which helps us stay cpu-speed independent.

Now we check to see if we hit something. I’m not going to dig into the math of it here, but basically collision.get_normal() figures out the angle of impact of the collision we just had. We then use thebounce() method to apply that as a bounce to our current velocity.

We also get a little faster, increasing our speed_factor by our acceleration.
func _physics_process(delta: float) -> void:
var collision = move_and_collide(velocity * delta * speed_factor)
if collision:
velocity = velocity.bounce(collision.get_normal())
speed_factor += speed_factor * acceleration
If we tried to test our ball it’d just sit there because it doesn’t have an initial velocity… and as we all know, an object at rest tends to stay at rest.
So let’s add some code to our_ready()method. This is (yet) another method that runs automatically – this time, it runs when a copy of the node is created. We’ll use it to call a function of our own creation, ball_reset(), so we can use the same code when we need a new ball.

func ball_reset()->void:
pass
Now for the ball_reset() function itself.

First, we drop it at position 160x 90y, the center of the screen.
Then, we use the randi_range()method to assign its vertical and horizontal velocity a random angle.

And finally, we return its speed_factor to 1.

func ball_reset():
position = Vector2(160,90)
velocity.x = randi_range(-100,30)
velocity.y = randi_range(-30,100)
speed_factor = 1
Now create an instance of the ball scene in your stage and test it out.
A few problems jump out at us. First, if we miss the ball, it goes on forever off-screen. We’ll handle that in a second. More rarely, the ball can get “stuck” in a corner or bouncing between the top and bottom wall in a straight line forever.
This is easily handled with a short bit of code in the ball’s _physics_process() method to give it a little nudge if it seems to be stuck.

func _physics_process(delta: float) -> void:
var collision = move_and_collide(velocity * delta * speed_factor)
if collision: # if we hit something:
velocity = velocity.bounce(collision.get_normal())
speed_factor += speed_factor * acceleration
if velocity.y > 0 and velocity.y < 5:
velocity.y = 20
if velocity.x == 0:
velocity.x = -20
To handle the ball going off-screen, we’re adding a new node to the Ball scene – VisibleOnScreenNotifier. What this does is send a signal if the ball exits the visible screen.
Where does it send the signal? Wherever we tell it.
In the Inspector switch to the Node tab, and then connect the screen_exited() signal to the Ball node script using the Connect button at the bottom.

This creates a new function at the bottom of the ball script, _on_visible_on_screen_notifier_2d_screen_exited(). In the function’s body we’re going to make a call to our ball_reset() function… and that’s it.

func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
ball_reset()
Now, when the ball goes off screen, it should reset to the middle.
One final bug that doesn’t come up terribly often. Because the ball and paddle are both physics objects, occasionally the ball will hit the paddle in such a way that it gets knocked back a bit, potentially even off-screen. To prevent this, we can add a script to our Player node.
What we’ll do is record its starting origin, then every tick of its physics_process reset its x coordinate to that origin, keeping it from moving horizontally.

extends CharacterBody2D
@onready var origin: Vector2
func _ready() -> void:
origin = position
func _physics_process(delta: float) -> void:
position.x = origin.x
Of course we need a way to track and display score, but UI is a huge topic that deserves its own post and this one is long enough as-is. You can see the finished Handball game below.
Want a copy of the project files to study, complete with annotated scripts? Support me on patreon, and you’ll get a download link for a zipped archive, along with a downloadable version of the handball game.
What to cover next… we could dive into UI with a game made entirely from buttons. Or we can add a second axis to our movement with a top down game about dodging. Or we could add gravity to our Handball game and make something more like Hackeysack. Let me now in the comments if there’s something in particular you’d like to see.