Lecture 2
- Welcome!
- Project Organization
- State Machines
- Sprite Sheets and Quads
- Paddle and Ball
- The Level Maker
- Collision
- Hearts and Score
- Tiers and Colors
- Particle Systems
- Progression and Levels
- High Scores and Persistence
- Summing Up
Welcome!
- This lecture covers Breakout, which has an interesting history. It ties into Pong very nicely because it was created by Atari back in the ’70s, allowing us to extend a couple of the ideas we saw in Pong. Pong and Breakout are very similar in a lot of ways, but Breakout has a whole lot of bricks instead of just one other paddle at the very top. It is also a single player game, which allows us to look at a whole bunch of different things like scoring and levels and some nice visuals.
- Breakout represents probably the biggest jump in the course in terms of overall complexity and difficulty. But the things we’ll look at today, in particular Sprite sheets, will help simplify projects tremendously going forward. They’ll also segue us nicely into next week’s lecture and into animations, which we’ll be looking at a lot with Mario and Zelda and other problem sets.
- Breakout as a game has a lot of states. Pong and Flappy Bird were quite simple. Flappy Bird only had four states in total, but Breakout has eight. All roads lead back to the start state. From there, we can go to high scores or to paddle select. From paddle select, we can begin serving the ball, which gives us a buffer before the core game loop. When we press Enter in the serve state, we transition to the play state. If we take damage, we go back to serve to let the player regain their composure. If we clear all the bricks, we go to a victory state, then back to serve for the next level. If we lose all our health, we go to game over, which either goes back to start or to an enter high score state if we achieved a high score.
Project Organization
- Previously, we had all of our files in a hodgepodge, just all at the surface in one layer in our folder. That’s okay for something like Pong or even Flappy Bird where things are a little bit on the simple side. But as projects scale and you have larger and more complicated projects with many different types of graphics and fonts and sounds, it’s just very unwieldy. It’s better to try to consolidate things into semantically meaningful folders.
- In Breakout, we have:
- A
sourcefolder for our Lua files (instead of putting them at the parent level) - A
soundsfolder for audio files - A
libfolder for libraries like push and class (some folks prefer to differentiate their own project’s code from other projects’ code) - A
graphicsorimagesfolder for textures - A
fontsfolder for font files
- A
- Rather than just requiring everything at the top of
main.lua, we can put all our dependencies in one central file. We’ve created a file calledDependencies.lua, which contains all our requires. - We also have a
constants.luafile because we had a bunch of constants like our window width and whatnot. This keeps them organized in one place.
State Machines
-
We’ve created a
StateMachineclass to manage our game states. The idea is that all roads will lead back to the start state, but from there we can transition to various other states like high scores, paddle select, serve, play, victory, game over, and enter high score. Frombreakout0/src/StateMachine.lua:StateMachine = Class{} function StateMachine:init(states) self.empty = { render = function() end, update = function() end, enter = function() end, exit = function() end } self.states = states or {} self.current = self.empty end function StateMachine:change(stateName, enterParams) assert(self.states[stateName]) self.current:exit() self.current = self.states[stateName]() self.current:enter(enterParams) end function StateMachine:update(dt) self.current:update(dt) end function StateMachine:render() self.current:render() endNotice that when we call
change, it will call the current state’sexitfunction if defined, then create a new instance of the target state and call itsenterfunction. This allows us to pass data from one state to another using theenterParamstable. -
Every time we call
gStateMachine:change, recall it will call its own exit function if defined, and then it will call its enter function if also defined. What that lets us do is bring data from one state into another state. Because every time we create a new state with that state machine, it’s going to create a brand new copy of that state. That’s often what we want, but often also not what we want, depending on whether the states should preserve some amount of continuity between them.
Sprite Sheets and Quads
- One of the biggest things we’ll look at in this lecture, besides the collision detection, is this idea of quads. Previously, we just talked about images. Images are relatively easy to draw. You just do
love.graphics.drawand then whatever texture you want. But in Breakout, we have a blue paddle, a green paddle, a purple paddle. If we want a bunch of different paddles, those would have to be their own images. If we were to make individual textures for each and every one of these images, we’d have close to 50 to 100 images. It would be very unwieldy. It would be horrible in code to be like, “Okay, what’s the name of this one? Blue? Blue with green stripes? Blue with yellow stripes?” - Folks typically in industry find it a little bit easier to work with these things called Sprite sheets or texture atlases, which allow you to condense all of your artwork into one file rather than have many separate images. With Sprite sheets, we can just load one texture atlas and then think in terms of rectangles. Think about how to splice that up into pieces that we can then reference. We can say, “Okay, draw our main texture at this particular rectangle’s offset, at this particular width and height.” That allows us to pretend as if we have maybe 100 images, but in truth, we’re just drawing one image. The graphics card is taking those UV coordinates, that quad data, and just drawing a tiny little piece of that for us.
- A couple of the functions we need to look at are
love.graphics.newQuadand passing a quad tolove.graphics.draw.love.graphics.newQuadis just essentially declaring a rectangle. It’s not a lot different fromlove.graphics.rectanglein terms of how we think about it, except it’s a piece of data that’s going to apply to our texture when we want to draw it. If we pass in a texture, a quad, and an X and Y, at XY it will draw our texture, but it will only draw the rectangle of that texture that we tell it to. -
We have a utility function called
GenerateQuadsthat takes an atlas and gives it the width and height that we want to assume every image within that texture is. Then it will just do the job of automatically splicing the whole thing for us top to bottom in those X by Y increments. Frombreakout1/src/Util.lua:function GenerateQuads(atlas, tilewidth, tileheight) local sheetWidth = atlas:getWidth() / tilewidth local sheetHeight = atlas:getHeight() / tileheight local sheetCounter = 1 local spritesheet = {} for y = 0, sheetHeight - 1 do for x = 0, sheetWidth - 1 do spritesheet[sheetCounter] = love.graphics.newQuad(x * tilewidth, y * tileheight, tilewidth, tileheight, atlas:getDimensions()) sheetCounter = sheetCounter + 1 end end return spritesheet endNotice how we iterate through rows (Y) and columns (X), creating a quad for each tile position. For every Y, we grab a tile’s width all the way until the end of the X axis, then loop back down to the next Y value. This is a common thing you’ll see when you have a Sprite sheet with uniform-sized elements.
-
The paddles are different sizes, so we have to do those a little more manually. We can’t just do a loop that iterates through the entirety of the texture because the paddles are 32, 64, 96, and 128 pixels wide. From
breakout1/src/Util.lua:function GenerateQuadsPaddles(atlas) local x = 0 local y = 64 local counter = 1 local quads = {} for i = 0, 3 do -- smallest quads[counter] = love.graphics.newQuad(x, y, 32, 16, atlas:getDimensions()) counter = counter + 1 -- medium quads[counter] = love.graphics.newQuad(x + 32, y, 64, 16, atlas:getDimensions()) counter = counter + 1 -- large quads[counter] = love.graphics.newQuad(x + 96, y, 96, 16, atlas:getDimensions()) counter = counter + 1 -- huge quads[counter] = love.graphics.newQuad(x, y + 16, 128, 16, atlas:getDimensions()) counter = counter + 1 x = 0 y = y + 32 end return quads endNotice that we manually specify the width for each paddle size (32, 64, 96, 128). We iterate four times for the four different paddle colors (skins), shifting down by 32 pixels each time to get to the next color row. Eventually, we have all the paddle rectangles in our quads table, and we can index into them by skin and size.
Paddle and Ball
-
The
Paddleclass is very similar to the paddles from Pong, except now we have adxinstead of adybecause we’re moving on the X axis instead of the Y axis. We also haveskinandsizevariables that can influence our appearance. Frombreakout1/src/Paddle.lua:Paddle = Class{} function Paddle:init() self.x = VIRTUAL_WIDTH / 2 - 32 self.y = VIRTUAL_HEIGHT - 32 self.dx = 0 self.width = 64 self.height = 16 self.skin = 1 self.size = 2 end function Paddle:update(dt) if love.keyboard.isDown('left') then self.dx = -PADDLE_SPEED elseif love.keyboard.isDown('right') then self.dx = PADDLE_SPEED else self.dx = 0 end if self.dx < 0 then self.x = math.max(0, self.x + self.dx * dt) else self.x = math.min(VIRTUAL_WIDTH - self.width, self.x + self.dx * dt) end end function Paddle:render() love.graphics.draw(gTextures['main'], gFrames['paddles'][self.size + 4 * (self.skin - 1)], self.x, self.y) endNotice we’re using the same clamping logic we saw with Pong using
math.maxandmath.minto make sure we don’t go off the left and right edges of the screen. In the render function, we index into the paddles quad table usingself.size + 4 * (self.skin - 1). Size is which of the four sizes, and multiplying by four and adding the skin offset gets us the correctly colored paddle. -
The
Ballclass uses the same AABB collision detection we saw with Pong. It can have a skin which is chosen at random just for visual variety. Frombreakout2/src/Ball.lua:Ball = Class{} function Ball:init(skin) self.width = 8 self.height = 8 self.dy = 0 self.dx = 0 self.skin = skin end function Ball:collides(target) if self.x > target.x + target.width or target.x > self.x + self.width then return false end if self.y > target.y + target.height or target.y > self.y + self.height then return false end return true end function Ball:update(dt) self.x = self.x + self.dx * dt self.y = self.y + self.dy * dt if self.x <= 0 then self.x = 0 self.dx = -self.dx gSounds['wall-hit']:play() end if self.x >= VIRTUAL_WIDTH - 8 then self.x = VIRTUAL_WIDTH - 8 self.dx = -self.dx gSounds['wall-hit']:play() end if self.y <= 0 then self.y = 0 self.dy = -self.dy gSounds['wall-hit']:play() end endNotice that the ball bounces off the left, right, and top walls by inverting its velocity. The ball can go below the screen (bottom edge), which is how the player loses health.
The Level Maker
-
The
LevelMakeris a class that encapsulates the idea of generating a level. Rather than having a method that we call on an instance, we create a static function because we don’t need a LevelMaker instance living anywhere. We can just call this function that exists in the LevelMaker class. Frombreakout3/src/LevelMaker.lua:LevelMaker = Class{} function LevelMaker.createMap(level) local bricks = {} local numRows = math.random(1, 5) local numCols = math.random(7, 13) for y = 1, numRows do for x = 1, numCols do b = Brick( (x-1) * 32 + 8 + (13 - numCols) * 16, y * 16 ) table.insert(bricks, b) end end return bricks endNotice the X coordinate calculation. We subtract X by 1 before multiplying by 32 because coordinates are zero-indexed even though Lua tables are one-indexed. We add 8 pixels of padding, and then we add an offset based on the number of columns vs. 13 (the max that fits) times 16 pixels. This ensures everything is centered. The Y coordinate is simpler: just
y * 16since the bricks are 16 pixels tall. -
The
Brickclass is simple. It has a tier and color for scoring and appearance, an X and Y position, and aninPlayflag that determines whether it should be rendered and can be collided with. Frombreakout3/src/Brick.lua:Brick = Class{} function Brick:init(x, y) self.tier = 0 self.color = 1 self.x = x self.y = y self.width = 32 self.height = 16 self.inPlay = true end function Brick:hit() gSounds['brick-hit-2']:play() self.inPlay = false end function Brick:render() if self.inPlay then love.graphics.draw(gTextures['main'], gFrames['bricks'][1 + ((self.color - 1) * 4) + self.tier], self.x, self.y) end endNotice the render math: we multiply color by 4 (minus 1 for zero-indexing) to get our color offset, then add tier to draw the correct tier and color brick. Using
inPlayas a flag rather than removing bricks from the table is useful if we want to restart the level with the same layout.
Collision
- Unlike Pong where the ball always bounced in a predictable way, in Breakout we want to give the paddle some physical control and have an influence on the angle. If we think of the ball coming down and hitting the paddle, the closer to the edge it hits, the faster we think it should bounce and more sharply bounce. That’s the feeling one gets when playing Pong or Breakout.
-
We need to figure out the midpoint of the paddle and the offset of the ball. Then, depending on which side the ball hits and whether the paddle is moving, we adjust the ball’s horizontal velocity. If we’re moving left and the ball hits the left side of the paddle, we want the ball to bounce sharply to the left. The further from center, the sharper the angle. From
breakout4/src/states/PlayState.lua:if self.ball:collides(self.paddle) then self.ball.y = self.paddle.y - 8 self.ball.dy = -self.ball.dy -- if we hit the paddle on its left side while moving left... if self.ball.x < self.paddle.x + (self.paddle.width / 2) and self.paddle.dx < 0 then self.ball.dx = -50 + -(8 * (self.paddle.x + self.paddle.width / 2 - self.ball.x)) -- else if we hit the paddle on its right side while moving right... elseif self.ball.x > self.paddle.x + (self.paddle.width / 2) and self.paddle.dx > 0 then self.ball.dx = 50 + (8 * math.abs(self.paddle.x + self.paddle.width / 2 - self.ball.x)) end gSounds['paddle-hit']:play() endNotice how we calculate the offset from the center of the paddle. We multiply by 8 (a magic number found through balancing) and add or subtract from a starting bounce velocity of 50. This gives it the feeling of a physical paddle, like putting English on the ball.
- Brick collision is a bit more complicated. Unlike Pong where the ball would always bounce off in a predictable direction, with Breakout the ball can hit bricks from any direction. If it bounces off the top, we want it to go back up. But if it hits from the left side and it’s going right, we want it to bounce back to the left. And so on throughout all four directions.
-
We check to see if the opposite side of our velocity is outside of the brick. If it is, we trigger a collision on that side. Otherwise, we’re within the X plus width of the brick and should check to see if the top or bottom edge is outside of the brick. From
breakout5/src/states/PlayState.lua:for k, brick in pairs(self.bricks) do if brick.inPlay and self.ball:collides(brick) then self.score = self.score + 10 brick:hit() -- left edge; only check if we're moving right if self.ball.x + 2 < brick.x and self.ball.dx > 0 then self.ball.dx = -self.ball.dx self.ball.x = brick.x - 8 -- right edge; only check if we're moving left elseif self.ball.x + 6 > brick.x + brick.width and self.ball.dx < 0 then self.ball.dx = -self.ball.dx self.ball.x = brick.x + 32 -- top edge if no X collisions elseif self.ball.y < brick.y then self.ball.dy = -self.ball.dy self.ball.y = brick.y - 8 -- bottom edge if no X collisions or top collision else self.ball.dy = -self.ball.dy self.ball.y = brick.y + 16 end -- slightly scale the y velocity to speed up the game self.ball.dy = self.ball.dy * 1.02 break end endNotice we offset the checks by a couple of pixels (using
+2and+6) so that flush corner hits register as Y flips, not X flips. We also multiplydyby 1.02 on each hit to gradually speed up the game. Thebreakensures we only process one brick collision per frame to handle corner cases properly.
Hearts and Score
- Now we need a way to determine what’s our loss condition. We have a few hearts at the top right of the screen. Hearts are like a very Zelda, very video game-y thing. We also have a score that we’ve added. Every brick is worth 10 points in this tier.
-
The serve state, for example, has a score, HP, and the bricks. That state needs to communicate with the play state. When we press Enter in the serve state, we pass all the important data to the play state. From
breakout5/src/states/ServeState.lua:function ServeState:update(dt) self.paddle:update(dt) self.ball.x = self.paddle.x + (self.paddle.width / 2) - 4 self.ball.y = self.paddle.y - 8 if love.keyboard.wasPressed('enter') or love.keyboard.wasPressed('return') then gStateMachine:change('play', { paddle = self.paddle, bricks = self.bricks, health = self.health, score = self.score, ball = self.ball }) end endNotice how the ball tracks the player’s paddle position before serving. When Enter is pressed, we pass in all important state info to the PlayState. These are all the variables we want to pass to say, “Hey, you now take charge of these variables. We are going to get deallocated. The state is going to be lost. But before we do that, here, take these things.”
-
We have a couple of helper functions defined in main.lua because they’re drawn in several different places. Rather than define them in every state and just have them repeat over and over again, it makes sense to have them defined here. From
breakout5/main.lua:function renderHealth(health) local healthX = VIRTUAL_WIDTH - 100 for i = 1, health do love.graphics.draw(gTextures['hearts'], gFrames['hearts'][1], healthX, 4) healthX = healthX + 11 end for i = 1, 3 - health do love.graphics.draw(gTextures['hearts'], gFrames['hearts'][2], healthX, 4) healthX = healthX + 11 end endNotice that we draw red hearts (frame 1) for each point of health we have, then gray/unfilled hearts (frame 2) for the remaining slots. If you have 3 HP, draw 3 red hearts. If you have 2 HP, draw 2 red hearts and 1 gray heart.
-
When the ball goes below the screen, we lose health. If health reaches zero, we transition to game over. Otherwise, we go back to the serve state with our data preserved. From
breakout5/src/states/PlayState.lua:if self.ball.y >= VIRTUAL_HEIGHT then self.health = self.health - 1 gSounds['hurt']:play() if self.health == 0 then gStateMachine:change('game-over', { score = self.score }) else gStateMachine:change('serve', { paddle = self.paddle, bricks = self.bricks, health = self.health, score = self.score }) end endNotice how we pass the appropriate data to each state. The game over state only needs the score for display. The serve state needs the paddle, bricks, health, and score to continue the game.
Tiers and Colors
- Currently we’ve only been dealing with very boring brick layouts where we just have blue bricks and they’re just worth 10 points. But if people are familiar with Breakout, there are various colored bricks, and they all mean different things. They can be a higher tier that takes multiple hits to break. They can be worth more points accordingly.
- Through relatively simple flag-based logic, we can create alternating colors, alternating spaces (skip patterns), and vary the visual appearance significantly. The level maker can decide: are we in an alternating row? If so, we need an on/off flag. So it’ll be on/off, on/off. Are we skipping bricks? Then brick, no brick, brick, no brick.
- Lua doesn’t have a continue statement, but it does have a form of goto that we can simulate continue with. We use labels and a goto statement. If we’re skipping and should skip this brick, we don’t actually generate a brick. We just say
goto continuewhere continue is a label defined with double colons::continue::. -
When a brick gets hit, instead of immediately setting
inPlayto false, we can decrease its color first. Green goes to blue, red goes to green, etc. When it finally reaches the lowest color at tier zero, then it gets removed from play. Frombreakout7/src/Brick.lua:function Brick:hit() gSounds['brick-hit-2']:stop() gSounds['brick-hit-2']:play() if self.tier > 0 then if self.color == 1 then self.tier = self.tier - 1 self.color = 5 else self.color = self.color - 1 end else if self.color == 1 then self.inPlay = false else self.color = self.color - 1 end end if not self.inPlay then gSounds['brick-hit-1']:stop() gSounds['brick-hit-1']:play() end endNotice that if we’re in a higher tier and our color is 1 (blue), we go down a tier and reset to color 5 (the highest color). Otherwise, we just decrease the color. At tier 0, if the color is 1, the brick is removed from play. This gives us much more visual variety and challenge.
Particle Systems
-
Particle systems are really cool because they’re essentially just a way to add visual flair. They’re called a system because the particles inside this emitter or generator have behavior that you can configure. Right now, when we hit bricks, they essentially just disappear right away. There’s some element of simplicity to that that’s a little bit unsatisfactory. Particle systems are a way to add just a little, literally, sparkle to your game.
-
To create a particle system, we use
love.graphics.newParticleSystem. Particles take a texture. In today’s example, we’re using a simple circular texture, and it’s going to then apply various alpha and colorization functions to those particles, emit them in a particular way, fade them out, and so forth. Frombreakout8/src/Brick.lua:paletteColors = { [1] = { ['r'] = 99, ['g'] = 155, ['b'] = 255 }, -- blue [2] = { ['r'] = 106, ['g'] = 190, ['b'] = 47 }, -- green [3] = { ['r'] = 217, ['g'] = 87, ['b'] = 99 }, -- red [4] = { ['r'] = 215, ['g'] = 123, ['b'] = 186 }, -- purple [5] = { ['r'] = 251, ['g'] = 242, ['b'] = 54 } -- gold } -- in Brick:init self.psystem = love.graphics.newParticleSystem(gTextures['particle'], 64) self.psystem:setParticleLifetime(0.5, 1) self.psystem:setLinearAcceleration(-15, 0, 15, 80) self.psystem:setEmissionArea('normal', 10, 10)Notice we set a lifetime between 0.5 and 1 second for each particle. The linear acceleration creates a range: X can be between -15 and 15 (so particles spread left and right), but Y is only between 0 and 80 (positive), so particles fall down like there’s artificial gravity.
-
When we hit a brick, we set the particle colors to match the brick color and emit the particles. From
breakout8/src/Brick.lua:self.psystem:setColors( paletteColors[self.color].r / 255, paletteColors[self.color].g / 255, paletteColors[self.color].b / 255, 55 * (self.tier + 1) / 255, paletteColors[self.color].r / 255, paletteColors[self.color].g / 255, paletteColors[self.color].b / 255, 0 ) self.psystem:emit(64)Notice
setColorstakes two RGBA color sets: the start color and end color. We use the same RGB but fade from some alpha (higher for higher tiers) to zero alpha (transparent). Thenemit(64)shoots out 64 particles. Each brick has its own particle system, and a particle system will be triggered to emit, decay over its lifetime, and then be ready to reuse.
Progression and Levels
- Whenever we finish a level by clearing all bricks, we want to display “Level X Complete” and then progress to the next level. With every victory, you can have a new grid of bricks that you make progressively more difficult. This is really important because now we have something to aspire to. “I made it past level 5” or “I got to level 10.”
-
Victory detection is simple: if all bricks are no longer in play, we’ve won. From
breakout9/src/states/PlayState.lua:function PlayState:checkVictory() for k, brick in pairs(self.bricks) do if brick.inPlay then return false end end return true endNotice we iterate through all bricks. If any are still in play, return false. If we’ve gone through all of them and none are in play, return true.
-
When transitioning to the next level, we increment the level number and create a new map with the higher level value, which the level maker can use to generate harder layouts. From
breakout9/src/states/VictoryState.lua:if love.keyboard.wasPressed('enter') or love.keyboard.wasPressed('return') then gStateMachine:change('serve', { level = self.level + 1, bricks = LevelMaker.createMap(self.level + 1), paddle = self.paddle, health = self.health, score = self.score }) endNotice we pass
self.level + 1to both the serve state and toLevelMaker.createMap. The level maker can then generate more sophisticated maps with higher tiers and colors as the level increases.
High Scores and Persistence
- If you think of any game you’ve ever played, a long time ago, back in the day, you didn’t really have persistent data. Legend of Zelda was one of the first that did, with a battery that kept memory active. Nowadays, with modern computers, we expect save files. It’s a little bit deflating to think you’re going to put all this time and effort into playing this game and just completely lose your high score.
-
Love2D provides filesystem functions for data persistence.
love.filesystem.setIdentitysays where we should assume we have free reign for a folder to put save data in. This is operating system dependent: on Mac it’s in Library/Application Support, on Windows it’s in AppData, on Linux it’s somewhere else. Frombreakout10/main.lua:function loadHighScores() love.filesystem.setIdentity('breakout') if not love.filesystem.getInfo('breakout.lst') then local scores = '' for i = 10, 1, -1 do scores = scores .. 'CTO\n' scores = scores .. tostring(i * 1000) .. '\n' end love.filesystem.write('breakout.lst', scores) end local name = true local counter = 1 local scores = {} for i = 1, 10 do scores[i] = { name = nil, score = nil } end for line in love.filesystem.lines('breakout.lst') do if name then scores[counter].name = string.sub(line, 1, 3) else scores[counter].score = tonumber(line) counter = counter + 1 end name = not name end return scores endNotice we think of every high score as initials then score, blocks of two lines. If no file exists, we generate default scores. Then we iterate over the lines, alternating between reading a name and reading a score.
-
For entering initials, we use ASCII values. 65 represents ‘A’, 90 represents ‘Z’. We store three numbers that we increment/decrement with up/down arrows, then convert them to characters using
string.char. Frombreakout11/src/states/EnterHighScoreState.lua:local chars = { [1] = 65, [2] = 65, [3] = 65 } local highlightedChar = 1 if love.keyboard.wasPressed('up') then chars[highlightedChar] = chars[highlightedChar] + 1 if chars[highlightedChar] > 90 then chars[highlightedChar] = 65 end elseif love.keyboard.wasPressed('down') then chars[highlightedChar] = chars[highlightedChar] - 1 if chars[highlightedChar] < 65 then chars[highlightedChar] = 90 end end local name = string.char(chars[1]) .. string.char(chars[2]) .. string.char(chars[3])Notice we clamp the values between 65 (A) and 90 (Z), wrapping around if we go past either end. This is a very common presentation for arcade games.
- When saving a new high score, we shift all the scores below down by one and insert the new score at the appropriate position, then write the entire table back to disk.
Summing Up
Breakout is probably the biggest hurdle stepwise in difficulty throughout the term. There will be other things we look at in Mario and Zelda that have their own difficulties, but Breakout is probably the biggest jump up to feeling like we made a full game rather than Pong which is simple and two-player, or Flappy Bird which is single-player but also very simple. Breakout has got a lot of pieces to make it a good implementation. In this lecture, we learned about…
- Sprite sheets and quads for efficiently managing many graphics from a single texture.
- Project organization with separate folders for source, graphics, sounds, and libraries.
- State machines for managing complex game flow across multiple states with data passing.
- Paddle and ball classes that use quads for rendering.
- Brick collision that handles hits from all four directions.
- Procedural level generation with the LevelMaker class.
- Health systems with visual heart displays.
- Tier and color systems for multi-hit bricks.
- Particle systems for visual polish and feedback.
- Level progression and victory states.
- Data persistence using Love2D’s filesystem API for high scores.
Next week we’ll look at anonymous functions, tweening and timers for smooth animations, and discrete problem solving in the context of a puzzle game. We’ll be looking at Match 3, also known as Candy Crush or Bejeweled.
See you next time!