Skip to content
WarpspeedSCP edited this page May 25, 2016 · 13 revisions

« 1c More Tables | index | 1e Functions »

On this step we’ll build the TileTable variable by parsing a string.

The complete source code of this step can be found here:

Prerequisites

This step assumes that you know what multi-line strings, pattern matching and generic loops are. They are explained on:

The problem

Using TileTable instead of manually drawing each tile was certainly an improvement. But it wasn’t exempt from problems. For instance:

  • TileTable is indexed first by the y-component, and then by x (TileTable[y][x]). That goes against all the programming and math conventions, in which x precedes y (TileTable[x][y])
  • For the tile types, we want something more graphically expressive than digits. Characters sound like a good alternative.
  • We want to keep the typing to the bare minimum. Remove all the unnecessary characters – colons, commas, apostrophes, etc.

It seems like a job for strings.

Assigning chars to quads

Before we assigned a number to each quad. Now we’ll assign characters instead.

When doing this, it is a good idea to use characters whose shape reminds of the tile in question.

I’ll use space (’ ‘) for the grass quad, ’#’ for the box, ‘^’ for the box top, and ‘*’ for the flower quad.

Before we where using numbers for indexing the quadInfo table:

local quadInfo = {
  {  0,  0 }, -- 1 = grass 
  { 32,  0 }, -- 2 = box
  {  0, 32 }, -- 3 = flowers
  { 32, 32 }  -- 4 = boxTop
}

Now we’ll use characters:

local quadInfo = {
  { ' ', 0,  0  }, -- grass 
  { '#', 32,  0 }, -- box
  { '*', 0, 32  }, -- flowers
  { '^', 32, 32 }  -- boxTop
}

The Quad calculation has to be modified to take into account the new quadInfo structure, and the fact that we use characters instead of numbers.

Quads = {}
for _,info in ipairs(quadInfo) do
  -- info[1] = character, info[2]= x, info[3] = y
  Quads[info[1]] = love.graphics.newQuad(info[2], info[3], TileW, TileH, tilesetW, tilesetH)
end

The tileString string

Let’s remember the level we built on the previous step:

We could encode that level on a multi-line string the following way:

  local tileString = [[
^#######################^
^                    *  ^
^  *                    ^
^              *        ^
^                       ^
^    ##  ^##  ^## ^ ^   ^
^   ^  ^ ^  ^ ^   ^ ^   ^
^   ^  ^ ^ *# ^   ^ ^   ^
^   ^  ^ ^##  ^## # #   ^
^   ^  ^ ^  ^ ^    ^  * ^
^ * ^  ^ ^  ^ ^    ^    ^
^   #  # ^* # ^  * ^    ^
^    ##  ###  ###  #    ^
^                       ^
^   *****************   ^
^                       ^
^  *                  * ^
#########################
]]

Let me put the data structure we had before, TileTable, for the sake of comparing:
TileTable = {
  { 4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,4 },
  { 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,4 },
  { 4,1,3,1,1,1,1,1,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,4 },
  { 4,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,4 },
  { 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4 },
  { 4,1,1,4,1,1,1,1,1,2,2,1,1,4,1,1,1,4,1,4,2,2,1,1,4 },
  { 4,1,1,4,1,1,1,1,4,3,3,4,1,2,1,1,1,2,1,4,1,1,1,1,4 },
  { 4,1,1,4,1,1,1,1,4,3,3,4,1,1,4,1,4,1,1,4,2,2,1,1,4 },
  { 4,1,1,4,1,1,1,1,4,3,3,4,1,1,2,1,2,1,1,4,1,1,1,1,4 },           
  { 4,1,1,4,1,1,1,1,2,3,3,2,1,1,1,4,1,1,1,4,1,1,1,1,4 },
  { 4,1,1,2,2,2,2,1,1,2,2,1,1,1,1,2,1,3,1,2,2,2,1,1,4 },
  { 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4 },
  { 4,1,1,1,1,1,1,1,1,2,1,1,1,1,2,2,4,1,1,1,1,1,1,1,4 },
  { 4,1,1,1,1,1,1,1,4,3,4,1,1,1,1,1,2,1,1,1,1,1,1,1,4 },
  { 4,1,1,3,1,1,1,1,2,3,2,1,1,1,1,2,1,1,1,1,1,1,1,1,4 },
  { 4,1,1,1,1,1,1,1,1,2,1,1,2,1,2,1,1,1,1,1,1,1,3,1,4 },
  { 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4 },
  { 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 }
}

The string version is easier to read and modify. It is a more adequate medium for the kind of data we want to represent.

It is, however, unusable on its current state. You see, you can’t do tileString[x][y] (or tileString[y][x]) to obtain the character that is on x, y. And you can’t iterate over the characters there as easily as you could with a table.

Using characters on a table kind of defeats the purpose of using characters in the first place; readability and ease of edition are gone.

