-
Notifications
You must be signed in to change notification settings - Fork 17
Arx 04 Container Classes
Now, when the preparations are finished, the next step is to draw several bricks on the screen.
We'll need an array of some kind to store them.
In fact, it turns out to be more convenient to create a specialized class for this task — BricksContainer
.
BricksContainer
is going to serve two basic purposes.
First, it's going to manage all low level details of looping over different bricks.
Second, it is responsible for, so to say, collective properties of these bricks.
For example, when there are no more bricks left, we would need to change to the next level.
Individual bricks do not need to know anything about other bricks.
Therefore, responsibility to track remaining bricks is laid upon the BricksContainer
class.
The key component of this class is an array, containing all the bricks. Here is an according constructor definition:
function BricksContainer:new( o )
o = o or {}
setmetatable(o, self)
self.__index = self
o.name = o.name or "bricks_container"
o.bricks = o.bricks or {}
.....
end
In arkanoid, bricks are laid on a level in a 2d pattern.
We can make o.bricks
array plain 1d rather than 2d.
However, the debugging and testing considerations suggest, that it is better to mimic the 2d structure and to make
this array 2d for easier access to individual bricks.
2d array can be represented as a table of tables, with each inner table holding a single row of bricks.
Such structure allows to define update
and draw
functions:
function BricksContainer:update( dt )
for _, brick_row in pairs( self.bricks ) do --(*1)
for _, brick in pairs( brick_row ) do
brick:update( dt )
end
end
end
function BricksContainer:draw()
for _, brick_row in pairs( self.bricks ) do
for _, brick in pairs( brick_row ) do
brick:draw()
end
end
end
(*1): an underscore _
is a valid Lua name, that is commonly used for dumb variables,
that are not necessary in the further code.
We also have to populate the array with bricks. This is also done in constructor.
For each brick we have to provide at least it's top left corner position.
To calculate this quantity for each brick, BricksContainer needs to know a number of rows and number of columns,
position of the top left corner of the first brick, width and height of individual bricks, and, finally, horizontal and vertical distances between them.
With such information, it is possible to populate o.bricks
:
function BricksContainer:new( o )
.....
o.bricks = o.bricks or {}
o.rows = 10 --(*1a)
o.columns = 10
o.top_left_position = vector( 100, 50 )
o.horizontal_distance = o.horizontal_distance or 10
o.vertical_distance = o.vertical_distance or 10
o.brick_width = o.brick_width or 50
o.brick_height = o.brick_height or 30 --(*1b)
for row = 1, o.rows do
local new_row = {}
for col = 1, o.columns do
local new_brick_position = o.top_left_position + --(*2)
vector( ( col - 1 ) * ( o.brick_width + o.horizontal_distance ),
( row - 1 ) * ( o.brick_height + o.vertical_distance ) )
local new_brick = Brick:new{ --(*3)
width = o.brick_width,
height = o.brick_height,
position = new_brick_position
}
new_row[ col ] = new_brick --(*4)
end
o.bricks[ row ] = new_row --(*5)
end
.....
end
(*1a)-(*1b): definition of the properties necessary to compute top left corner position for each brick.
(*2): top left position is computed.
(*3): a new brick is created.
(*4): the new brick is inserted into the table representing a row
(*5): the table holding a row of bricks is inserted into the bricks
table.
After the BricksContainer
class is ready, it is necessary to require
it in the main.lua
We are not going to use individual Brick
class from the main.lua
any longer (unless for testing and debugging), so it is possible to delete corresponding require
lines.
local Platform = require "Platform"
local Ball = require "Ball"
local BricksContainer = require "BricksContainer"
.....
In love.load()
, love.update()
and love.draw()
we also have to change individual brick
to bricks_container
:
function love.load()
ball = Ball:new()
platform = Platform:new()
bricks_container = BricksContainer:new()
.....
end
function love.update( dt )
ball:update( dt )
platform:update( dt )
bricks_container:update( dt )
.....
end
function love.draw()
ball:draw()
platform:draw()
bricks_container:draw()
.....
end
While we are discussing container classes, it is also a good time to add walls to the borders of the screen.
A wall is a rectangle, just as a brick is, so the Wall
class is similar to the Brick
class.
The only necessary changes are renaming and modification of the default values.
By default, the wall will be 20 pixel wide, located vertically on the left side of the screen:
function Wall:new( o )
o = o or {}
setmetatable(o, self)
self.__index = self
o.name = o.name or "wall"
o.position = o.position or vector( 0, 0 )
o.width = o.width or 20
o.height = o.height or love.window.getHeight()
return o
end
Since we need 4 walls -- one on each side of the screen, -- it's useful to define WallsContainer
class.
It is mostly similar to BricksContainer
, except for the constructor.
In BricksContainer
it was necessary to define a 2d array of the bricks; in this case
there are only four walls and there is no need to make o.walls
a 2d array.
function WallsContainer:new( o )
.....
o.walls = o.walls or {}
o.wall_thickness = o.wall_thickness or 20
local left_wall = Wall:new{
position = vector( 0, 0 ),
width = o.wall_thickness,
height = love.window.getHeight()
}
local right_wall = Wall:new{
position = vector( love.window.getWidth() - o.wall_thickness, 0 ),
width = o.wall_thickness,
height = love.window.getHeight()
}
local top_wall = Wall:new{
position = vector( 0, 0 ),
width = love.window.getWidth(),
height = o.wall_thickness
}
local bottom_wall = Wall:new{
position = vector( 0, love.window.getHeight() - o.wall_thickness ),
width = love.window.getWidth(),
height = o.wall_thickness
}
o.walls.left = left_wall
o.walls.right = right_wall
o.walls.top = top_wall
o.walls.bottom = bottom_wall
.....
end
The walls overlap a bit, but currently that is not a problem.
Iteration over the walls in update
and draw
is also a bit simpler for 1d array:
function WallsContainer:update( dt )
for _, wall in pairs( self.walls ) do
wall:update( dt )
end
end
function WallsContainer:draw()
for _, wall in pairs( self.walls ) do
wall:draw()
end
end
Of course, it is also necessary to require WallsContainer
from the main.lua
,
make an object of this type in love.load()
, and then repeatedly call it's update
and draw
callbacks.
This is all similar to the bricks_container
case.
Feedback is crucial to improve the tutorial!
Let me know if you have any questions, critique, suggestions or just any other ideas.
Chapter 1: Prototype
- The Ball, The Brick, The Platform
- Game Objects as Lua Tables
- Bricks and Walls
- Detecting Collisions
- Resolving Collisions
- Levels
Appendix A: Storing Levels as Strings
Appendix B: Optimized Collision Detection (draft)
Chapter 2: General Code Structure
- Splitting Code into Several Files
- Loading Levels from Files
- Straightforward Gamestates
- Advanced Gamestates
- Basic Tiles
- Different Brick Types
- Basic Sound
- Game Over
Appendix C: Stricter Modules (draft)
Appendix D-1: Intro to Classes (draft)
Appendix D-2: Chapter 2 Using Classes.
Chapter 3 (deprecated): Details
- Improved Ball Rebounds
- Ball Launch From Platform (Two Objects Moving Together)
- Mouse Controls
- Spawning Bonuses
- Bonus Effects
- Glue Bonus
- Add New Ball Bonus
- Life and Next Level Bonuses
- Random Bonuses
- Menu Buttons
- Wall Tiles
- Side Panel
- Score
- Fonts
- More Sounds
- Final Screen
- Packaging
Appendix D: GUI Layouts
Appendix E: Love-release and Love.js
Beyond Programming: