Tutorial details

Tutorial 18 - Saving and Loading Complicated Tables | App Code for Sale | Preview

Tutorial 18 - Saving and Loading Complicated Tables | iOS Tutorial

How to load and save level data from our Tower Defence level generator

Overview PAGE TOP

jhk.PNG

18.1 Recap PAGE TOP

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 PAGE TOP

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 PAGE TOP

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.

1   --# Cell
2   Cell = class()
3   
4   -- dGenerator Cell Class
5   -- Reefwing Software
6   --
7   -- Version 1.2 (Modified from MineSweeper Cell Class)
8   --
9   -- Each element in the dGenerator two dimensional grid{}
10  -- consists of a cell object. Each cell is responsible for
11  -- tracking its own state and drawing the appropriate sprite
12  -- based on this state.
13  --
14  -- States available are: Obstacle, Clear, Start or Finish.
15  -- There can only be one start and one finish cell in the
16  -- matrix.
17  
18  function Cell:init(i, j, state, x, y)
19  
20     -- Cell Initialisation.
21  
22     self.index = vec2(i, j)             -- location of cell within the grid{} table
23     self.state = state                  -- contents of cell
24     self.pos = vec2(x, y)               -- position of cell on the screen
25     self.action = nil                   -- call back function when cell tapped
26     self.size = vec2(32, 32)            -- size of cell on screen
27     self.showGrid = false               -- if true a border will be drawn around the cell
28  
29  end
30  
31  -- Cell Data Export and Import Functions (for saving and retrieving data)
32  
33  function Cell:dataToString()
34  
35     return self.state
36  
37  end
38  
39  function Cell:dataFromString(str)
40  
41     self.state = str
42  
43  end
44  
45  -- Cell Draw Functions
46  
47  function Cell:draw()
48  
49     -- Codea does not automatically call this method
50     -- Draw the appropriate cell image based on its state.
51  
52     if self.state == stateObstacle then
53         sprite("Dropbox:obstacleSprite", self.pos.x, self.pos.y)
54     elseif self.state == stateClear then
55         sprite("Dropbox:clearSprite", self.pos.x, self.pos.y)
56     elseif self.state == stateStart then
57         sprite("Dropbox:startSprite", self.pos.x, self.pos.y)
58     elseif self.state == stateEnd then
59         sprite("Dropbox:endSprite", self.pos.x, self.pos.y)
60     elseif self.state == statePath then
61         sprite("Dropbox:pathSprite", self.pos.x, self.pos.y)
62     end
63  
64     -- If showGrid is true we draw a border around the cell.
65  
66     if self.showGrid then
67         pushStyle()
68         noSmooth()
69         fill(clearColor)
70         stroke(darkGrayColor)
71         strokeWidth(1)
72         rectMode(CENTER)
73         rect(self.pos.x, self.pos.y, self.size.x, self.size.y)
74         popStyle()
75     end
76  
77  end
78  
79  -- Cell Touch Handling
80  
81  function Cell:hit(p)
82  
83     -- Was the touch on this cell?
84     -- Note code repurposed from the original button class
85     -- provide in the Codea examples.
86  
87     local l = self.pos.x - self.size.x/2
88     local r = self.pos.x + self.size.x/2
89     local t = self.pos.y + self.size.y/2
90     local b = self.pos.y - self.size.y/2
91     if p.x > l and p.x < r and
92        p.y > b and p.y < t then
93         return true
94     end
95     return false
96  
97  end
98  
99  function Cell:touched(touch)
100 
101    -- Codea does not automatically call this method
102 
103    if touch.state == ENDED and self:hit(vec2(touch.x,touch.y)) then
104 
105        if self.action then
106            -- call back method called.
107            self.action(self.index)
108        end
109 
110    end
111 
112 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.

