Breakout (part 2): entity management

Review

In the previous section we made a checklist of requirements and accomplished one of them:

  • The objective of the game is to destroy all the bricks on the screen
  • The player controls a "paddle" entity that hits a ball
  • The ball destroys the bricks
  • The ball needs to stay within the boundaries of the screen
  • If the ball touches the bottom of the screen, the game ends

In the previous exercise, the goal was to move the boundaries so they were just off screen. This gives the effect that the ball is bouncing off the edges of the game window.

-- entities/boundary-bottom.lua
entity.body = love.physics.newBody(world, 400, 606, 'static')
-- entities/boundary-left.lua
entity.body = love.physics.newBody(world, -6, 300, 'static')
-- entities/boundary-right.lua
entity.body = love.physics.newBody(world, 806, 300, 'static')
-- entities/boundary-top.lua
entity.body = love.physics.newBody(world, 400, -6, 'static')

Here they have been moved 6 pixels off screen just to use even numbers and make calculation easier. Previously we also raised the question of whether or not the boundaries would work if we still require'd them in main.lua but didn't draw them in love.draw. The answer is they still work but we don't see them. Since they are off screen, that doesn't matter anyway and we can save our program from doing extra work:

-- main.lua
love.draw = function()
  local ball_x, ball_y = ball.body:getWorldCenter()
  love.graphics.circle('fill', ball_x, ball_y, ball.shape:getRadius())
  love.graphics.polygon('line', paddle.body:getWorldPoints(paddle.shape:getPoints()))
end

Entity list

Let's think about the problem of brick entities for a minute. We could create an entity file for each brick, but they are more or less the same except that they spawn in different spots. Imagine making 50 different entity files and then inside love.draw making 50 lines to draw each brick and so on. What we can instead do is make an entity file for 1 brick then make a list with 50 copies of it (or however many brick copies we end up fitting on the screen). We can then loop over this list to draw the bricks.

Let's first create the brick entity file:

-- entities/brick.lua

local world = require('world')

return function(x_pos, y_pos)
  local entity = {}
  entity.body = love.physics.newBody(world, x_pos, y_pos, 'static')
  entity.shape = love.physics.newRectangleShape(50, 20)
  entity.fixture = love.physics.newFixture(entity.body, entity.shape)
  entity.fixture:setUserData(entity)

  return entity
end

Instead of returning an entity in this file, we returned a function that takes an x-position and y-position as parameters. When the function gets invoked wherever it is required, it will generate a new entity with those coordinates for its spawn point. Here's how we can use it:

-- main.lua

local boundary_bottom = require('entities/boundary-bottom')
local boundary_left = require('entities/boundary-left')
local boundary_right = require('entities/boundary-right')
local boundary_top = require('entities/boundary-top')
local paddle = require('entities/paddle')
local ball = require('entities/ball')
local brick = require('entities/brick')

local entities = {
  brick(100, 100),
  brick(200, 100),
  brick(300, 100)
}


local world = require('world')

-- Boolean to keep track of whether our game is paused or not
local paused = false

local key_map = {
  escape = function()
    love.event.quit()
  end,
  space = function()
    paused = not paused
  end
}

love.draw = function()
  local ball_x, ball_y = ball.body:getWorldCenter()
  love.graphics.circle('fill', ball_x, ball_y, ball.shape:getRadius())
  love.graphics.polygon('line', paddle.body:getWorldPoints(paddle.shape:getPoints()))

  for _, entity in ipairs(entities) do
    love.graphics.polygon('fill', entity.body:getWorldPoints(entity.shape:getPoints()))
  end
end

love.focus = function(focused)
  if not focused then
    paused = true
  end
end

love.keypressed = function(pressed_key)
  -- Check in the key map if there is a function
  -- that matches this pressed key's name
  if key_map[pressed_key] then
    key_map[pressed_key]()
  end
end

love.update = function(dt)
  if not paused then
    world:update(dt)
  end
end

We made an entity table with a list of brick entities in it, then in love.draw we made a for loop to draw each entity in the list. Before we change anything else try running the game and taking a look that the bricks appear and that everything works.

Rule of single responsibility

Our goal for the rest of this section will be to simplify entity management. One strategy we'll have for doing this is to think of each file in our game as having a single responsibility. A good sign that we're doing this is main.lua is very small and easy to scan over with the eyes and digest.

So what is the responsibility of main.lua?

  • Create the callback functions necessary to run the game.

Here's some things it is doing that don't fit that responsibility:

  • Load and store all the entities
  • Figure out how to draw each type of entity in love.draw
  • Store a map of keypresses

Imagine our game is an organization and each file is a role in the company. Our main file is like the secretary that knows how to handle requests from outsiders. If somebody called asking the secretary about building-maintenance issues, the secretary wouldn't grab plumbing tools and take care of the problem but rather dispatch the person whose responsibility is that exact kind of problem. As the owner of this organization we should know everyone's roles so it's easy to know where each responsibility lies. It will make it easier for us to grow the company to the size we desire.

One easy improvement is to not write out all the instructions for drawing each entity within the main file, but rather let each entity file be responsible for every feature of that entity, including how to draw that entity. We may want to get fancy later and draw bricks in different colors, for instance. That could get complicated and we don't want the main file to retain a bunch of code about brick colors and such.

Modifying the entities is as easy as creating draw functions in the entity tables:

-- entities/brick.lua

local world = require('world')

