Parsing a level file

The first thing we'll do for this project is work on the code that reads a level datafile and build an internal representation out of it; not actual sprites or images, but simply a table describing the various terrain elements, distinctive features, and interactive elements that the level area contains.

Getting ready

The level file format consists of two sections, separated by a blank line. The first section starts with a line that describes the type of terrain that will be used to represent the map: desert, cave, forest, mountain, and so on. The rest of the section is a sort of text picture of the level, rather like the maps used for the Sokoban levels in Project 2, SuperCargo – Using Events to Track Game Progress.

After the blank line is a list of items and objects that appear in the level; a name, possibly preceded by the name of an item-set module that specifies where the item's description is found, the position of a square or range of squares where the item will exist in the level, and any other details about the object, such as what a treasure chest contains or which level an exit leads to. A line might optionally consist of just an item-set name followed by a colon, which sets the default for any following item names that don't specify their own source set.

Note

There are also third-party tools that allow you to draw maps using a graphical interface, such as Tiled , which typically stores maps in XML or JSON files; libraries like Lime do some of the heavy lifting to load these files into your game. This method has the advantage that it makes it easy for designers to create maps visually; however, the maps aren't usually very human-readable and take up more disk space, as well as sometimes requiring more memory to process.

Getting on with it

Start by opening the file map.lua from the project folder. This file provides a single-function module that currently returns an empty table. There are two other functions already in the file, which we'll use to go through the two distinct sections of the file; the small one simply checks a line and returns if it's blank, and the more complicated one takes an iterator, such as we'd usually use to run a single for loop, and splits it into two functions, each of which runs its own for loop, first up to the first entry that meets the test, then the rest of the way through the iterator. We'll use these functions together to run one loop up to the first blank line (the map image) and one to process everything else in the file (object descriptions).

Note

Custom iterators are one of the most powerful constructions in Lua, but sadly not one of the most intuitive. We'll cover their power, as well as their construction, in greater detail in Project 9, Into the Woods – Computer Navigation of Environments.

Splitting the level into map and object data

First, obtain the two iterators that will process the whole file in two sections, separated by a blank line:

  local self = {}
  local layout, objects = split(blank, io.lines(filename))
  return self

Next, frame in the two loops that will use these sequences:

  local layout, objects = split(blank, io.lines(filename))
  for map_line in layout() do
  end
  for object in objects() do
  end
  return self

We'll fill in the first loop with some basic structure code to prepare for processing the second loop.

Creating the map canvas

In order to add objects to the map, we need to have the representation of those spaces stored in the map array, so we'll go over each line and character in the map image and add a corresponding space to the map.

Skipping the terrain description, add a row to the map for each line in the first part:

  for map_line in layout() do
    local terrain = map_line:match('^(.+):$')
    if terrain then
    else
      local row = {}
      table.insert(self, row)
    end
  end

For each character in the map line, add a space to the row. Each space will have additional data added to it when we develop the map portion more fully:

      local row = {}
      table.insert(self, row)
      for tile in map_line:gmatch('.') do
        local space = {
          Features = {},
        }
        table.insert(row, space)
      end
    end

Reading objects into the position data

Each line will either be an object including a set prefix, a set prefix alone to be used for later objects without one, or an object without any set prefix that uses the last declared prefix. So before the loop starts, we'll need to stake out a variable to store the last declared default prefix:

  end
  local family_group
  for object in objects() do

So, the first thing we need to do is check whether the line meets the description of a new default prefix; a word followed by a colon:

  for object in objects() do
    local family = objectName:match("^(%S+):$")
    if family then
      family_group = family
    end
  end

Otherwise, the object needs to be processed according to its identifier and position (and any other specifics):

    if family then
      family_group = family
    else
      local objectName, data = object:match("^(%S+)%s*(.*)$")
    end

If the object name doesn't contain its own set prefix, we need to apply the current default set prefix:

    else
      local objectName, data = object:match("^(%S+)%s*(.*)$")
      local family, name = objectName:match("^([^:]+):?(.*)$")
      if name == '' then
        family, name = family_group, family
      end
    end

Each object in the list has a position on the map, and some objects have more info; for example, an exit to another level should specify which level it connects to and where in the destination level it takes the player. We'll separate this optional data from the position info and split the position into its x and y parts, separated by a comma:

      if name == '' then
        family, name = family_group, family
      end
      local x, y, details = data:match("^([-%d]+),([-%d]+)%s*(.*)$")
    end

Each part is able to contain a range to support large objects, so we'll process the x and y portions to check for a dash or multiple numbers as needed:

      local x, y, details = data:match("^([-%d]+),([-%d]+)%s*(.*)$")
      local xMin, range, xMax = x:match('(%d*)(%-?)(%d*)')
      xMin = tonumber(xMin) or 1
      xMax = tonumber(xMax) or range ~= '-' and tonumber(xMin) or #self[1]
      local yMin, range, yMax = y:match('(%d*)(%-?)(%d*)')
      yMin = tonumber(yMin) or 1
      yMax = tonumber(yMax) or range ~= '-' and tonumber(yMin) or #self
    end

Finally, we'll generate the specified object and add it to each of the spaces in the specified range. Each set prefix will define a module in the objects package, containing one constructor function for each object in the set:

      yMax = tonumber(yMax) or range ~= '-' and tonumber(yMin) or #self
      for x = xMin, xMax do
        for y = yMin, yMax do
          table.insert(self[y][x].Features, require("objects."..family)[name](details))
        end
      end
    end

Save map.lua.

What did we do?

We skipped over the first section of a two-part file (for the most part), read each entry in the second section, and tracked a changeable setting across multiple entries. We stored instances of the specified objects in the locations they were tagged as belonging in.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.70.101