Saturday, September 29, 2012

Tutorial 18 - Saving and Loading Complicated Tables



18.1 Recap

   
Tutorial 18 brings together elements from a number of the previous Tutorials. It is an example of where asking the right question makes finding the answer much simpler. Our aim is to be able to load and save level data from our Tower Defence level generator. Most of the data we need is stored in a table. The tricky part is that Codea only allows us to save data as strings at the moment (some smart folks over at the Codea Forum have worked out how to jam data into image files and save those).
  
Tutorial 16 examined a technique for saving simple table structures like a one dimensional array. Initially we thought that saving a more complicated table wouldn't be that tough. All we needed was one of the existing Lua table to string parsers like Pickle / UnPickle. WRONG!
    

18.2 Limitations of Table Serialisation Parsers

  
We discussed Lua tables in Interlude 6. They are simple in concept but capable of representing very complicated structures. Tables can contain all the simple Lua types (booleans, strings, nil, and number), functions, other tables, closures, etc. The most complete table to string parser that we could find was Data Dumper. But even Data Dumper can not handle tables that contain:
  • Both light and full userdata
  • Coroutines
  • C functions
And of course our level data contains userdata (e.g. vec2 is represented using userdata). So where to from here?
    

18.3 The Modified Cell Class

   
This is where asking the right question makes all the difference. Looking at which data we actually need to save, it becomes apparent that we don't need to save the entire table (including functions) as this can all be reconstituted. All we really need from the Grid table is what is contained in each cell. So rather than trying to convert a two dimensional array of cell objects (which contain userdata) into a string, we extend the cell class to provides its contents (state) as a string. We also provide the inverse function which sets the cell state from a string so we can load the level back in. The extended cell class is shown below.
  
--# Cell
Cell = class()
-- dGenerator Cell Class
-- Reefwing Software
--
-- Version 1.2 (Modified from MineSweeper Cell Class)
--
-- Each element in the dGenerator two dimensional grid{}
-- consists of a cell object. Each cell is responsible for
-- tracking its own state and drawing the appropriate sprite
-- based on this state.
--
-- States available are: Obstacle, Clear, Start or Finish.
-- There can only be one start and one finish cell in the
-- matrix.
function Cell:init(i, j, state, x, y)
-- Cell Initialisation.
self.index = vec2(i, j) -- location of cell within the grid{} table
self.state = state -- contents of cell
self.pos = vec2(x, y) -- position of cell on the screen
self.action = nil -- call back function when cell tapped
self.size = vec2(32, 32) -- size of cell on screen
self.showGrid = false -- if true a border will be drawn around the cell
end
-- Cell Data Export and Import Functions (for saving and retrieving data)
function Cell:dataToString()
return self.state
end
function Cell:dataFromString(str)
self.state = str
end
-- Cell Draw Functions
function Cell:draw()
-- Codea does not automatically call this method
-- Draw the appropriate cell image based on its state.
if self.state == stateObstacle then
sprite("Dropbox:obstacleSprite", self.pos.x, self.pos.y)
elseif self.state == stateClear then
sprite("Dropbox:clearSprite", self.pos.x, self.pos.y)
elseif self.state == stateStart then
sprite("Dropbox:startSprite", self.pos.x, self.pos.y)
elseif self.state == stateEnd then
sprite("Dropbox:endSprite", self.pos.x, self.pos.y)
elseif self.state == statePath then
sprite("Dropbox:pathSprite", self.pos.x, self.pos.y)
end
-- If showGrid is true we draw a border around the cell.
if self.showGrid then
pushStyle()
noSmooth()
fill(clearColor)
stroke(darkGrayColor)
strokeWidth(1)
rectMode(CENTER)
rect(self.pos.x, self.pos.y, self.size.x, self.size.y)
popStyle()
end
end
-- Cell Touch Handling
function Cell:hit(p)
-- Was the touch on this cell?
-- Note code repurposed from the original button class
-- provide in the Codea examples.
local l = self.pos.x - self.size.x/2
local r = self.pos.x + self.size.x/2
local t = self.pos.y + self.size.y/2
local b = self.pos.y - self.size.y/2
if p.x > l and p.x < r and
p.y > b and p.y < t then
return true
end
return false
end
function Cell:touched(touch)
-- Codea does not automatically call this method
if touch.state == ENDED and self:hit(vec2(touch.x,touch.y)) then
if self.action then
-- call back method called.
self.action(self.index)
end
end
end
view raw Cell.lua hosted with ❤ by GitHub
  