1   --# Main
2   -- dGenerator
3   --
4   -- This program is a WYSIWYG Level Editor which can be used
5   -- in a tower defence or Rogue type game.
6   --
7   -- We will use it to demonstrate our A* pathfinding tutorial.
8   
9   supportedOrientations(ANY)
10  
11  DEBUG = true        -- If true additional debug information is printed.
12  
13  -- Define the grid cell and App states
14  
15  stateNil = 0
16  stateStart = 1
17  stateEnd = 2
18  stateClear = 3
19  stateObstacle = 4
20  stateCheck = 5
21  stateToggleGrid = 6
22  stateToggleDebug = 7
23  stateShowFileManager = 8
24  
25  currentState = stateNil
26  
27  function setup()
28  
29     version = 1.1
30  
31     saveProjectInfo("Description", "Level Generator for Tower Defence Game v"..version)
32     saveProjectInfo("Author", "Reefwing Software")
33     saveProjectInfo("Date", "30 August 2012")
34     saveProjectInfo("Version", version)
35     saveProjectInfo("Comments", "Original Release.")
36  
37     print("-- dGenerator v"..version.."\n")
38  
39     -- We will keep track of whether we have selected the
40     -- start and finish cell and if so the co-ordinates of
41     -- these two cells. This will save us having to iterate
42     -- through the entire table to find them.
43  
44     startCellSelected = false
45     endCellSelected = false
46  
47     startPos = nil
48     endPos = nil
49  
50     -- Create a global variable to hold the current tap state
51  
52     tapState = stateStart
53  
54     -- Initialise the grid matrix, we will reuse a lot of the
55     -- code from our MineSweeper App. We have selected a sprite
56     -- size of 32 so you can use Spritely to generate your sprites
57     -- if you want. We used Sprite Something this time.
58  
59     mSpriteSize = 32
60     gridWidth = 15
61     gridHeight = 15
62     -- baseX = WIDTH/2 - (mSpriteSize * gridWidth) / 2 + 15
63     baseX = 30
64     grid = {}
65     createGrid()
66  
67     -- Create the 4 tap state menu select buttons & 4 action buttons
68  
69     local bX = baseX + 33
70     local bY = 50
71  
72     startButton = Button(bX, bY, true, "Start")
73     startButton.tag = stateStart
74     startButton.action = function()buttonPressed(stateStart) end
75  
76     endButton = Button(bX + 74, bY, false, "End")
77     endButton.tag = stateEnd
78     endButton.action = function()buttonPressed(stateEnd) end
79  
80     clearButton = Button(bX + 148, bY, false, "Path")
81     clearButton.tag = stateClear
82     clearButton.action = function()buttonPressed(stateClear) end
83  
84     wallButton = Button(bX + 222, bY, false, "Wall")
85     wallButton.tag = stateWall
86     wallButton.action = function()buttonPressed(stateObstacle) end
87  
88     gridButton = Button(bX + 326, bY, false, "Grid")
89     gridButton.tag = stateToggleGrid
90     gridButton.action = function()buttonPressed(stateToggleGrid) end
91  
92     checkButton = Button(bX + 400, bY, false, "A*")
93     checkButton.tag = stateCheck
94     checkButton.action = function()buttonPressed(stateCheck) end
95  
96     debugButton = Button(bX + 474, bY, true, "dBug")
97     debugButton.tag = stateToggleDebug
98     debugButton.action = function()buttonPressed(stateToggleDebug) end
99  
100    exitButton = Button(bX + 548, bY, false, "Exit")
101    exitButton.action = function()close() end
102 
103    -- Unlike the other buttons, we want the checkButton to be
104    -- momentary. To create this illusion we will use a counter
105    -- called checkCounter which will turn off the button after
106    -- 0.5 seconds.
107 
108    checkCounter = 0
109 
110    -- It is actually easier to use the procedurally generated
111    -- mesh buttons, so we will use these for the Load, Save and
112    -- Reset buttons.
113 
114    local x, y = baseX - mSpriteSize/2, HEIGHT - 128
115 
116    resetButton = MeshButton("Reset", x, y, 128, 50)
117    resetButton.action = function() resetGrid() end
118 
119    loadButton = MeshButton("Load", x + 178, y, 128, 50)
120    loadButton.action = function() loadLevel() end
121 
122    saveButton = MeshButton("Save", x + 356, y, 128, 50)
123    saveButton.action = function() saveLevel() end
124 
125    -- Create the game name and level text boxes
126 
127    y = HEIGHT - 50
128 
129    gameNameTextBox = TextBox(x, y, 178+128, "<Enter Game Name>")
130    levelTextBox = TextBox(x + 356, y, 128, "<Level>")
131 
132    -- Initialise the FileManager class
133 
134    FileManager:init()
135 
136 end
137 
138 -- Menu Bar Call Back Methods
139 
140 function cancelFileManager()
141    -- Custom implementation goes here
142    currentState = stateNil
143 end
144 
145 function aboutFileManager()
146    -- Custom implementation goes here
147 end
148 
149 function saveFile()
150    -- Custom implementation goes here
151 end
152 
153 function loadFile()
154 
155    -- Once file is selected load level
156 
157    if currentDirectory == GlobalData and globalKeyTable[currentKey] ~= nil then
158        local keyString = globalKeyTable[currentKey]
159        local valString = readGlobalData(globalKeyTable[currentKey])
160        if DEBUG then
161            print("-- Loading Key: " .. keyString)
162        end
163        if string.starts(keyString, "dGEN") then
164            print("dGen - valid data key found.")
165            local keyArray = explode(",", keyString)
166            gameNameTextBox.text = keyArray[2]
167            levelTextBox.text = keyArray[3]
168            if string.match(keyArray[4], "true") == "true" then
169                print("dGen - start cell selected.")
170                startCellSelected = true
171                if startPos == nil then
172                    startPos = vec2(keyArray[5], keyArray[6])
173                else
174                    startPos.x = keyArray[5]
175                    startPos.y = keyArray[6]
176                end
177            else
178                print("dGen - start cell not selected.")
179                startCellSelected = false
180                startPos.x = nil
181                startPos.y = nil
182            end
183            if string.match(keyArray[7], "true") == "true" then
184                print("dGen - end cell selected.")
185                endCellSelected = true
186                if endPos == nil then
187                    endPos = vec2(keyArray[8], keyArray[9])
188                else
189                    startPos.x = keyArray[8]
190                    startPos.y = keyArray[9]
191                end
192            else
193                print("dGen - end cell not selected.")
194                endCellSelected = false
195                endPos.x = nil
196                endPos.y = nil
197            end
198            generateGrid = loadstring(valString)
199            createGrid(generateGrid())
200            print("dGen - Grid loaded.")
201        else
202            print("dGen ERROR - invalid file type.")
203        end
204 
205    end
206 
207    currentState = stateNil
208 
209 end
210 
211 function deleteFile()
212    if currentDirectory == nil then
213        -- No Directory Selected
214    elseif currentDirectory == ProjectData and projectKeyTable[currentKey] ~= nil then
215        saveProjectData(projectKeyTable[currentKey], nil)
216        loadProjectKeys()
217        displayKeys(projectKeyTable)
218        currentKey = 1
219        valueString = string.truncate(readProjectData(projectKeyTable[currentKey]) or "nil",150)
220    elseif currentDirectory == ProjectInfo and infoKeyTable[currentKey] ~= nil then
221        -- delete is not currently supported for ProjectInfo because we can't
222        -- get a list of the keys in this pList.
223    elseif currentDirectory == LocalData and localKeyTable[currentKey] ~= nil then
224        saveLocalData(localKeyTable[currentKey], nil)
225        loadLocalKeys()
226        displayKeys(localKeyTable)
227        currentKey = 1
228        valueString = string.truncate(readLocalData(localKeyTable[currentKey]) or "nil",150)
229    elseif currentDirectory == GlobalData and globalKeyTable[currentKey] ~= nil then
230        saveGlobalData(globalKeyTable[currentKey], nil)
231        loadGlobalKeys()
232        displayKeys(globalKeyTable)
233        currentKey = 1
234        valueString = string.truncate(readGlobalData(globalKeyTable[currentKey]) or "nil",150)
235    end
236 end
237 
238 -- dGenerator Draw() functions
239 
240 function draw()
241 
242    -- This sets a dark background color
243 
244    background(codeaDarkBackground)
245 
246    if currentState == stateFileManager then
247 
248        FileManager:draw()
249 
250    else
251 
252        -- Draw the grid
253 
254        drawGrid()
255 
256        -- Draw the tap state button tool bar
257 
258        pushStyle()
259        local toolBarX = baseX - mSpriteSize/2
260 
261        fill(lightGrayColor)
262        stroke(whiteColor)
263        strokeWidth(4)
264        rect(toolBarX, 7, 320, 84)
265 
266        lineCapMode(SQUARE)
267        stroke(blackColor)
268        strokeWidth(6)
269        line(toolBarX + 2, 7, toolBarX + 2, 91)
270        line(toolBarX, 90, toolBarX + 320, 90)
271 
272        -- If the checkButton is pressed start the checkCounter timer
273        -- this will unpress the button after 0.5 secs.
274 
275        if checkButton.pressed then
276            checkCounter = checkCounter + DeltaTime
277            if checkCounter > 1 then
278                checkButton.pressed = false
279                checkCounter = 0
280            end
281        end
282 
283        -- Draw the buttons
284 
285        startButton:draw()
286        endButton:draw()
287        clearButton:draw()
288        wallButton:draw()
289        gridButton:draw()
290        checkButton:draw()
291        debugButton:draw()
292        exitButton:draw()
293        resetButton:draw()
294        loadButton:draw()
295        saveButton:draw()
296 
297        -- And the Text Boxes
298 
299        gameNameTextBox:draw()
300        levelTextBox:draw()
301 
302        popStyle()
303 
304   end
305 
306 end
307 
308 function drawGrid()
309 
310    -- Iterate through the grid matrix and draw each cell
311 
312    for i = 1, gridWidth do
313        for j = 1, gridHeight do
314            grid[i][j]: draw()
315        end
316    end
317 
318 end
319 
320 -- Load and Save Level Functions
321 
322 function explode(div,str)
323 
324    if (div=='') then return false end
325    local pos,arr = 0,{}
326    -- for each divider found
327    for st,sp in function() return string.find(str,div,pos,true) end do
328        table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
329        pos = sp + 1 -- Jump past current divider
330    end
331    table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
332    return arr
333 
334 end
335 
336 function saveLevel()
337 
338    print("-- Saving Level Data.")
339 
340    -- Create and populate a table (saveGrid) to save the grid state.
341    -- This way we can save and retrieve the level data.
342    --
343    -- Save Data string format:
344    --
345    -- key = "DGEN,gameName,levelNumberstartCellSelected,startPos.x,startPos.y,
346    --       endCellSelected,endPos.x,endPos.y"
347    --
348    -- value = "saveGrid"
349 
350    local saveGrid = {}
351 
352    for i = 1, gridWidth do
353        saveGrid[i] = {}     -- create a new row
354        for j = 1, gridHeight do
355            saveGrid[i][j] = grid[i][j]:dataToString()
356        end
357    end
358 
359    -- Note that no checks are made regarding whether game name or
360    -- level number has been entered by the user. Overwriting previous
361    -- levels also isn't checked.
362 
363    local key = "dGEN," .. gameNameTextBox.text .. "," .. levelTextBox.text ..","
364    local suffix
365 
366    if startCellSelected and startPos ~= nil then
367        suffix = "true," .. startPos.x .. "," .. startPos.y .. ","
368    else
369        suffix = "false,nil,nil,"
370    end
371 
372    if endCellSelected and endPos ~= nil then
373        suffix = suffix .. "true," .. endPos.x .. "," .. endPos.y .. ","
374    else
375        suffix = suffix .. "false,nil,nil,"
376    end
377 
378    key = key .. suffix
379 
380    local saveString = DataDumper(saveGrid)
381 
382    if DEBUG then
383        print("Key: " .. key)
384        print("Value: " .. saveString)
385    end
386 
387    saveGlobalData(key, saveString)
388 
389 end
390 
391 function loadLevel()
392 
393    print("-- Loading Level Data.")
394    print("-- Available Keys:")
395 
396    currentState = stateFileManager
397 
398    globalDataKeyTable = listGlobalData()
399 
400    for i, v in ipairs(globalDataKeyTable) do
401        if string.starts(v, "dGEN") then
402            print(i..": "..v)
403        elseif i == #globalDataKeyTable and DEBUG then
404            print("-- End of Global Data File.")
405        end
406    end
407 
408 end
409 
410 -- Handle iPad Orientation Changes
411 
412 function updateGridLocation(newOrientation)
413 
414    -- This function is required to reposition the grid
415    -- if the iPad orientation changes.
416 
417    baseX = 30
418    local y = HEIGHT/2 - (mSpriteSize * gridHeight) / 2
419    local x = baseX
420 
421    for i = 1, gridWidth do
422        for j = 1, gridHeight do
423            grid[i][j].pos.x = x
424            grid[i][j].pos.y = y
425            x = x + mSpriteSize
426        end
427        x = baseX
428        y = y + mSpriteSize
429    end
430 
431    local bX = baseX + 33
432 
433    startButton.x = bX
434    endButton.x = bX + 74
435    clearButton.x = bX + 148
436    wallButton.x = bX + 222
437 
438    local rbX, rbY = baseX - mSpriteSize/2, HEIGHT - 128
439 
440    resetButton.x, resetButton.y = rbX, rbY
441    loadButton.x, loadButton.y = rbX + 178, rbY
442    saveButton.x, saveButton.y = rbX + 356, rbY
443 
444    local tbY = HEIGHT - 50
445 
446    gameNameTextBox.x, gameNameTextBox.y = rbX, tbY
447    levelTextBox.x, levelTextBox.y = rbX + 356, tbY
448 
449    if newOrientation == LANDSCAPE_LEFT or newOrientation == LANDSCAPE_RIGHT then
450        gridButton.x = bX + 326
451        checkButton.x = bX + 400
452        debugButton.x = bX + 474
453        debugButton.y = checkButton.y
454        exitButton.x = bX + 548
455        exitButton.y = gridButton.y
456    else
457        local rbY = rbY - 130
458        gridButton.x = bX + 320
459        exitButton.x = gridButton.x
460        exitButton.y = gridButton.y + 74
461        checkButton.x = bX + 394
462        debugButton.x = checkButton.x
463        debugButton.y = checkButton.y + 74
464        resetButton.y = rbY
465        loadButton.y = rbY
466        saveButton.y = rbY
467    end
468 
469 end
470 
471 function orientationChanged(newOrientation)
472 
473    if currentState == stateShowFileManager then
474 
475        -- Update ListScroll co-ordinates for new orientation
476 
477        local y = HEIGHT - 500
478 
479        if directoryList ~= nil then
480            directoryList.pos.y = y
481        end
482 
483        if dataKeyList ~= nil then
484            dataKeyList.pos.y = y
485        end
486 
487        -- Update Menu Bar co-ordinates for new orientation
488 
489        y = HEIGHT - 80
490 
491        if b1tab ~= nil then
492            for i = 1, #b1tab do
493                b1tab[i].y = y
494            end
495        end
496 
497        y = HEIGHT - 110
498 
499        if b2tab ~= nil then
500            for i = 1, #b2tab do
501                b2tab[i].y = y
502                y = y - 30
503            end
504        end
505 
506    else
507        updateGridLocation(newOrientation)
508    end
509 
510 end
511 
512 -- Grid creation and reset functions
513 
514 function createGrid(obstacleGrid)
515 
516    local y = HEIGHT/2 - (mSpriteSize * gridHeight) / 2
517    local x = baseX
518 
519    -- Create the grid using nested tables.
520    -- It operates as a two dimensional array (or matrix)
521 
522    if DEBUG then
523        if obstacleGrid == nil then
524            print("-- dGen: Creating Empty Grid.")
525        else
526            print("-- dGen: Creating Grid from File.")
527        end
528    end
529 
530    for i = 1, gridWidth do
531        grid[i] = {}     -- create a new row
532        for j = 1, gridHeight do
533            if obstacleGrid == nil then
534                grid[i][j] = Cell(i, j, stateObstacle, x, y)
535            else
536                grid[i][j] = Cell(i, j, obstacleGrid[i][j], x, y)
537            end
538            grid[i][j].action = function() handleCellTouch(grid[i][j].index) end
539            x = x + mSpriteSize
540        end
541        x = baseX
542        y = y + mSpriteSize
543    end
544 
545 end
546 
547 function resetGrid()
548 
549    -- When the reset button is tapped this function will
550    -- reset the table
551 
552    if DEBUG then
553        print("-- dGen: Resetting Grid.")
554    end
555 
556    for i = 1, gridWidth do
557        for j = 1, gridHeight do
558            grid[i][j] = nil
559        end
560    end
561 
562    grid = {}
563    createGrid()
564 
565    if gridButton.pressed then
566        toggleGridState()
567    end
568 
569    startCellSelected = false
570    endCellSelected = false
571    startPos = nil
572    endPos = nil
573 
574 end
575 
576 -- Cell Related Functions
577 --
578 -- We discussed closure functions in a separate tutorial, but
579 -- for now to understand what is going on in the count neighbouring cell
580 -- functions you need to know that when a function is enclosed in
581 -- another function, it has full access to local variables from the
582 -- enclosing function. In this example, inNeighbourCells() increments the local
583 -- variable obstacleNum in countObstacles().
584 
585 function inNeighbourCells(startX, endX, startY, endY, closure)
586    for i = math.max(startX, 1), math.min(endX, gridWidth) do
587        for j = math.max(startY, 1), math.min(endY, gridHeight) do
588            closure(i, j)
589        end
590    end
591 end
592 
593 function countObstacles(index)
594 
595    local obstacleNum = 0
596 
597    inNeighbourCells(index.x - 1, index.x + 1, index.y - 1, index.y + 1,
598        function(x, y) if grid[x][y].state == stateObstacle then
599            obstacleNum = obstacleNum + 1 end
600        end)
601 
602    return obstacleNum
603 
604 end
605 
606 -- Button Action Methods
607 
608 function toggleGridState()
609 
610    -- This function will toggle whether the grid overlay is shown.
611 
612    for i = 1, gridWidth do
613        for j = 1, gridHeight do
614            grid[i][j].showGrid = not grid[i][j].showGrid
615        end
616    end
617 
618 end
619 
620 function toggleDebugState()
621 
622    -- This function will toggle whether the Debug window is shown.
623 
624    if debugButton.pressed then
625        displayMode(STANDARD)
626        DEBUG = true
627    else
628        displayMode(FULLSCREEN_NO_BUTTONS)
629        DEBUG = false
630    end
631 
632 end
633 
634 function clearPath()
635 
636    -- Clear the path from the Grid.
637 
638    if DEBUG then
639        print("-- dGEN: Clearing Path from Grid")
640    end
641 
642    for i = 1, gridWidth do
643        for j = 1, gridHeight do
644            if grid[i][j].state == statePath then
645                grid[i][j].state = stateClear
646            end
647        end
648    end
649 
650 end
651 
652 function findPath()
653 
654    -- If a start and end cell has been defined, use
655    -- the A* algorithm to find and display a path.
656 
657    if startCellSelected then
658        if endCellSelected then
659            print("-- dGEN: Calculating A* Path.")
660            path = CalcPath(CalcMoves(grid, startPos.x, startPos.y, endPos.x, endPos.y))
661            if path == nil then
662                print("-- dGEN No path found.")
663            else
664                print("-- dGEN: Path found:\n")
665 
666                if DEBUG then
667                    print(to_string(path, 2))
668                end
669 
670                -- start and end one cell early so we dont overwrite
671                -- the start and end cell status.
672 
673                for i = 2, table.getn(path) - 1 do
674                    grid[path[i].x][path[i].y].state = statePath
675                end
676            end
677        else
678            print("--dGEN ERROR: Path can't be found until end cell is selected.")
679        end
680    else
681        print("--dGEN  ERROR: Path can't be found until start cell is selected.")
682    end
683 
684 end
685 
686 -- Touch Handling
687 
688 function buttonPressed(index)
689 
690    if index < stateCheck then
691        tapState = index
692    end
693 
694    if index == stateStart then
695        endButton.pressed = false
696        clearButton.pressed = false
697        wallButton.pressed = false
698    elseif index == stateEnd then
699        startButton.pressed = false
700        clearButton.pressed = false
701        wallButton.pressed = false
702    elseif index == stateClear then
703        startButton.pressed = false
704        endButton.pressed = false
705        wallButton.pressed = false
706    elseif index == stateObstacle then
707        startButton.pressed = false
708        endButton.pressed = false
709        clearButton.pressed = false
710    elseif index == stateToggleGrid then
711        toggleGridState()
712    elseif index == stateCheck then
713        findPath()
714    elseif index == stateToggleDebug then
715        toggleDebugState()
716    else
717        print("-- dGEN WARNING: Unknown button index pressed.")
718    end
719 
720 end
721 
722 function touched(touch)
723 
724    if currentState == stateFileManager then
725 
726        FileManager:touched(touch)
727 
728    else
729 
730        -- Pass through touch handling to buttons, textbox and the grid cells
731 
732        startButton:touched(touch)
733        endButton:touched(touch)
734        clearButton:touched(touch)
735        wallButton:touched(touch)
736        gridButton:touched(touch)
737        checkButton:touched(touch)
738        debugButton:touched(touch)
739        exitButton:touched(touch)
740        resetButton:touched(touch)
741        saveButton:touched(touch)
742        loadButton:touched(touch)
743 
744        gameNameTextBox:touched(touch)
745        levelTextBox:touched(touch)
746 
747        for i = 1, gridWidth do
748            for j = 1, gridHeight do
749                grid[i][j]:touched(touch)
750            end
751        end
752 
753    end
754 
755 end
756 
757 function handleCellTouch(index)
758 
759    if tapState == stateStart and startCellSelected then
760        print("-- dGEN Warning: Only one cell may be assigned as the start cell.")
761    elseif tapState == stateEnd and endCellSelected then
762        print("-- dGEN Warning: Only one cell may be assigned as the end cell.")
763    else
764        if tapState == stateStart then
765            startCellSelected = true
766            startPos = vec2(index.x, index.y)
767        elseif tapState == stateEnd then
768            endCellSelected = true
769            endPos = vec2(index.x, index.y)
770        end
771 
772        if grid[index.x][index.y].state == stateStart then
773            startCellSelected = false
774            startPos = nil
775            clearPath()
776        elseif grid[index.x][index.y].state == stateEnd then
777            endCellSelected = false
778            endPos = nil
779            clearPath()
780        end
781 
782        grid[index.x][index.y].state = tapState
783    end
784 
785 end
786 
787 -- KeyBoard handling function
788 -- Used to enter game name and level
789 
790 function keyboard(key)
791 
792    -- Add text to the textbox which has focus
793 
794    local mTextBox = gameNameTextBox
795 
796    if levelTextBox.hasFocus then
797        mTextBox = levelTextBox
798    end
799 
800    if key ~= nil then
801        if string.byte(key) == 10 then     -- <RETURN> Key pressed
802            hideKeyboard()
803            mTextBox.hasFocus = false
804        elseif string.byte(key) ~= 44 then -- filter out commas
805            mTextBox:acceptKey(key)
806        end
807    end
808 
809 end
810 
811 -- Some String Helper Functions:
812 
813 function string.starts(String, Start)
814    return string.sub(String, 1, string.len(Start)) == Start
815 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.

jklp.PNG

18.4 Loading & Saving Data PAGE TOP

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.

Reference PAGE TOP

http://codeatuts.blogspot.in/2012/09/tutorial-18-saving-and-loading.html

0 Comments Leave a comment

Please login in order to leave a comment.

Newest first
!

Sign-in to your Chupamobile Account.

The Easiest way to Launch your next App or Game.

Join Chupamobile and get instant access to thousands of ready made App and Game Templates.

Creating an account means you’re okay with Chupamobile’s Terms of Service and Privacy Policy.