As we left it previously, I’d added a screenshot of a basic setup. The reality was actually a lot more complicated as to what had really happened there.
I’d already started setting up an object that was, for want of a better term, a level factory. It handled the creation of objects into the playfield on demand for the start of a new level, i.e. when bricks would be needed.
I put together a bunch of bricks, both in terms of sprites and in terms of Godot objects (or ‘scenes’ as it calls them, everything’s a collection of nodes which form a scene, and you make scenes out of other scenes)
But I’d already gone subconsciously old-school about this because I am old-school.
What I had done was create a structure that describes a level in code. Essentially defining that if I have 15 bricks across and maybe 6 rows down I can describe that with numbers, 0 is an empty space, 1 is a grey brick, 2 is a blue brick and so on.
[ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], ]
There, a level.
This also meant I could wire up any of the code needed for collisions on demand, too. Which was probably my second mistake.
I’d read the manual for Godot about signals and how they are used to allow objects to talk to each other. I’d also done the basic 2D tutorial which is a simple game and uses signals to indicate when a player meets an enemy, using the Area2D system.
This is quite logical in my head, the bat, the ball and the bricks are all definable by areas and the collision model certainly could be explained away in terms of areas overlapping. And in my head, this didn’t mean physics as such because I wasn’t describing anything in terms of forces or gravity or acceleration. My player was a simple object with a sprite and an Area2D collider that responded to the mouse (so no need for velocity, gravity etc), the ball was a simple object with a sprite and an Area2D collider, and I’d rigged up its movement each frame based on implementing a simple velocity vector (x/y change per frame), and the bricks… well… same idea. I’d also added some walls that worked the same way too, but no sprites, just colliders in the right places on the board.
It confused me at first that I’d explicitly have to wire up collision between the bricks by hand when instantiating them… luckily for me I could but something felt a bit off about this. Exactly how off it would feel… well, we’ll get to that.
But I’d wired it up, loosely gotten it to behave, the ball could detect collision with the bricks and the walls I’d put in, and it sorta worked. But I realised the collision detection wasn’t right, I wasn’t accounting for the various ways that the ball would move relative to the brick, e.g. bouncing off relative to the position on the brick (if you hit the right side of a brick, you bounce off horizontally while retaining vertical momentum)
And so I sat and wrote all this out. Sat there with a little diagram written in comments describing the 8 segments of a brick collision based on the ball’s relative position and what the bounce physics should be.
This nagged at me, though. Surely this couldn’t be right? Well, as it happened, no, but I didn’t realise that at the time.
The revelation came when I added the routine to speed the ball up over time. Easy enough, have some kind of timer running… of course I did my own in the process routine based on the delta rather than actually using the Timers in Godot. This, it turns out, would be a recurring theme of the project, doing things by hand that Godot can do for me.
What happened next… was not expected.
What’s happening here is that by this point, the ball is moving fast enough that it is able to skip through the collider of a given brick and into the next one at which point the collision is resolved for that later brick.
In short, my entire approach was wrong. I then discovered, through some frantic Google searches, that other collision models exist, namely the CharacterBody2D. Unlike a RigidBody where its movement is governed by physics and forces (like gravity which isn’t actually applicable here), a CharacterBody2D is governed primarily by code – I give things their velocity and I ask them if they have collided.
Overnight this was basically a revelation. This was a crucial missing piece of the jigsaw for me: of course you wouldn’t typically define signals for everything by hand, because you wouldn’t define this for bullets in a game, and you wouldn’t define the physics by hand unless you were doing something really weird.
This made the core game much more sensible and I swiftly refactored the whole brick/bat/ball setupinto CharacterBody2Ds, the walls into StaticBody2Ds and for the most part this worked as you’d expect. There were a couple of weird glitches with the bat under specific times but we’ll get to that properly in a later post.
The other neat thing about this is that I got to simplify all the handling of collisions. The ball would run the move_and_collide and it would know what it collided with. The only question becomes whether the bounce-ee wants to do something about it. It’s an easy thing to set up here, you’re given the object you’ve collided with, it’s an easy ask to see if it has a method called ‘hit’ or similar (like a signal but more direct and with no explicit wiring), if not assume we just do the default bounce with no further action.
This gets us the walls immediately and the bat, too. Bricks are a little more tricky, because we want to be able to do different things with the brick we’ve collided with depending on what type of brick it is – in all brick cases, it’s still going to bounce but it’s what we do with the brick in question. First step then is to give the brick code that ‘hit’ method and set it up to report back to say ‘well, I wanted to know about the collision but I haven’t done anything about the ball’ so the normal bounce can occur. After that it’s an easy step to determine whether it’s a non-destructible gold brick (just bounce), if it’s a silver brick (has it had enough bounces to be destroyed) or a regular brick (destroy it) and update the score. But a useful journey nonetheless about handling collision – and no doubt we will come back to this in a future post too.
Next post: powerups and information displays.