TileTable = {
  { '^','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','*',' ','^' },
  { '^',' ','*',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','*',' ',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ','#',' ',' ','#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ','^',' ',' ',' ',' ',' ','#','#',' ',' ','^',' ',' ',' ','^',' ','^','#','#',' ',' ','^' },
  { '^',' ',' ','^',' ',' ',' ',' ','^','*','*','^',' ','#',' ',' ',' ','#',' ','^',' ',' ',' ',' ','^' },
  { '^',' ',' ','^',' ',' ',' ',' ','^','*','*','^',' ',' ','^',' ','^',' ',' ','^','#','#',' ',' ','^' },
  { '^',' ',' ','^',' ',' ',' ',' ','^','*','*','^',' ',' ','#',' ','#',' ',' ','^',' ',' ',' ',' ','^' },           
  { '^',' ',' ','^',' ',' ',' ',' ','#','*','*','#',' ',' ',' ','^',' ',' ',' ','^',' ',' ',' ',' ','^' },
  { '^',' ',' ','#','#','#','#',' ',' ','#','#',' ',' ',' ',' ','#',' ','*',' ','#','#','#',' ',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ',' ','#',' ',' ',' ',' ','#','#','^',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ','^','*','^',' ',' ',' ',' ',' ','#',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ','*',' ',' ',' ',' ','#','*','#',' ',' ',' ',' ','#',' ',' ',' ',' ',' ',' ',' ',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ',' ','#',' ',' ','#',' ','#',' ',' ',' ',' ',' ',' ',' ','*',' ','^' },
  { '^',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','^' },
  { '#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#' }
}

This construction would be nice to use, but not to modify – it’s got just too many characters.

Plus, that table is indexed with the y component first, and x second. We want it the other way around.

The solution is, of course, using tileString to generate that second tiletable TileTable (with x and y conveniently switched). This way, we can specify and modify the tiles in a way that is easy for the programmer, as well as having them arranged on a table that allows easy computerized manipulation.

We can do that with some nested loops.

tileString parsing

Once tileString is defined as mentioned above, the following code can generate a usable TileTable

TileTable = {}

local width = #(tileString:match("[^\n]+"))

for x = 1,width,1 do TileTable[x] = {} end  

local rowIndex,columnIndex = 1,1
for row in tileString:gmatch("[^\n]+") do
  assert(#row == width, 'Map is not aligned: width of row ' .. tostring(rowIndex) .. ' should be ' .. tostring(width) .. ', but it is ' .. tostring(#row))
  columnIndex = 1
  for character in row:gmatch(".") do
    TileTable[columnIndex][rowIndex] = character
    columnIndex = columnIndex + 1
  end
  rowIndex=rowIndex+1
end

As always, let’s analyze it line by line.

TileTable = {}

That’s initializing the TileTable variable with an empty string. Let’s move a long.

Calculating width

local width = #(tileString:match("[^\n]+"))

Now what the hell is THAT?

That line is actually calculating the width of our map. It gets the first line, gets its length with the # operator, and assigns that to a variable called width. In other words, it is equivalent to:

local firstLine = tileString:match("[^\n]+")
local width = #(firstLine)

So we are invoking the match method with this pattern "[^\n]+". That pattern has been carefully crafted so it means “all characters between the start and a newline character”. I’m not going to explain in detail how it works; you can deduce how that works by reading the Lua Pattern Tutorial on the LuaUsers wiki.

So, after that line, we know how wide our map is going to be.

Creating the columns

for x = 1,width,1 do TileTable[x] = {} end

That line is initializing TileTable with a number of empty tables that depends on the value of width. For example, if width=3, then TileTable will have 3 empty tables inside: { {}, {}, {} }

Since we are creating the columns first, we are making sure that our TileTable is indexed first by x, then by y.

Parsing rows

local rowIndex,columnIndex = 1,1
for row in tileString:gmatch("[^\n]+") do

The first line there initializes two local variables called rowIndex and columnIndex to 1.

The second line starts a generic loop that uses string.gmatch as its iterator function (if you don’t understand those concepts, please review the loops tutorial).

On each iteration, string.gmatch(pattern) returns the substrings matched by pattern. The pattern we are using("[^\n]+") is the same as before – it returns all the characters between each 2 newline characters. The visible effect is that on each iteration, the variable row contains one line of tileString, starting on the first line.

Width invariant

  assert(#row == width, 'Map is not aligned: width of row ' .. tostring(rowIndex) .. ' should be ' .. tostring(width) .. ', but it is ' .. tostring(#row))

That line checks that your map is properly “aligned”. That means that all rows have the same width as the first one. If that is not the case, the game will throw an error.

Parsing cells

  columnIndex = 1
  for character in row:gmatch(".") do

Resetting columnIndex to 1 on the first line, and starting a nested generic loop, but this time iterating over the characters of row (the "." pattern means “any char”). Inside the body of this second loop, we’ll iteratively have all the characters that row contains, in order.

    TileTable[columnIndex][rowIndex] = character
    columnIndex = columnIndex + 1

Once we have a character (in the variable called character) and x and y coordinates, we can set them up in TileTable.

We also need to manually increase the value of x (string.gmatch doesn’t provide a “counter” variable for doing it more automatically).

Ending the loops

  end
  rowIndex=rowIndex+1
end

The first end closes the loop parsing the cells in a row, while the second closes the external loop, the one that parsing the rows on the map. In between, it is necessary to manually increase the value of y, for the same reason that it was needed for x.

love.draw

love.draw will work much like before, with a couple changes:

function love.draw()

  for columnIndex,column in ipairs(TileTable) do
    for rowIndex,char in ipairs(column) do
      local x,y = (columnIndex-1)*TileW, (rowIndex-1)*TileH
      love.graphics.draw(Tileset, Quads[char], x, y)
    end
  end

end

The changes are:

  • First of all, the external loop is now iterating over the columns instead of the rows of TileTable, and the internal loop is the other way around. This is a direct consequence of rearranging TileTable so it is indexed with columns first.
  • The second change is that we have renamed the internal value variable. It was called number, and now it’s called char, to reflect that what we have in TileTable are characters, not numbers.

Result

We were able to specify our tiles nicely, in a compact, expressive way, and we used some nested loops for making it understandable for the computer.

On the next step, we’ll keep using tables to make our code smaller and more dry.

Exercises

  1. Experiment changing the tiles on TileString
  2. Change the grass character from a space (’ ‘) to a dot (’.’). Change the Quads & TileString variables accordingly.

« 1c More Tables | index | 1e Functions »