Once we can get the grid cell contents out as a string it is easy to write a function to create a simple two dimensional table holding each of these strings. Data Dumper makes short work of converting this table to a string which we can save to global data. Even better, Data Dumper saves the table in a format that loadstring can use to rebuild the original table. The updated Main class for dGenerator details these save and load functions.
  
--# Main
-- dGenerator
--
-- This program is a WYSIWYG Level Editor which can be used
-- in a tower defence or Rogue type game.
--
-- We will use it to demonstrate our A* pathfinding tutorial.
supportedOrientations(ANY)
DEBUG = true -- If true additional debug information is printed.
-- Define the grid cell and App states
stateNil = 0
stateStart = 1
stateEnd = 2
stateClear = 3
stateObstacle = 4
stateCheck = 5
stateToggleGrid = 6
stateToggleDebug = 7
stateShowFileManager = 8
currentState = stateNil
function setup()
version = 1.1
saveProjectInfo("Description", "Level Generator for Tower Defence Game v"..version)
saveProjectInfo("Author", "Reefwing Software")
saveProjectInfo("Date", "30 August 2012")
saveProjectInfo("Version", version)
saveProjectInfo("Comments", "Original Release.")
print("-- dGenerator v"..version.."\n")
-- We will keep track of whether we have selected the
-- start and finish cell and if so the co-ordinates of
-- these two cells. This will save us having to iterate
-- through the entire table to find them.
startCellSelected = false
endCellSelected = false
startPos = nil
endPos = nil
-- Create a global variable to hold the current tap state
tapState = stateStart
-- Initialise the grid matrix, we will reuse a lot of the
-- code from our MineSweeper App. We have selected a sprite
-- size of 32 so you can use Spritely to generate your sprites
-- if you want. We used Sprite Something this time.
mSpriteSize = 32
gridWidth = 15
gridHeight = 15
-- baseX = WIDTH/2 - (mSpriteSize * gridWidth) / 2 + 15
baseX = 30
grid = {}
createGrid()
-- Create the 4 tap state menu select buttons & 4 action buttons
local bX = baseX + 33
local bY = 50
startButton = Button(bX, bY, true, "Start")
startButton.tag = stateStart
startButton.action = function()buttonPressed(stateStart) end
endButton = Button(bX + 74, bY, false, "End")
endButton.tag = stateEnd
endButton.action = function()buttonPressed(stateEnd) end
clearButton = Button(bX + 148, bY, false, "Path")
clearButton.tag = stateClear
clearButton.action = function()buttonPressed(stateClear) end
wallButton = Button(bX + 222, bY, false, "Wall")
wallButton.tag = stateWall
wallButton.action = function()buttonPressed(stateObstacle) end
gridButton = Button(bX + 326, bY, false, "Grid")
gridButton.tag = stateToggleGrid
gridButton.action = function()buttonPressed(stateToggleGrid) end
checkButton = Button(bX + 400, bY, false, "A*")
checkButton.tag = stateCheck
checkButton.action = function()buttonPressed(stateCheck) end
debugButton = Button(bX + 474, bY, true, "dBug")
debugButton.tag = stateToggleDebug
debugButton.action = function()buttonPressed(stateToggleDebug) end
exitButton = Button(bX + 548, bY, false, "Exit")
exitButton.action = function()close() end
-- Unlike the other buttons, we want the checkButton to be
-- momentary. To create this illusion we will use a counter
-- called checkCounter which will turn off the button after
-- 0.5 seconds.
checkCounter = 0
-- It is actually easier to use the procedurally generated
-- mesh buttons, so we will use these for the Load, Save and
-- Reset buttons.
local x, y = baseX - mSpriteSize/2, HEIGHT - 128
resetButton = MeshButton("Reset", x, y, 128, 50)
resetButton.action = function() resetGrid() end
loadButton = MeshButton("Load", x + 178, y, 128, 50)
loadButton.action = function() loadLevel() end
saveButton = MeshButton("Save", x + 356, y, 128, 50)
saveButton.action = function() saveLevel() end
-- Create the game name and level text boxes
y = HEIGHT - 50
gameNameTextBox = TextBox(x, y, 178+128, "<Enter Game Name>")
levelTextBox = TextBox(x + 356, y, 128, "<Level>")
-- Initialise the FileManager class
FileManager:init()
end
-- Menu Bar Call Back Methods
function cancelFileManager()
-- Custom implementation goes here
currentState = stateNil
end
function aboutFileManager()
-- Custom implementation goes here
end
function saveFile()
-- Custom implementation goes here
end
function loadFile()
-- Once file is selected load level
if currentDirectory == GlobalData and globalKeyTable[currentKey] ~= nil then
local keyString = globalKeyTable[currentKey]
local valString = readGlobalData(globalKeyTable[currentKey])
if DEBUG then
print("-- Loading Key: " .. keyString)
end
if string.starts(keyString, "dGEN") then
print("dGen - valid data key found.")
local keyArray = explode(",", keyString)
gameNameTextBox.text = keyArray[2]
levelTextBox.text = keyArray[3]
if string.match(keyArray[4], "true") == "true" then
print("dGen - start cell selected.")
startCellSelected = true
if startPos == nil then
startPos = vec2(keyArray[5], keyArray[6])
else
startPos.x = keyArray[5]
startPos.y = keyArray[6]
end
else
print("dGen - start cell not selected.")
startCellSelected = false
startPos.x = nil
startPos.y = nil
end
if string.match(keyArray[7], "true") == "true" then
print("dGen - end cell selected.")
endCellSelected = true
if endPos == nil then
endPos = vec2(keyArray[8], keyArray[9])
else
startPos.x = keyArray[8]
startPos.y = keyArray[9]
end
else
print("dGen - end cell not selected.")
endCellSelected = false
endPos.x = nil
endPos.y = nil
end
generateGrid = loadstring(valString)
createGrid(generateGrid())
print("dGen - Grid loaded.")
else
print("dGen ERROR - invalid file type.")
end
end
currentState = stateNil
end
function deleteFile()
if currentDirectory == nil then
-- No Directory Selected
elseif currentDirectory == ProjectData and projectKeyTable[currentKey] ~= nil then
saveProjectData(projectKeyTable[currentKey], nil)
loadProjectKeys()
displayKeys(projectKeyTable)
currentKey = 1
valueString = string.truncate(readProjectData(projectKeyTable[currentKey]) or "nil",150)
elseif currentDirectory == ProjectInfo and infoKeyTable[currentKey] ~= nil then
-- delete is not currently supported for ProjectInfo because we can't
-- get a list of the keys in this pList.
elseif currentDirectory == LocalData and localKeyTable[currentKey] ~= nil then
saveLocalData(localKeyTable[currentKey], nil)
loadLocalKeys()
displayKeys(localKeyTable)
currentKey = 1
valueString = string.truncate(readLocalData(localKeyTable[currentKey]) or "nil",150)
elseif currentDirectory == GlobalData and globalKeyTable[currentKey] ~= nil then
saveGlobalData(globalKeyTable[currentKey], nil)
loadGlobalKeys()
displayKeys(globalKeyTable)
currentKey = 1
valueString = string.truncate(readGlobalData(globalKeyTable[currentKey]) or "nil",150)
end
end
-- dGenerator Draw() functions
function draw()
-- This sets a dark background color
background(codeaDarkBackground)
if currentState == stateFileManager then
FileManager:draw()
else
-- Draw the grid
drawGrid()
-- Draw the tap state button tool bar
pushStyle()
local toolBarX = baseX - mSpriteSize/2
fill(lightGrayColor)
stroke(whiteColor)
strokeWidth(4)
rect(toolBarX, 7, 320, 84)
lineCapMode(SQUARE)
stroke(blackColor)
strokeWidth(6)
line(toolBarX + 2, 7, toolBarX + 2, 91)
line(toolBarX, 90, toolBarX + 320, 90)
-- If the checkButton is pressed start the checkCounter timer
-- this will unpress the button after 0.5 secs.
if checkButton.pressed then
checkCounter = checkCounter + DeltaTime
if checkCounter > 1 then
checkButton.pressed = false
checkCounter = 0
end
end
-- Draw the buttons
startButton:draw()
endButton:draw()
clearButton:draw()
wallButton:draw()
gridButton:draw()
checkButton:draw()
debugButton:draw()
exitButton:draw()
resetButton:draw()
loadButton:draw()
saveButton:draw()
-- And the Text Boxes
gameNameTextBox:draw()
levelTextBox:draw()
popStyle()
end
end
function drawGrid()
-- Iterate through the grid matrix and draw each cell
for i = 1, gridWidth do
for j = 1, gridHeight do
grid[i][j]: draw()
end
end
end
-- Load and Save Level Functions
function explode(div,str)
if (div=='') then return false end
local pos,arr = 0,{}
-- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do
table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
pos = sp + 1 -- Jump past current divider
end
table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
return arr
end
function saveLevel()
print("-- Saving Level Data.")
-- Create and populate a table (saveGrid) to save the grid state.
-- This way we can save and retrieve the level data.
--
-- Save Data string format:
--
-- key = "DGEN,gameName,levelNumberstartCellSelected,startPos.x,startPos.y,
-- endCellSelected,endPos.x,endPos.y"
--
-- value = "saveGrid"
local saveGrid = {}
for i = 1, gridWidth do
saveGrid[i] = {} -- create a new row
for j = 1, gridHeight do
saveGrid[i][j] = grid[i][j]:dataToString()
end
end
-- Note that no checks are made regarding whether game name or
-- level number has been entered by the user. Overwriting previous
-- levels also isn't checked.
local key = "dGEN," .. gameNameTextBox.text .. "," .. levelTextBox.text ..","
local suffix
if startCellSelected and startPos ~= nil then
suffix = "true," .. startPos.x .. "," .. startPos.y .. ","
else
suffix = "false,nil,nil,"
end
if endCellSelected and endPos ~= nil then
suffix = suffix .. "true," .. endPos.x .. "," .. endPos.y .. ","
else
suffix = suffix .. "false,nil,nil,"
end
key = key .. suffix
local saveString = DataDumper(saveGrid)
if DEBUG then
print("Key: " .. key)
print("Value: " .. saveString)
end
saveGlobalData(key, saveString)
end
function loadLevel()
print("-- Loading Level Data.")
print("-- Available Keys:")
currentState = stateFileManager
globalDataKeyTable = listGlobalData()
for i, v in ipairs(globalDataKeyTable) do
if string.starts(v, "dGEN") then
print(i..": "..v)
elseif i == #globalDataKeyTable and DEBUG then
print("-- End of Global Data File.")
end
end
end
-- Handle iPad Orientation Changes
function updateGridLocation(newOrientation)
-- This function is required to reposition the grid
-- if the iPad orientation changes.
baseX = 30
local y = HEIGHT/2 - (mSpriteSize * gridHeight) / 2
local x = baseX
for i = 1, gridWidth do
for j = 1, gridHeight do
grid[i][j].pos.x = x
grid[i][j].pos.y = y
x = x + mSpriteSize
end
x = baseX
y = y + mSpriteSize
end
local bX = baseX + 33
startButton.x = bX
endButton.x = bX + 74
clearButton.x = bX + 148
wallButton.x = bX + 222
local rbX, rbY = baseX - mSpriteSize/2, HEIGHT - 128
resetButton.x, resetButton.y = rbX, rbY
loadButton.x, loadButton.y = rbX + 178, rbY
saveButton.x, saveButton.y = rbX + 356, rbY
local tbY = HEIGHT - 50
gameNameTextBox.x, gameNameTextBox.y = rbX, tbY
levelTextBox.x, levelTextBox.y = rbX + 356, tbY
if newOrientation == LANDSCAPE_LEFT or newOrientation == LANDSCAPE_RIGHT then
gridButton.x = bX + 326
checkButton.x = bX + 400
debugButton.x = bX + 474
debugButton.y = checkButton.y
exitButton.x = bX + 548
exitButton.y = gridButton.y
else
local rbY = rbY - 130
gridButton.x = bX + 320
exitButton.x = gridButton.x
exitButton.y = gridButton.y + 74
checkButton.x = bX + 394
debugButton.x = checkButton.x
debugButton.y = checkButton.y + 74
resetButton.y = rbY
loadButton.y = rbY
saveButton.y = rbY
end
end
function orientationChanged(newOrientation)
if currentState == stateShowFileManager then
-- Update ListScroll co-ordinates for new orientation
local y = HEIGHT - 500
if directoryList ~= nil then
directoryList.pos.y = y
end
if dataKeyList ~= nil then
dataKeyList.pos.y = y
end
-- Update Menu Bar co-ordinates for new orientation
y = HEIGHT - 80
if b1tab ~= nil then
for i = 1, #b1tab do
b1tab[i].y = y
end
end
y = HEIGHT - 110
if b2tab ~= nil then
for i = 1, #b2tab do
b2tab[i].y = y
y = y - 30
end
end
else
updateGridLocation(newOrientation)
end
end
-- Grid creation and reset functions
function createGrid(obstacleGrid)
local y = HEIGHT/2 - (mSpriteSize * gridHeight) / 2
local x = baseX
-- Create the grid using nested tables.
-- It operates as a two dimensional array (or matrix)
if DEBUG then
if obstacleGrid == nil then
print("-- dGen: Creating Empty Grid.")
else
print("-- dGen: Creating Grid from File.")
end
end
for i = 1, gridWidth do
grid[i] = {} -- create a new row
for j = 1, gridHeight do
if obstacleGrid == nil then
grid[i][j] = Cell(i, j, stateObstacle, x, y)
else
grid[i][j] = Cell(i, j, obstacleGrid[i][j], x, y)
end
grid[i][j].action = function() handleCellTouch(grid[i][j].index) end
x = x + mSpriteSize
end
x = baseX
y = y + mSpriteSize
end
end
function resetGrid()
-- When the reset button is tapped this function will
-- reset the table
if DEBUG then
print("-- dGen: Resetting Grid.")
end
for i = 1, gridWidth do
for j = 1, gridHeight do
grid[i][j] = nil
end
end
grid = {}
createGrid()
if gridButton.pressed then
toggleGridState()
end
startCellSelected = false
endCellSelected = false
startPos = nil
endPos = nil
end
-- Cell Related Functions
--
-- We discussed closure functions in a separate tutorial, but
-- for now to understand what is going on in the count neighbouring cell
-- functions you need to know that when a function is enclosed in
-- another function, it has full access to local variables from the
-- enclosing function. In this example, inNeighbourCells() increments the local
-- variable obstacleNum in countObstacles().
function inNeighbourCells(startX, endX, startY, endY, closure)
for i = math.max(startX, 1), math.min(endX, gridWidth) do
for j = math.max(startY, 1), math.min(endY, gridHeight) do
closure(i, j)
end
end
end
function countObstacles(index)
local obstacleNum = 0
inNeighbourCells(index.x - 1, index.x + 1, index.y - 1, index.y + 1,
function(x, y) if grid[x][y].state == stateObstacle then
obstacleNum = obstacleNum + 1 end
end)
return obstacleNum
end
-- Button Action Methods
function toggleGridState()
-- This function will toggle whether the grid overlay is shown.
for i = 1, gridWidth do
for j = 1, gridHeight do
grid[i][j].showGrid = not grid[i][j].showGrid
end
end
end
function toggleDebugState()
-- This function will toggle whether the Debug window is shown.
if debugButton.pressed then
displayMode(STANDARD)
DEBUG = true
else
displayMode(FULLSCREEN_NO_BUTTONS)
DEBUG = false
end
end
function clearPath()
-- Clear the path from the Grid.
if DEBUG then
print("-- dGEN: Clearing Path from Grid")
end
for i = 1, gridWidth do
for j = 1, gridHeight do
if grid[i][j].state == statePath then
grid[i][j].state = stateClear
end
end
end
end
function findPath()
-- If a start and end cell has been defined, use
-- the A* algorithm to find and display a path.
if startCellSelected then
if endCellSelected then
print("-- dGEN: Calculating A* Path.")
path = CalcPath(CalcMoves(grid, startPos.x, startPos.y, endPos.x, endPos.y))
if path == nil then
print("-- dGEN No path found.")
else
print("-- dGEN: Path found:\n")
if DEBUG then
print(to_string(path, 2))
end
-- start and end one cell early so we dont overwrite
-- the start and end cell status.
for i = 2, table.getn(path) - 1 do
grid[path[i].x][path[i].y].state = statePath
end
end
else
print("--dGEN ERROR: Path can't be found until end cell is selected.")
end
else
print("--dGEN ERROR: Path can't be found until start cell is selected.")
end
end
-- Touch Handling
function buttonPressed(index)
if index < stateCheck then
tapState = index
end
if index == stateStart then
endButton.pressed = false
clearButton.pressed = false
wallButton.pressed = false
elseif index == stateEnd then
startButton.pressed = false
clearButton.pressed = false
wallButton.pressed = false
elseif index == stateClear then
startButton.pressed = false
endButton.pressed = false
wallButton.pressed = false
elseif index == stateObstacle then
startButton.pressed = false
endButton.pressed = false
clearButton.pressed = false
elseif index == stateToggleGrid then
toggleGridState()
elseif index == stateCheck then
findPath()
elseif index == stateToggleDebug then
toggleDebugState()
else
print("-- dGEN WARNING: Unknown button index pressed.")
end
end
function touched(touch)
if currentState == stateFileManager then
FileManager:touched(touch)
else
-- Pass through touch handling to buttons, textbox and the grid cells
startButton:touched(touch)
endButton:touched(touch)
clearButton:touched(touch)
wallButton:touched(touch)
gridButton:touched(touch)
checkButton:touched(touch)
debugButton:touched(touch)
exitButton:touched(touch)
resetButton:touched(touch)
saveButton:touched(touch)
loadButton:touched(touch)
gameNameTextBox:touched(touch)
levelTextBox:touched(touch)
for i = 1, gridWidth do
for j = 1, gridHeight do
grid[i][j]:touched(touch)
end
end
end
end
function handleCellTouch(index)
if tapState == stateStart and startCellSelected then
print("-- dGEN Warning: Only one cell may be assigned as the start cell.")
elseif tapState == stateEnd and endCellSelected then
print("-- dGEN Warning: Only one cell may be assigned as the end cell.")
else
if tapState == stateStart then
startCellSelected = true
startPos = vec2(index.x, index.y)
elseif tapState == stateEnd then
endCellSelected = true
endPos = vec2(index.x, index.y)
end
if grid[index.x][index.y].state == stateStart then
startCellSelected = false
startPos = nil
clearPath()
elseif grid[index.x][index.y].state == stateEnd then
endCellSelected = false
endPos = nil
clearPath()
end
grid[index.x][index.y].state = tapState
end
end
-- KeyBoard handling function
-- Used to enter game name and level
function keyboard(key)
-- Add text to the textbox which has focus
local mTextBox = gameNameTextBox
if levelTextBox.hasFocus then
mTextBox = levelTextBox
end
if key ~= nil then
if string.byte(key) == 10 then -- <RETURN> Key pressed
hideKeyboard()
mTextBox.hasFocus = false
elseif string.byte(key) ~= 44 then -- filter out commas
mTextBox:acceptKey(key)
end
end
end
-- Some String Helper Functions:
function string.starts(String, Start)
return string.sub(String, 1, string.len(Start)) == Start
end
view raw Main.lua hosted with ❤ by GitHub

