Arx 07 Loading Level From External File - noooway/love2d_arkanoid_tutorial GitHub Wiki

The next thing I want to show is how to load levels from external files and how to switch between the levels.

A common situation is to have a separate file for each level or each distinct location in your game. I'm also going to use such approach. The files with the level descriptions will be kept at the levels folder.

Levels in arkanoid are composed of different arrangements of bricks. Apart from position, bricks also can differ in type (say, have different color ).

Immediately, the question arises, which format to choose to store this information in a level-file. For a large game, most probably you are going to use a stand-alone tool for level design, for example, Tiled map editor. An output of such program is usually an *.xml or some other text file. To load it, it would be necessary either to write a parser or to use an external library. In our case, there is no need for such complications and I'm going to use Lua syntax to describe level files.

While writing a constructor for the BricksContainer class, we have identified the parameters necessary to specify an arrangement of the bricks. These parameters are the top left postion of the top left brick, the width and height of each brick and the horizontal and vertical distance between the bricks. Using these parameters, we produce the 2d array of bricks stored in the .bricks field of the BricksContainer object. We didn't have a type of the brick since we have assumed they were similar. But we obviously need to add the type now.

We could have set all the necessary parameters individually for each brick -- but that would be overkill. Instead, I'm going to place them as default values in the constructor. The only thing that is going to be stored in the level file is a 2d table, telling us, whether or not to create a brick on a certain position, and which type it should have.

It is possible to write each level-file as a separate Lua module. However, it is considered a good practice to avoid any code in level files, preferably making them data-only. Therefore, the only thing that remains from the Lua module structure is the return statement. Here is an example of 01.lua file:

return {
   name = "first",
   bricks = {
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
      {3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
      {2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
      {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
   }
}

Brick are created only at nonzero positions, with values indicating the brick type.

We need to add bricktype as the field to the Brick class.

function Brick:new( o )
   .....
   o.height = o.height or 30
   o.bricktype = o.bricktype or 1
   .....
   return o
end

The level file has to be loaded somewhere. A natural place is love.load callback. After loading, we need to pass it to the bricks_container constructor

function love.load()
   level = require "levels/01"
   collider = HC.new()
   .....
   bricks_container = BricksContainer:new( { level = level,
   		      			     collider = collider } )
   .....
end

and record it in the appropriate field

function BricksContainer:new( o )
   .....
   o.bricks = o.bricks or {}
   o.level = o.level or nil
   o.collider = o.collider or {}
   .....
end

If the level-file was not specified, we use the default level-construction procedure. In the opposite case, we construct from the level file.

function BricksContainer:new( o )
   .....
   o.brick_height = o.brick_height or 30
   if not o.level then
	  o:default_level_construction_procedure()
   else
	  o:construct_from_level()
   end
   return o
end

The default procedure doesn't change much from the previous parts. Construction from the file should take into account the specified type of the brick. If it is nonzero, it is assigned to the newly created brick; if it is zero, brick is not created at all. Otherwise, BricksContainer:construct_from_level() is similar to BricksContainer:default_level_construction_procedure():

function BricksContainer:construct_from_level()
   for row = 1, self.rows do
      local new_row = {}
      for col = 1, self.columns do
         local bricktype = self.level.bricks[row][col]           --(*1)
         if bricktype ~= 0 then                                  --(*2)
            local new_brick_position = self.top_left_position +
               vector(
                  ( col - 1 ) *
                     ( self.brick_width + self.horizontal_distance ),
                  ( row - 1 ) *
                     ( self.brick_height + self.vertical_distance ) )
            local new_brick = Brick:new{
               width = self.brick_width,
               height = self.brick_height,
               position = new_brick_position,
               bricktype = bricktype,
               collider = self.collider
            }
            new_row[ col ] = new_brick
         end
      end
      self.bricks[ row ] = new_row
   end   
end

(*1): Reading bricktype from the saved level description.
(*2): Only bricks with nonzero types are created.

To add a bit of diversity, we can make Brick:draw() function display various brick types a bit differently. Let's say, that bricks of type 1 will be in red, 2 - green, and 3 - blue.

function Brick:draw()
   love.graphics.rectangle( 'line',
				self.position.x,
				self.position.y,
				self.width,
				self.height )
   local r, g, b, a = love.graphics.getColor( )
   if self.bricktype == 1 then
	  love.graphics.setColor( 255, 0, 0, 100 )
   elseif self.bricktype == 2 then
	  love.graphics.setColor( 0, 255, 0, 100 )
   elseif self.bricktype == 3 then
	  love.graphics.setColor( 0, 0, 255, 100 )
   else 
	  love.graphics.setColor( 255, 255, 255, 100 ) 
   end	 
   self.collider_shape:draw( 'fill' )
   love.graphics.setColor( r, g, b, a )
end