return function(x_pos, y_pos)
  local entity = {}
  entity.body = love.physics.newBody(world, x_pos, y_pos, 'static')
  entity.shape = love.physics.newRectangleShape(50, 20)
  entity.fixture = love.physics.newFixture(entity.body, entity.shape)
  entity.fixture:setUserData(entity)

  entity.draw = function(self)
    love.graphics.polygon('fill', self.body:getWorldPoints(self.shape:getPoints()))
  end

  return entity
end
-- entities/paddle.lua

local world = require('world')

return function(pos_x, pos_y)
  local entity = {}
  entity.body = love.physics.newBody(world, pos_x, pos_y, 'static')
  entity.shape = love.physics.newRectangleShape(180, 20)
  entity.fixture = love.physics.newFixture(entity.body, entity.shape)
  entity.fixture:setUserData(entity)

  entity.draw = function(self)
    love.graphics.polygon('line', self.body:getWorldPoints(self.shape:getPoints()))
  end

  return entity
end
-- entities/ball.lua

local world = require('world')

return function(x_pos, y_pos)
  local entity = {}
  entity.body = love.physics.newBody(world, x_pos, y_pos, 'dynamic')
  entity.body:setMass(32)
  entity.body:setLinearVelocity(300, 300)
  entity.shape = love.physics.newCircleShape(0, 0, 10)
  entity.fixture = love.physics.newFixture(entity.body, entity.shape)
  entity.fixture:setRestitution(1)
  entity.fixture:setUserData(entity)

  entity.draw = function(self)
    local self_x, self_y = self.body:getWorldCenter()
    love.graphics.circle('fill', self_x, self_y, self.shape:getRadius())
  end

  return entity
end

Go ahead and make all the entities return a function with x_pos and y_pos parameters and we'll just add everything to the entity list like the bricks. Don't forget to change out the numbers in the love.physics.newBody(world, 200, 200, 'dynamic') with the arguments being passed in by the function: love.physics.newBody(world, x_pos, y_pos, 'dynamic'). For the boundaries entity files there is no need for entity.draw functions, but still make them return functions with the two parameters. Now update the entities list in main.lua to include all the entities:

-- main.lua

local boundary_bottom = require('entities/boundary-bottom')
local boundary_left = require('entities/boundary-left')
local boundary_right = require('entities/boundary-right')
local boundary_top = require('entities/boundary-top')
local paddle = require('entities/paddle')
local ball = require('entities/ball')
local brick = require('entities/brick')

local entities = {
  boundary_bottom(400, 606),
  boundary_left(-6, 300),
  boundary_right(806, 300),
  boundary_top(400, -6),
  paddle(300, 500),
  ball(200, 200),
  brick(100, 100),
  brick(200, 100),
  brick(300, 100)
}


local world = require('world')

-- Boolean to keep track of whether our game is paused or not
local paused = false

local key_map = {
  escape = function()
    love.event.quit()
  end,
  space = function()
    paused = not paused
  end
}

love.draw = function()
  for _, entity in ipairs(entities) do
    if entity.draw then entity:draw() end
  end
end

love.focus = function(focused)
  if not focused then
    paused = true
  end
end

love.keypressed = function(pressed_key)
  -- Check in the key map if there is a function
  -- that matches this pressed key's name
  if key_map[pressed_key] then
    key_map[pressed_key]()
  end
end

love.update = function(dt)
  if not paused then
    world:update(dt)
  end
end

Take a look at our love.draw function. It is much simpler now that it no longer needs to know how to draw each entity. It just asks the entity if it knows how to draw itself and if it does it tells it to do so. Remember that invoking entity:draw() is just shorthand for writing entity.draw(entity) because of the :.

Ok, but putting the entities in a list didn't clean up this file. Now this file is responsible for knowing where to spawn the entities and having them in a list just makes this file bigger. Well you see the reason we put them in a list is because we want to make a new game file called entities.lua that will be responsible for loading, spawning, and storing all the entities when the game starts up. Create a new file then cut all the entity require statements and the entity list and paste it in the new file:

-- entities.lua

local boundary_bottom = require('entities/boundary-bottom')
local boundary_left = require('entities/boundary-left')
local boundary_right = require('entities/boundary-right')
local boundary_top = require('entities/boundary-top')
local paddle = require('entities/paddle')
local ball = require('entities/ball')
local brick = require('entities/brick')

return {
  boundary_bottom(400, 606),
  boundary_left(-6, 300),
  boundary_right(806, 300),
  boundary_top(400, -6),
  paddle(300, 500),
  ball(200, 200),
  brick(100, 100),
  brick(200, 100),
  brick(300, 100)
}

And now the top of our main file only needs to load the entities file and it will have the list to use in love.draw and elsewhere as needed:

-- main.lua

local entities = require('entities')
local world = require('world')

When you run the game, you should be seeing something similar to this:

If you missed anything or are having issues, here's a copy of the completed source code for this section: https://github.com/RVAGameJams/learn2love/tree/master/code/breakout-2

And that's about it for entity management. We'll figure out how to handle keypresses for the paddle and everything else in the next section. We'll finish the cleanup in our main file while we're at it.

Exercises

  • Now that our entities have passed off knowledge on where they spawn over to entities.lua, our left and right boundaries are identical files. Replace boundary-left.lua and boundary-right.lua with a single boundary-vertical.lua file and spawn two copies of that in entities.lua. If you get stuck, check out the entities.lua file in the next section for how this is done.

results matching ""

    No results matching ""