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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# 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 |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# 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 |
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