We updated the File Manager class from Tutorial 17 to allow us to select the file to be loaded. As part of this update we got rid of the submenus because we didn't need them and they meant that the user had to do an extra tap to load. In addition, Delete was right next to Load which is poor design. One slip of the finger would be potentially disastrous (particularly because there is no confirmation for delete and no undo). The other improvement to this version of File Manager is we truncate keys and values which are larger than the ListScroll widths. You can see the upgraded version in the screenshot below. The About menu item doesn't do anything at this stage.
     

18.4 Loading & Saving Data

   
Tapping the Save button on the main screen of dGenerator will call the saveLevel() function in Main(). The function starts off by saving the simplified table data in saveGrid. It then generates the key which contains a header "dGen" (to indicate when loading if it is the right data type), the game name and level number, a boolean indicating if the start cell has been selected and its co-ordinates and then a boolean indicating if the end cell has been selected and its co-ordinates. Finally we use Data Dumper to convert saveGrid to a string and save the key and value to global data.
  
Loading a level is the reverse. Tapping the Load button on the dGenerator main screen will call the loadLevel() function in Main. This prints out some debug data but its only compulsory action is to set the App state to stateFileManager. This will draw the File Manager instead of the main screen and handle its touches.
  
Once the user has navigated to global data and selected an appropriate key containing level data, tapping load on the File Manager menu bar will call the loadFile() function in Main. This function splits the key back into its component parts using the explode() helper function and then uses loadstring() to create a function which rebuilds the data table. The createGrid() function was updated so that it can be loaded using this data table.
  
And that is all there is to it. You can use this link to download the entire code including the updated classes.
  
Next up we will look at adding creeps to your levels in a number of waves.

No comments:

Post a Comment