BreakoutGdx Week 5: Livin' Large in Phase 2
So this week we get the good and bad news in one package. A ton got accomplished this week. And I mean a ton. Far, far more then I’d ever be able to cover in just one post. I took the week off of work (for reasons unrelated to the game), and since I found myself with some spare time I spent most afternoons working through features, and it turns out you can accomplish a whole lot in an afternoon if your mind is clear and if you have the afternoon distraction free to work on stuff. There’s a list of features/fixes/additions here, because I know I won’t be able to talk through them all in detail:
- Refactored ball/paddle physics so that it works far more smoothly. Additionally, gave the ball a “speed limit” and a “speed minimum”, so the balls linear speed vector will always have a magnitude between these two values.
- Added powerups to the game. Powerups drop from powerup bricks and give the player extra advantages. Currently implemented powerups are 1-Up, which grants the player an extra life, and Multiball, which spawns another ball into the game.
- Added ToughBricks. These bricks take more then one hit to kill, and have separate sprites depending on how many lives they have remaining. ToughBricks can have between 1-4 initial lives.
- Added multi-level functionality, as well as added ten game levels. Game now has levels associated with it, a level loader, and a level manager. Levels are defined via CSV files definining the brick order from left to right. Game can also have a configurable amount of levels in it. When the 10th level is beaten, the game loops back to the 1st level and calls it the 11th level. Therefore the game will keep going until you run out of lives.
- Embellished the GameOver and LevelComplete screens a little. The GameOver screen now gives you what your final score was, and the LevelComplete screen now reads “Level __ Clear!!!”. Also, GameScreen now displays the players current score in the top right corner, and the current level in the top left corner.
- Added ability to toggle ScreenShake on and off, since my play tester (aka my wife) mentioned that she thought that was a cool feature, but that she didn’t really like it.
- Modified how game reset functionality works so that it resets the existing GameMaster, instead of scrapping everything and creating it from scratch. It was necessary to figure this out to get multi level functionality to work anyway, and it saves the GC from having to clean up extra dereferenced objects.
- Added a background to the game, so it’s not just bricks on a black background. This background exists on all screens (Menu, Preferences, and GameScreen). Getting this to work exactly right in all circumstances (eg. resize events between various screens, etc), took longer then I expected, but the addition of this was actually really cool. It gave the game far more “pizzazz” then I thought it would.
- Got the HTML5 version of the game working. Game will now work inside the browser. Created a ‘GameType’ enum that is passed to the core project from the launcher, and platform specific features are added/removed based on what version of the game is running. This took awhile, but was AWESOME to see up and running. Additionally, the Jenkins CI pipeline now compiles both JAR and HTML5 artifacts, retaining both of them and packaging them in a single zip file for storage.
- Added ‘ball start’ functionality. Ball now starts “attached” to the paddle. Upon the player pressing the left or right mouse button, the ball will “shoot” in either the left or right direction depending on the last direction the paddle was moving before the player triggered the ball.
- Removed development debug features, such as the ability to zoom the camera in and out, and arbitrarily add balls to the map.
For comparison purposes, I’ve included gifs of version 0.0.3 of the game from last week and 0.0.4 of the game from this week, below.
Let’s start with the ball-paddle physics interactions. In version 0.0.3 and previous I relied entirely on Box2D to handle our physics interactions for us. This was sub par, ultimately because Box2D is trying to model a semi realistic world, and we want most of our interactions to be anything but realistic. It worked good for ball-brick and ball-wall interactions, as those are static objects, but when it came to ball-paddle interactions we needed Box2D to handle interactions between two dynamic bodies in a way that worked for us. I had been setting the “density” of the ball to zero (therefore making the ball a massless object), and the density of the paddle to a really high number. Additionally I set the “restitution” of the ball to 1.0 which should have made any interaction perfectly elastic. But this doesn’t quite work for real physical calculations. For instance, say a paddle with some mass, hits a ball with mass zero. In that scenario, the ball would have infinite acceleration applied to it, which isn’t ideal. So it seems that even if you set the density to zero, when Box2D does it’s calculations it gives that ball a little mass, if only to make the resultant numbers calculable.
So what does that mean for us? It means that the ball would gradually slow down throughout a play session, and the paddle would gradually get lower as it had this mass strike it. This wasn’t ideal. Also, the ball wasn’t very controllable by the player, which also wasn’t good. Finally, if you “rammed” the paddle into the ball, the ball would fly off at ludicrous speed, also not great. We wanted our interactions to follow a symmetric, repeatable behaviour, we wanted the ball to not acclerate to ludicrous speed, we wanted the ball to not slow down throughout a play session, no matter how many times it had been hit, and we wanted the paddle to not change it’s Y position, no matter how many times it had been hit.
To achieve this, I went back to the GameContactListener, and began handling the interactions manually by setting the balls linear velocity upon a paddle strike. I wanted the player to have some control over the ball, so I determined that if the player hit the ball with the right 25% of the paddle, the ball should move to the upper right (regardless of it’s original velocity vector). If the paddle hit the ball on the left 25% of the paddle, it should move to the upper left, and if the ball hit the paddle anywhere else, it should reflect it’s velocity so that it was travelling back upwards on the Y axis, leaving the X component unchanged. Whenever the GameContactListener detects that a ball-paddle collision has occured, it notifies the BallManager of such, passing the index of the ball that was involved in the collision. The ball manager then gets the position of the paddle body, the position of the ball in question, and does basic arithmetic based on these numbers and the width of the paddle to determine what the ball should do, and then it directs the ball in question to change it’s velocity to do that.
This still left the issue of the balls sometimes moving too quickly or too slowly however. I chewed on this problem for awhile before I realized that the balls linear velocity is just a vector. The vector has an X component and a Y component, but these can also be represented as direction and magnitude. I defined some static constant floats that represented what the ball “speed limit” was, that was the maximum velocity that a ball could travel, and what the ball “speed minimum” was, that is the minimum velocity a ball could be moving at before we manually sped it up. Then I built some logic into the ball update cycle that applied these restrictions. A screenshot of the speed control code is below.
As you can see, at each update cycle the ball itself looks at the magnitude of it’s velocity vector. If the vector is too large or too small, we determine a “scaling factor” that would be needed to get the ball back to the speed limit. Once calculated, we then perform scalar multiplication on the balls linear velocity vector to get the balls speed back within the constraints.
There’s not a whole lot to say regarding the powerup feature. I made a PowerupManager that handles all powerups that exist in the game (spawning them in when directed to, and dereferencing them when they fall below the game’s screen. What powerup is generated when one is commanded to spawn into the game is random. One unique thing about the powerups, they are not Box2D bodies. We don’t actually want the powerup to be a Box2D body, as we just want it to fall through the map (and fall through any ball or brick bodies that may exist in it’s coordinates), unless it comes into contact with the Paddle. To accomplish this, on every update cycle each powerup object gets the bounding box that encapsulates the paddle, and the bounding box that encapsulates it’s own powerup sprite, and checks to see if there is overlap between those bounding boxes. If there is, then it triggers the GameMaster to apply whatever effect that powerup should give the player, and then it destroys itself.
Multi level functionality was kind of a fun feature. I implemented a LevelManager that controls loading in levels, and implemented some functionality into the BrickManager so that it can build levels upon direction from the LevelManager. The levels themselves just consist of CSV files, and I built a small parser in the levelmanager that knows how to parse these. So the level loading has two parts to it:
- At the start of the game when the application starts, the level manager loads in a “game definition file”. This file consists of a comma separated list of paths to “level files”. The levelmanager keeps an iterator in memory of what level the player is on, and can load in these level files on command.
- When directed to load in a level file, the levelmanager loads it in and then ticks up it’s iterator. When parsing the level file, it matches each string within to a BrickType enum value, and stores the resultant deserialized file as an array of BrickType enums. It then passes this array to the BrickManager, who then builds the actual game board with that information.
The fun part of the level functionality was that, once this was built, I had a fairly extensible framwork to build levels within. So I pulled out some graph paper, designed ten levels, and made level files for each of them.
Moving onto the game background. On the face of it this doesn’t seem like a large feature, and really it wasn’t, but I had quite a few bugs in it’s implementation that I wanted to talk about. Early in the project we made the decision that the game should use a “FitViewport”, that letterboxed the game if it was expanded/shrunk so that it maintained the correct aspect ratio. This was absolutely the right decision for the game itself, but for the background I wanted an asset that would stretch or shrink to fill the screen. So the effect should be letterboxing with our background, and we should have no black letterboxing anywhere. Also, the background should be applied the same way across any screen in the game, so that at one screen size you could switch between the various screens and the background should look like it was staying the same.
To accomplish this for the GameScreen, I made an instance of an ExtendViewport that was used to display the background, and in the game’s render method we switch between the ExtendViewport and the FitViewport so that all the game objects follow the FitViewport, and the background follows the ExtendViewport. This worked great, and having figured that out I then attempted to do the same thing with the other screens, and this did not work at all. The issue is that Scene2D is doing a lot of camera work for us already, and that didn’t mesh well with us throwing a separate sprite batch and viewport into the mix, and trying to render other things. However, I did find some functionality for applying backgrounds within Scene2D itself (setting the background element for elements within the stage), and that actually implemented the functionality that I was looking for.
Finally the last thing to talk about, the HTML5 cross compilation. To start with this feature, I went back into the project’s commit history and found where I had removed all of the old HTML5 code, and I readded that. I had to change the way I implemented a couple of features. If you’re using the HTML5 GWT compilation, you really need to limit yourself mostly to methods that LibGDX exposes. I had been using java.util.Scanners for a couple of things in the LevelManager, that broke the HTML5 build, and I had also been using String.format in a couple of places. Upon refactoring these and building, I was presented with a completely black screen. Everything compiled fine, but nothing rendered/worked at all. I Googled a couple of errors that were showing up in the browsers console output, but couldn’t find anything definitive. This was quite the bug to track down. The thing it ultimately ended up being was the TiledMapHelper object, with it’s loading/parsing of the tmx tiled object map that I had made with the Tiled map creator tool. It turns out that by default this tool stores the map string as a base64 encoded, compressed, “string”. This works totally fine within the desktop version of the project, but loading it involves uncompressing that “string”. The browser cannot do this. With the amount of time it took to track this down, I’m gonna make a bullet of this rule for future me:
If you are going to cross compile your game with HTML5, and if you are using a Tiled map in your game, you absolutely, absolutely must configure Tiled so that it exports the map as an UNCOMPRESSED, base64 encoded XML file.
Outside of that, I just ran into little nit picky restrictions that browsers have that you wouldn’t think of if you weren’t running your application in the browser. For instance, I had previously been having my game music start up immediately upon launching the application. Both Chrome and Firefox blocked this right away, as they have a rule that the application shouldn’t be able to play sound unless the user has interacted with it (to protect against AdWare). I worked around a number of these little gotchas until the experience both felt nice and was mirrored between both desktop and HTML versions of the game.
With that I think the game (or at least v1.0 of the game, I doubt I’ll revisit it for a v1.1 but I like to keep my options open) is feature complete. I’m playing around with a couple ideas (not gameplay related, more application related) that I may add this week. For example, I’m considering making a credits/attributions screen that lists the sources of the various assets I used (not necessary per the license for any asset I used, but maybe good to include anyway). I plan on considering/playing around with a couple of ideas this week, maybe implementing them, and then actually putting the game on itch.io next weekend.
Sorry for the wall of text. Like I said, a ton (even more then I talked about here) got accomplished this week. It’s been quite a good learning experience!