This is a part of a series of notes. You can find notes to other lectures here Please feel free to bring to my attention mistakes, other interesting resources and feedback via the comments section. I’m all ears and will do my best to incorporate them into the blog post.
Resources
- Slices in Python.
- Why Use Sprite Sheets? - Link 1, Link 2.
- Intro and Exercises in Particle Systems - This is awesome.
- ASCII Explained
- Folders Under AppData in Windows
- ./local/share/ in Linux
- Collision Detection for Fast Objects.
Breakout State Flow
Quick overview of possible movements between states. It’s quite clear from slide#7 that it is possible to go to the HighScoreState from the StartState and vice versa but it’s not possible to directly enter the PaddleSelectState from the HighScoreState. He also briefly explains what happens in every state. For instance if the ball goes below the paddle and the player loses a life, the game transitions from the PlayState to the ServeState. I don’t want to go into these details for the sake of brevity. Essentially you draw a detailed state diagram to get a better picture of the flow. It’ll also help you refactor your code better. To know more use cases of a state diagram see Section Pong-9
of Lecture Pong
breakout0 “The Day-0 Update”
Organise your project into folders. See slide#9 and you’ll get a clear idea.
Global tables are initialised for fonts, textures and music. Within the music table, you notice that the background music is separated from the other audio files by an extra line break (line 85 of Main.lua). He mentions that using a ResourceManager
class is a better way ahead.
A screen is setup like the usual lectures. Instead of having the constants in the same file (VIRTUAL_WIDTH
and VIRTUAL_HEIGHT
) all the constants are moved to src/constants.lua
.
A state machine is setup and StartState
is entered into. Like last lecture a global input table is setup (for keypresses). A function displayFPS()
is written so that the frame rate is visible on the screen instead of having to see the terminal. It’s a common practice to display FPS on the screen during development. You can read more about it in my notes on Pong.
Instead of having to require multiple files repeatedly a src/Dependency.lua
is used. It imports all dependencies internally and by importing it, you implicitly import other dependencies. All states are moved to src/states
to make it modular. It might seem like a lot of folders at first but it simplifies and keeps things organised especially in larger games.
On line 181, you will notice the background image being rendered. The virtual heights and widths are being divided by the height and width of the background image to get the scaling factor. Also notice that 1 is subtracted from the background image’s height and width. This creates a larger than necessary scaling factor. Ensures that the image is stretched and always fills the screen.
breakout1 “The Quad Update”
local highlighted
in state/StartState.lua
is used to keep track of the highlighted menu option (start or highscore). It is set to 1 to highlight Start by default.
Lines 23 - 28 => the highlight variable is toggled between 1 and 2 every time the user presses the up or the down key. In this case we only have two menu options and can simply toggle. In other cases we’d have to increment and decrement highlight by 1 on up and down press respectively.
Escape key presses aren’t global anymore. Pressing the escape button in some states switches to the previous state (acts like the back button).
The highlighted option is given a different colour. Remember that Love2D is kind of like a state machine and uses the last set colour for rendering text, so we explicitly set the colour every time we want to render something. Also see line 53, the last of the four 255 sets the alpha of the colour. Setting it to 255 makes it as opaque as possible. The value can be tweaked to give your text tints.
Sprite Sheets
Instead of having multiple image files for each of your asset, put them all in one giant image. We can then load the single file into memory and get parts of the drawable (image) and render them. Basically select a sub portion of the image and ask Love to render it at your desired location. See slide#11 and 12. Sprite atlas is a synonym for sprite sheet.
Check slide#13 for the function definitions.
There are tools to generate quads (parts of sprites which contain images). They prove to be useful when you have image files with many sprites especially when the dimensions of the sprites are uneven. They can help you generate dimensions to get the respective sprites.
Every file from this point onwards will be required by src/Dependencies.lua
.
Util.lua => has a function to generate quads. Basically iterate over the atlas and divide them into tiles of equal width and height. The code is pretty straightforward. It stores the quads in a table and returns the table.
On line 42 => Colton has defined a helper function that is similar to slicing in Python. Lua doesn’t provide such an option by default. On line 45 is the meat of the function, if starting point isn’t mentioned, it starts from the beginning, if the end isn’t mentioned it goes on till the end (represented by #tbl
. # gives the size of the table). The third argument of a for loop in Lua increments the variable by that much (if you’ve used loop in other languages, you probably have used i++ or i = i + 2). It’s just that. So in short it’s a typical for loop in a language like C, JavaScript or C++.
Line 57 => defines a generateQuadPaddles
to get the paddles. Notice the image file and see how they are spread across. Each block is 16 px tall so we start y at 64 and x at 0 so that we get the first paddle on the 4th row. Adjustments are made to get the paddles on the left as well. There are 4 such rows, instead of having to repeat the logic, a loop is used, y is incremented by 32 to index new rows and x is set to 0 to get the first paddle. They are stored in a table as usual and returned.
Line 65 => of Main.lua you can see the function (also pass in the main sprite sheet to the function) in use. A global table gFrames
is defined. We store all the quads that pertain to paddles in this global table.
The Paddle class has variables skin
and size
that keep track of the colour and size. If you notice, we load the paddle quads in the order in which they appear in the sprite sheet. There are 4 sprites of each colour and there are 4 colours. The second paddle of the 3rd colour can be accessed from the table by indexing #10 from the table (2 colours skipped and 1 skip from the 3rd colour so ((2 x 4) + 2)
). The other bits should make you feel right at home. Very similar to the previous lectures (getting input, updating x coordinate, containing movement within the boundaries and render (this time using quads instead of the entire texture)).
PlayState.lua line 20 => A new paddle is initialised. It also has logic to play and pause the game.
Also by generating quads there’s one less headache now. No need to name, sort and organise images. We can simply load then in one table and them random access them from the table using indices.
Check resources to know about Python slices and the advantages of using sprite sheets.
breakout2 “The Bounce Update”
In Util.lua a new function GenerateQuadBalls
is defined. It generates the quads pertaining to the balls from the atlas. It’s very similar in fashion to how quads are generated for the paddles. Notice in the sprite sheet (slide#12) that each ball is around 8px in height.
A global table is used for all quads. Colton says it’s more organised and neater.
Ball objects are initialised with a skin. It also implements a generic AABB collision. It can take a target (any drawable) and check if the ball collides with it or not. An instance the Ball class can get easily get the required quad by gFrames['Balls'][self.skin]
. A random number is passed while initialising the ball so that it gets a random colour.
Ball.lua line 68 => Walls start deflecting the ball now. If the ball hits the top, we let the X velocity be and just reverse Y velocity. If we hit the left or right edge, the Y velocity remains, just reverse X velocity. Recall in Pong we actually had to reset the position during a collision else we’d risk getting stuck in an infinite loop. The case where the ball hits the paddle at a weird angle is yet to be fixed.
PlayState.lua => Initialises a ball, checks for collisions with the paddle.
breakout3 “The Brick Update”
generateQuadBricks()
of Util.lua is used in the gFrames
table. It internally uses the GenerateQuads
function to split the atlas into dimensions of given width and height. We pass in the width and height of the brick. You probably figured out that the function would return many more quads than required (that too wrong ones) as we use a crude approach to split the sprite sheet using the dimensions of the brick. The quads it returns are no doubt uneven and pertain to the entire sprite sheet but we don’t care. We’ll slice it to get the first 21 (the bricks) and the rest will be discarded. This is a one time thing that happens during the start of the game and it really doesn’t matter even if it’s a little inefficient.
Bricks have a member variable called inPlay
. It’s a flag that identifies if it’s alive or not. Using this flag is a simple workaround for having to deallocate the brick (if it’s hit, it probably needs to be removed from the table that holds all bricks during gameplay). This flag is used with an if condition before updating and rendering (we selectively render and update bricks that are still inPlay). For a small game this approach works. For large games we’d have to deallocate memory. On brick hits, we just have to set inPlay to false.
Line 49 => We index the appropriate colour and tier (bricks of varying health or strength levels, differentiated by colours) like how we did for paddles to get the right quad from the quad table (offsetting by multiplication and addition).
PlayState.lua line 34 => LevelMaker.createMap()
a function to create a table of bricks to be used anywhere. Additionally we also iterate over all the bricks and check if the individual brick collides with the ball. We also render bricks selectively using the flag described above.
LevelMaker.lua line 26 - 47 => All it does now is randomly choose the number of rows and columns and places the bricks along the correct coordinates to center align all the bricks. It’s pretty simply as all the bricks are of the same colour and it fills the entire grid with bricks. Lines 34 to 43 are the most interesting lines in this file as of now, it initialises Bricks after taking top and left padding into consideration. The lines are well commented and are easy to understand. Fancy patterns and colours will be covered in eventual updates.
breakout4 “The Collision Update”
Currently the ball just passes through a brick. It should deflect after hitting a brick. In addition we also need to know where the ball is coming from, so that we can get it to bounce in the right direction. (If the ball hits a brick from the left, we just deflect it towards the right etc). We need to find out which portion of the ball isn’t inside the brick and we’ll also be using dx to know the direction it was travelling in before hitting the brick.
When the ball comes in contact with the corner of a paddle, it should take a sharp turn or cut. However if it hits an area near the center, it shouldn’t deflect by much of an angle. See slide #17. A good measure for this intensity is the distance from the center of the paddle.
PlayState.lua line 64 - 71 => There are two if else branches which we’ll go over one by one.
-
self.paddle.dx < 0
dx not being zero indicates that the paddle is in motion. It being less than 0 indicates that it is moving towards the left.self.ball.x < self.paddle.x + (self.paddle.width/2)
is used to check if the ball’s x coordinate is less than the x coordinate of the center of the paddle. The second line sets a dx of 50 in the left along with 8 times the distance of the ball from the center. Distance from the center is given by(self.paddle.x + self.paddle.width /2 - self.ball.x)
. The 8 times creates the amplification (sharper angle and faster speed) effect. -
The next else if branch is very similar. He’s just made changes to directions (dx should be positive for instance) and used
math.abs
to convert the negative difference between the center and the ball to a positive dx.
For deflecting the ball off the brick in the right direction we use a simple rule based system. It’s simple but works most of the times (only fails if it hits the brick at weird angles). For the sake of time and brevity Colton decided to use this approach. See slide #18 for a picture and some more details. He pointed to another tutorial that is quite complex but solves this problem accurately. Relevant links to those tutorials - this and this.
PlayState.lua Line 80 => checks if a Brick collides with a Ball. Lines 95 - 121 take care of deflecting the ball in the right direction. There are 4 branches in the if else ladder. Let’s go over them one by one.
- Line 95 => checks if the ball’s x coordinate it less than the brick (meaning the ball is approaching the brick from the left). Notice
self.ball.x + 2 < brick.x
, 2 is added to the ball’s x coordinate to account for edge collisions (top left or bottom left corner of the brick). If the ball hits say the bottom left corner it gives more priority to the coming from right. The jist lies inself.ball.x < brick.x
, the + 2 is a small fix. - 98 & 99 => If the above condition is true, just reverse the velocity of the ball and shift it outside to avoid pesky infinite loops (recall these infinite loops were covered in Lecture 01 - Pong. We subtract 8 because that’s the width of the ball and moves it away from the brick. We could even use
self.ball.width
instead. - Line 102 =>
self.ball.x + 6
(same offset of 2 but on the right, should have beenself.ball.x + 8
but we reduce 2 being the fix for edges and it becomes + 6). We shift by 32 as the brick is 32 units broad. - Similar treatment for top and bottom collisions. Reverse dy instead of dx and move by 8 and 16 units (the brick is 16 units tall).
- Line 124 => Increase the speed of the ball with each hit.
These collision detection rules will fail once dx or dy becomes too high (the ball moves a lot each frame, one frame it might be outside a brick and in the next frame it could have traversed multiple bricks and get past them. Recall that we test for detection every frame, since the ball isn’t in contact with a brick, we won’t be able to detect the collision). It just skips over them.
Colton explains a better way to detect collisions at the cost of more resources. Jist of the better idea is as follows.
- Detect collisions along the entire path travelled by the ball between the last frame and current frame. You have dx and dy so go backward to the last frame.
- From there on make small additions to the ball’s x and y values until you touch dx and dy (i.e reach the position in the latest frame after dx and dy are added). Keep checking for collisions after these small additions that is.
- This way you’ll ensure that you don’t skip through objects.
Check resources for the link on detecting collisions when an object is too fast.
breakout5 “The Hearts Update”
StartState.lua 35 - 40 => Switch to the serve state if option 1 is selected from the start state. Pass in health, the paddle, the score and bricks.
Only the assets are global in this game. Every state is passed variables that it needs. Recall our state machine can take in a table. Even web frameworks like React work in a similar fashion. It passes the required data to the state instead of maintaining globals. Keep in mind that if a certain state requires some data, the present state which transitions into that certain state will also need to have access to that data. So you might have to pass data via various states so that data can trickle down to that certain state.
The serve and play state probably need bricks, a paddle, hearts for indicating life etc but the high score state needs only the highscore. By passing in what’s required rather than using globals you keep stuff clean and encapsulated. You also get a bird eye view of what each state needs.
ServeState.lua 39 => On state transition we additionally pass in the health as well.
Main.lua
- Has quads for hearts. It was generated by splitting an image evenly (recall how we split the bricks).
- 208 => Function to render the hearts. It first renders the hearts the user has left and then renders the hearts he’s lost. To render the hollow hearts (missing health) the difference between total lives and lives held is taken, so if the user hasn’t lost any life the for loop fails in it’s very first iteration.
PlayState.lua
- 81 => Every brick hit simply add 10 to the score. Will be improved in the future updates.
- 132 => Check if the ball goes below the screen, reduce health then switch to serve state. If lost all lives switch to the game over state. Pass in the variables that the respective states require.
- 164 => renderScore is used to display the score (very simple and the definition can be found on line 239 in Main.lua).
Any variable defined without using the keyword local becomes a global and is accessible from anywhere in the program. So it’s a good practice to use local
to avoid bugs that arise from unintentionally changing global variables.
breakout6 “The Pretty Colours Update”
Flags for overall map shape and individual row properties. These flags are used to create random levels that look handcrafted (The code doesn’t play with the overall map shape at all, you can safely ignore NONE and the 2 PYRAMID flags).
LevelMaker.lua
- Line 40 => Ensure that the number of columns are odd as even columns can lead to asymmetry.
- Line 45 => Decide the tier based on the level currently in subject to a maximum tier of 3 (we just have 3 tiers).
- Line 48 => Colour cycles are set and restricted to 5 and decided based on level as well. Lines 45 and 48 have the effect of increasing a tier and resetting the highestColor to blue every 6 levels.
- The flags skip and alternate are used to create fancy patterns. If skipFlag is true (set randomly on line 65), every other slot of the row is empty. If alternate is true (set on line 68), the colours of the brick alternate and if solid is true, the entire row is of the same colour. Additionally the number or rows are also random.
Just simple concepts like skipping and alternating colours are used for our procedurally generated game maps.
Student Question:- What happens if the ball is between a brick in a particular frame?
Like mentioned earlier our code fails when delta is too high. So if it’s between a brick, it’ll trigger AABB collision. Recall that we try finding out the portion of the ball that’s outside the brick. No portion of the ball falls outside the brick hence the last default branch (bottom collision) is triggered. It behaves as if the ball has been hit from the below.
breakout7 “The Tier Update”
Now the Brick class makes use of colour and tier while updating and rendering a brick. Checkout the new updated Brick class.
Brick.lua line 44 - 58 => Get a brick of a higher tier (strength level) to a weaker state represented by a different colour. If it’s in the weakest state and gets hit again, set inPlay to false so that the brick doesn’t get rendered.
PlayState.lua line 81 => Simple arithmetic based on colour to update the score once a brick is destroyed. So different colours and different tiers get different scores. The base tier is zero so you don’t get the additional 200 points bonus on hitting it. You can play around and try out different rules to reach an optimum scoring system.
breakout8 “The Particle Update”
Particle systems make a variety of effect achievable. These effects are normally difficult to achieve using sprite animations. Things like an organic fire, flowy or smooth effects are normally achieved using the particle system.
How would you create a fire? Lots of fire particles in an area (representing the center) and gradually reduce the number as you move further away. They probably float upwards and then disappear. In other words write some logic to spawn fewer and fewer particles from a given point, set some -ve dy so that they float upwards and then fade away. You could additionally use a timer to transition the colour from glowing red to yellow. As some particles move up they could turn brown or black (smoke) and disappear from the screen.
We use Love’s integrated particle system. It takes in a texture as a foundation and the number of particles to emit. The number of particles the particle system can emit, their speed etc ultimately decide the possible effects you can create. Particle systems have loads of functions and you can check the ones provided by Love here.
Every brick when hit, needs a particle system of it’s own of their own colour (red brick hits should give out red particles).
Logic for the particle system used in our game:- Randomly generate particles close to our brick, fade away after a certain amount of time. They are of the same colour as the brick in the start and become transparent before they ultimately fade away.
Brick.lua line 20 - 51 => Defines a colour palette in the form of RGB triplets for each brick.
Colton mentions the importance of having a limited colour palette. It gives a cohesive look and even gives a retro like touch. Makes it easier to choose colours as it limits the choices you have. Can be a huge time saver.
-- particle system belonging to the brick, emitted on hit
self.psystem = love.graphics.newParticleSystem(gTextures['particle'], 64)
-- various behavior-determining functions for the particle system
-- https://love2d.org/wiki/ParticleSystem
-- lasts between 0.5-1 seconds seconds
self.psystem:setParticleLifetime(0.5, 1)
-- give it an acceleration of anywhere between X1,Y1 and X2,Y2 (0, 0) and (80, 80) here
-- gives generally downward
self.psystem:setLinearAcceleration(-15, 0, 15, 80)
-- spread of particles; normal looks more natural than uniform, which is clumpy; numbers
-- are amount of standard deviation away in X and Y axis
self.psystem:setAreaSpread('normal', 10, 10)
The code snippet given above are lines 66 - 81 of Brick.lua. The life time, area spread and texture of the particle system is set. It’s a part of the init function because each brick has it’s own particle system.
Lines 92 - 102 => set the colours for the particle system. 2 colours are set. The transparency (alpha) varies between them. 55 * (self.tier + 1)
makes a higher tier more opaque hence brighter. All this code does is specify two colours (using the colour table defined in lines # 20 - 51) with different levels of transparency. The particle system creates particles of the first colour and transitions them to the second colour (transparent in our case) during the lifespan of the particle. 64 particles are emitted.
Line 152 => clearly shows that the particle system is initialised with the center of the brick as it’s base (recall that the dimensions of the Brick are 32 and 16).
Check resources for a detailed introduction and a few exercises on Particle Systems.
breakout 9 “The Progression Update”
This update starts showing levels to the user. All we have to do is store a number. The number is incremented when all the bricks are destroyed (hence all the bricks have inPlay set to false). A new state victory
is created. The game transitions into the victory state when the condition above is fulfilled.
StartState.lua 40 => When changing to a new state just pass in the level in the table. Any time we change state, we just pass in an additional level variable.
PlayState.lua 204 => A function to check victory. If a brick is in play return false else return true.
PlayState.lua 88 => If victory is true, play a sound and transition to victory state. All the victory state does is display a message. Pretty straight forward. Also gives an option to resume game by proceeding to the next level.
VictoryState.lua 35 => Notice when we transition from the victory state to the new state again, the victory state increments the level and also makes use of the level maker to create a new set of bricks.
breakout10 “The High Score Update”
Check out the file system related functions in slide #27.
Most operating systems have specified directories to store application data. Love only allows writing files to the directory assigned by the operating system for such purposes. You can create subdirectories and files within this directory.
For instance on Windows the directory is C:\Users\user\AppData\Roaming\LOVE
. The directories for other operating systems can be examined here. This directory is already hard coded by Lua. So you will only need to use love.filesystem.setIdentity
only when you want to write to a subdirectory within this directory (you mostly won’t need to).
We’ll just have a table (table of tables holding name and score) and sort them by score. While writing to a file we’ll need to convert it to a string. Colton decided to take a simple approach while storing this info in the file. So 10 high scores would result in 20 lines in the file.
name
score
name
score
.
.
main.lua line 213 => Create a folder named breakout
in the local storage directory. If a file named breakout.lst
doesn’t exist, create some dummy high scores and save it to that file (also called seeding). If the file already exists iterate over every line using abstractions provided by Love. If it’s a name just take the first three characters (we only accept 3 characters and this serves as a safety check against long names). If it’s a number read the line (you will get a string type) and convert it to an integer. If this sounds too simple, you could write some code for error checking as well. You could check if the file has been tampered with or if a score is a garbage value, handle it elegantly and prevent it from being rendered and so on to make it more robust.
Check resources and read about these special folders and their purposes.
breakout11 “The Entry Update”
Allows the user to enter his name when he gets a high score. An arcade game like style is used (only 3 characters for storing a name, use up arrows to traverse from A to Z and left and right arrows to change position).
How should we move from A to B, back to A and so on in response to up and down key presses?
A student suggested that we could have a table with A-Z and index the next or previous element. This is bulky and Colton proceeded to describe a better way which I’ll summarise below.
A new state EnterHighScoreState
is created. Check out slide #28 to see how it looks. It’ll also make following along easier.
A table local chars
is used to store the ASCII values of the three characters. local highlightedChar
is an integer that keeps track of the desired character (highlighted on the screen) amongst the three characters. We index the chars
table using highlightedChar
and change the value accordingly. If the left or right arrow key is pressed, it decrements and increments highlightedChar and if the up or down arrow keys are pressed it changes the value of chars[highlightedChar]
by +1 or -1 effectively going to the next or previous alphabet.
If we exceed 90 (z in ASCII) we must wrap around to 65 (a) and if we try going below 65, we should display z. These edge cases are taken care of using if conditions (on line 74 and 79 of EnterHighScoreState.lua).
To reach this state the user should make it to the high score list. So it needs to have the current high score list and also the current score. We have to pass these along the possible states.
We start by comparing the current high score from the bottom of the list and place it below the score higher than it and shift everything below it one spot down.
Check resources to know a little about ASCII.
breakout12 “The Paddle Select Update”
Just displays two arrows for directions and renders a paddle. Every time the right or the left key is pressed it increments or decrements a counter and indexes into the quad table to choose the appropriate paddle. Also the direction hint arrows change (between transparent and opaque) to indicate if the user has reached an extreme and cannot scroll further for more possible paddles in that direction. Check slide#29 to get a better picture of what I’m saying.
Once a colour is chosen by the user, initialise the paddle with the respective colour (recall the choice is stored in the counter) and the paddle can be passed to the various states you might transition into.
breakout13 “The Music Update”
Sound effects for the game.
He also mentioned to have a look at the code that recovers the life of a user by a heart once a certain threshold is reached. This rewards the user for his efforts.