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.
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.
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.
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).
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.
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.
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
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
.
3.16.70.101