Trying to plug a memory leak

Hi 

 I seem to have a memory leak. I’m using the following code to monitor memory and calling it at the end of each function:

 printMemUsage = function()                   local memUsed = (collectgarbage("count")) / 1000         local texUsed = system.getInfo( "textureMemoryUsed" ) / 1000000              print("System Memory Used:", string.format("%.03f", memUsed), "Mb")         print("Texture Memory Used:", string.format("%.03f", texUsed), "Mb")     print("------------------------------------------\n")           return true end

 my usage does fluctuate as expected but consistently and gradually goes up over time. There doesn’t seem to be a clear pattern as to which functions are causing the increment. Most of my functions cause gains, but then when the memory usage fluctuates down every so often, it doesn’t go down as far as it did the time before, and consequently each new peak is higher than the previous.

All my display objects are included in one display group or another. Some objects have event listeners and transitions attached. Transitions are handled thus:

myObj.myTransition = transition.from ...

Here’s my code for getting rid of a typical group:

display.getCurrentStage():setFocus(nil) displaySplashGroup:removeSelf() displaySplashGroup = nil

There are some global entities: a few variables holding small (<500) integer values like scores and counters and some booleans, plus an array of integers holding consecutive whole number values from 1 to around 500). I don’t declare any of these before using them. They just get reassigned their starting values each time round.

There’s a table holding a few hundred text questions that is around 150KB which exists throughout the life of the game. There is also a text file that holds the scores and is read from and written to. 

thanks in advance,

David

It’s hard to use the current memory foot print as a determination on what’s causing memory to go up or down because when you delete something, it doesn’t get collected right away.  Garbage collection runs periodically behind the scenes and you might see your numbers drop at times that doesn’t seem like you’re deleting anything.  You can always force collection when you want it, but it’s a resource intensive process and you probably shouldn’t make it a habit of forcing collection.

Without seeing the code where you’re creating things, it’s going to be hard to advise much more.

HI

Here is a truncated version (basically 1/3) of my program. Sorry it’s so long. It you care to take a look that would be awesome.

local json = require("json") function main() --------------------------------------------------------- --define FUNCTIONS --------------------------------------------------------- local loadQuestions\_and\_createGameDataFile local decodeJsonFileIntoContents local displaySplash ... [and a bunch more] ------------------------------------------------------ -- Define font name and sizes --------------------------------------------------------- MULTIPLE\_CHOICE\_FONT\_SIZE = 60 -- the text of the 3 answer choices LINK\_FONT\_SIZE = 60 -- for text link "next" and "intro" SCORES\_FONT\_SIZE = 55 -- for the scores on the score page ... [and more] ---------------------------- -- 1. FUNCTION loadQuestions\_and\_createGameDataFile -- load the database of questions from the json file in the resources directory into a table called "questions" -- create the file that will hold the game scores, if it's not already there ------------------------------------ loadQuestions\_and\_createGameDataFile = function () questions = json.decode( decodeJsonFileIntoContents( "table\_of\_questions.json" )) --\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* create the game\_data file in caches if it's not already there local path = system.pathForFile("game\_data.json",system.CachesDirectory) local file = io.open(path,"r") if file then --print("game\_data file exists") else -- it's not there so we must be playing for the first time local savePath = system.pathForFile("game\_data.json",system.CachesDirectory) local saveFile = io.open(savePath,"w") local data = { { question\_type="Who wrote what", high\_score=0, [... and more fields] }, { question\_type="Musical terms", high\_score=0, [... and more fields]}, { question\_type="Score excerpts", high\_score=0, [... and more fields] }, [...more] } local contents = json.encode(data) saveFile:write(contents) io.close(saveFile) end end --------------------------------------------------------- -- 2. FUNCTION decodeJsonFileIntoContents --------------------------------------------------------- decodeJsonFileIntoContents = function( filename) local path = system.pathForFile( filename, system.ResourcesDirectory ) local contents local file = io.open( path, "r" ) if file then contents = file:read( "\*a" ) io.close( file ) -- close the file after using it end return contents end -- end of function jasonFile --------------------------------------------------------- -- 3. FUNCTION displaySplash -- this is the intro splash to the app - only plays once. --------------------------------------------------------- displaySplash = function( ) displaySplashGroup=display.newGroup() local welcomeDisplay1 = display.newText(displaySplashGroup,"Music History Flashcards" , display.contentWidth \* 0.17,display.contentHeight \* 0.3, FONT\_NAME, 80 ) welcomeDisplay1.anchorX=0 welcomeDisplay1:setFillColor(0,0,0) displaySplashGroup.transitionfrom = transition.from( welcomeDisplay1, { time=1000, delay=0, alpha=0 } ) displaySplashGroup.transitionto = transition.to( welcomeDisplay1, { time=1000, delay=4000,alpha=0 } ) local welcomeDisplay2 = display.newText(displaySplashGroup,"Flashcards for the Baroque, Classical, Romantic and Modern Eras" , display.contentWidth \* 0.17, display.contentHeight \* 0.6, display.contentWidth \* .75,0, FONT\_NAME, 70 ) welcomeDisplay2.anchorX=0 welcomeDisplay2:setFillColor(.87,0,0) welcomeDisplay2.transitionfrom = transition.from( welcomeDisplay2, { time=1000, delay=0, alpha=0 } ) welcomeDisplay2.transitionto = transition.to( welcomeDisplay2, { time=1000, delay=4000, alpha=0 } ) ... [lots more text and image objects here] local startPlay = display.newText(displaySplashGroup,"Touch here to start" , display.contentWidth \* .5, display.contentHeight \* .5, FONT\_NAME,100 ) startPlay:setFillColor(0.8,0,0) startPlay.tranfrom = transition.from( startPlay, { time=1000, delay=27000, alpha=0 } ) startPlay:addEventListener("touch", skipIntroOrTouchHereToStartListener) end -- of displaySplash ----------------------------------- -- 12 FUNCTION: chooseQuestionType --this is the 'main menu' of the app where users can choose what type of question to answer ----------------------------------- chooseQuestionType = function() questionChoicesGroup = display.newGroup() local questionType\_repertoire = display.newText(questionChoicesGroup, "Representative works" , display.contentWidth \* .15,display.contentHeight \* .07, FONT\_NAME, MENU\_FONT\_SIZE) questionType\_repertoire.anchorX=0 questionType\_repertoire.anchorY=0 questionType\_repertoire:setFillColor(0,0,0) questionType\_repertoire.type = "repertoire" questionType\_repertoire:addEventListener("touch", chooseQuestionTypeListener) local questionType\_composer\_rep = display.newText(questionChoicesGroup, "Who wrote what" , display.contentWidth \* .15,display.contentHeight \* .19, FONT\_NAME, MENU\_FONT\_SIZE) questionType\_composer\_rep.anchorX=0 questionType\_composer\_rep.anchorY=0 questionType\_composer\_rep:setFillColor(0,0,0) questionType\_composer\_rep.type = "composer\_rep" questionType\_composer\_rep:addEventListener("touch", chooseQuestionTypeListener) ... [and a few more menu choices presented with text objects and listeners] local chooseType = display.newText(questionChoicesGroup, "Pick a topic" , display.contentWidth \* .63, display.contentHeight \* .7 , FONT\_NAME, MENU\_FONT\_SIZE ) chooseType:setFillColor(.87,0,0) end -- of chooseQuestionType ----------------------------------- -- 13. FUNCTION: chooseQuestionTypeListener -- uses global boolean variables to hold the user's choice of question type ----------------------------------- chooseQuestionTypeListener = function ( event ) if ( event.phase == "began" ) then display.getCurrentStage():setFocus(event.target) event.target:setFillColor(.87,0,0) elseif ( event.phase == "ended" ) then display.getCurrentStage():setFocus(nil) questionChoicesGroup:removeSelf() questionChoicesGroup = nil choice\_composer\_rep = false choice\_definitions = false choice\_excerpts = false choice\_bio = false choice\_composer\_style = false choice\_repertoire = false choice\_everything = false if (event.target.type == "composer\_rep") then choice\_composer\_rep = true elseif (event.target.type == "definitions") then choice\_definitions = true elseif (event.target.type == "excerpts") then choice\_excerpts = true ... [etc.] end initializeNewGame() displayCard() end end -- chooseQuestionTypeListener() ----------------------------------- -- 14. FUNCTION: initializeNewGame ----------------------------------- initializeNewGame = function() math.randomseed( os.time() ) questionsAnswered = 0 -- the number of questions answered questionsIgnored = 0 -- the number of question ignored (these are all the questions that aren't the type that the user chose to answer and need to be passed over when we iterate through the table of questions) score = 0 -- the total of correct answers inRegularCard = false -- are we answering a regular type question ... inExcerptCard = false -- ... or an excerpt card (which has a different layout)? -- new array which will start with as many elements as there are questions and hold values thus: array[1] = 1, array[2] = 2, etc. arrayToRandomizeQuestions = {} local rq for rq=1, #questions do arrayToRandomizeQuestions[rq] = rq end end -- end of function initializeNewGame() ----------------------------------- -- 12. FUNCTION: getRandomIndexForQuestions -- this generates a number at random from 1 to the number of questions remaining -- it then assigns this value to questionIndex and takes that number out of the array -- questionIndex is then used to pull the card at that position from the question table ----------------------------------- getRandomIndexForQuestions = function () local randomNumber = math.random(1, #questions - (questionsAnswered + questionsIgnored)) questionIndex = table.remove(arrayToRandomizeQuestions, randomNumber) end -- end of function getRandomIndexForQuestions() ------------------------------------------------ -- 13. FUNCTION displayCard -- this looks at what type of question the user wants to answer -- and calls the appropriate function (display regular card or display excerpt card) ------------------------------------------------ displayCard = function() getRandomIndexForQuestions() displayQuestionCardGroup = display.newGroup() if (choice\_composer\_rep == true) then if (questions[questionIndex].type == "composer\_rep" ) then displayRegularCard() else ignoreCard() end elseif (choice\_definitions == true) then if (questions[questionIndex].type == "definitions" ) then displayRegularCard() else ignoreCard() end elseif (choice\_excerpts == true) then if (questions[questionIndex].type == "excerpts") then displayExcerptCard() else ignoreCard() end ... [and a few more] end end -- end of function displayCard() ----------------------------------- -- 14. FUNCTION: displayRegularCard -- this displays the default type of flashcard with a text question and 3 answer choices ----------------------------------- displayRegularCard = function () number\_of\_guesses = 0 -- you get two guesses so we have to count this inRegularCard = true -- we are in the regular type card inExcerptCard = false -- display the question: local regularCardQuestion = display.newText(displayQuestionCardGroup, questions[questionIndex].question, display.contentWidth \* .1, display.contentHeight \* .33,display.contentWidth \* .88,0, FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardQuestion.anchorX = 0 regularCardQuestion.anchorY = 0.5 regularCardQuestion:setFillColor(.87,0,0) local regularCardQuestionbackground = display.newRect( displayQuestionCardGroup, display.contentWidth \* 0.08,display.contentHeight \* .33,regularCardQuestion.width \* 1.05, regularCardQuestion.height \* 1.08 ) regularCardQuestionbackground.anchorX = 0 regularCardQuestionbackground.anchorY = 0.5 regularCardQuestionbackground.strokeWidth = 0 regularCardQuestionbackground:setFillColor( 1,1,1,.5) regularCardQuestionbackground:toBack() local x,y,z = get3Random() -- these 3 values are used to randomize the placement of the answer choices, so the right answer is not always in the same position local spacing = display.contentHeight \* .15 --display the 3 answer choices: local regularCardChoice1 = display.newText(displayQuestionCardGroup, questions[questionIndex].correct\_answer, display.contentWidth \* .1, display.contentHeight \* .35 + (x \* spacing), FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardChoice1.anchorX = 0 regularCardChoice1.anchorY = 0 regularCardChoice1:setFillColor(0,0,0) regularCardChoice1.isCorrect = true regularCardChoice1:addEventListener( "touch", listenForResponseAndDisplayAnswerListener ) local regularCardChoice2 = display.newText(displayQuestionCardGroup,questions[questionIndex].wrong\_answer\_1, display.contentWidth \* .1, display.contentHeight \* .35 + (y \* spacing), FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardChoice2.anchorX = 0 regularCardChoice2.anchorY = 0 regularCardChoice2:setFillColor(0,0,0) regularCardChoice2.isCorrect = false regularCardChoice2:addEventListener( "touch", listenForResponseAndDisplayAnswerListener ) local regularCardChoice3 = display.newText(displayQuestionCardGroup, questions[questionIndex].wrong\_answer\_2, display.contentWidth \* .1, display.contentHeight \* .35 + (z \* spacing), FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardChoice3.anchorX = 0 regularCardChoice3.anchorY = 0 regularCardChoice3:setFillColor(0,0,0) regularCardChoice3.isCorrect = false regularCardChoice3:addEventListener( "touch", listenForResponseAndDisplayAnswerListener ) local which\_inquisitor = math.random(1,2) --RANDOMLY Pick one of two images if (which\_inquisitor == 1) then if (regularCardQuestionbackground.height \< display.contentHeight \* .25 ) then regularCardInquisitor = "pics/portraits/larger\_question\_card\_haydn.png" imageWidth = 189 --GLOBAL imageHeight = 180 --GLOBAL ... end -- display the randomly chosen image local inquisitor = display.newImageRect(displayQuestionCardGroup,regularCardInquisitor, imageWidth,imageHeight) inquisitor.anchorY = 1 inquisitor.x = display.contentWidth \* .16 inquisitor.y = (regularCardQuestionbackground.y ) - (regularCardQuestionbackground.height \* .5) inquisitor:toBack() ... end -- end of displayRegularCard() ----------------------------------- -- 15. FUNCTION: displayExcerptCard ----------------------------------- ... similar to the previous function but with different layout ----------------------------------- -- 16. FUNCTION: ignoreCard -- this function is necessary because we need to keep a count of -- how many cards are ignored ----------------------------------- ignoreCard = function () questionsIgnored = questionsIgnored + 1 if (questionsIgnored + questionsAnswered == #questions) then displayFinalScreen() else displayCard() end end -- ignoreCard() ----------------------------------- -- 17. FUNCTION: get3random ----------------------------------- get3Random = function () local a = math.random(1,3) local b = math.random(1,3) local c = math.random(1,3) while (a == b) do b = math.random(1,3) end while (c == b or c == a) do c = math.random(1,3) end return a,b,c end -- end of get3Random() ----------------------------------- -- 20. FUNCTION: listenForResponseAndDisplayAnswerListener ----------------------------------- listenForResponseAndDisplayAnswerListener = function ( event ) if ( event.phase == "began" ) then -- if 1 display.getCurrentStage():setFocus(event.target) event.target:setFillColor(.87, 0, 0) elseif ( event.phase == "ended" ) then display.getCurrentStage():setFocus(nil) if (event.target.isCorrect == false and number\_of\_guesses == 0) then -- the the user got it wrong the first time: give second chance: ... [prompt user to try again - uses text objects with transitions, all added to displayQuestionCardGroup] end else -- they either got it right first (or 2nd) time, or they've used up their 2nd chance displayQuestionCardGroup:removeSelf() displayQuestionCardGroup = nil displayAnswerCardGroup = display.newGroup() ... [increment the score if appropriate, increment the var holding the number of questions answered, and display the correct answer using various text/image objects] end -- end of if 2 end -- of if 1 end -- of function listenForResponseAndDisplayAnswerListener() ----------------------------------- -- 21. FUNCTION displayScoreInCorner ----------------------------------- ... ----------------------------------- -- 22. FUNCTION continueToNextCardOrQuit -- decide whether there are any cards left to display ----------------------------------- continueToNextCardOrQuit = function(event) if ( event.phase == "began" ) then display.getCurrentStage():setFocus(event.target) event.target:setFillColor(.87,0, 0,.3) elseif ( event.phase == "ended" ) then display.getCurrentStage():setFocus(nil) displayAnswerCardGroup:removeSelf() displayAnswerCardGroup = nil if ((questionsAnswered + questionsIgnored) \< #questions) then -- there are question left displayCard() else -- game is over - provide final score and feedback displayFinalScreen() end -- end of 2nd inner if end end -- end of continueToNextCardOrQuit() ----------------------------------- -- 23. FUNCTION: displayFinalScreen -- show the user their final score and provide links to see scores and return to menu -- also updates the score text file in the caches directory ----------------------------------- displayFinalScreen = function () local scoreAsPercentage = math.round(( score / questionsAnswered ) \* 1000) / 10 local gameDataTable = {} -- create the array/table that will hold game data finalScreenGroup = display.newGroup() ... [more text and image objects with listeners] local array\_index = 0 if choice\_composer\_rep == true then array\_index = 1 elseif choice\_definitions == true then array\_index = 2 elseif choice\_excerpts == true then array\_index = 3 elseif choice\_bio == true then array\_index = 4 elseif choice\_composer\_style == true then array\_index = 5 elseif choice\_repertoire == true then array\_index = 6 elseif choice\_everything == true then array\_index = 7 end local comment ... [assign value to 'comment' based on the score] gameDataTable = loadTable("game\_data.json") -- update the game data file: if (scoreAsPercentage \> gameDataTable[array\_index].high\_score) then gameDataTable[array\_index].high\_score = scoreAsPercentage end -- store the new high score if it is a new high gameDataTable[array\_index].cumulative\_score = gameDataTable[array\_index].cumulative\_score + scoreAsPercentage gameDataTable[array\_index].games\_played = gameDataTable[array\_index].games\_played + 1 gameDataTable[array\_index].average = math.round((gameDataTable[array\_index].cumulative\_score / gameDataTable[array\_index].games\_played)\*10)/10 saveTable(gameDataTable,"game\_data.json") -- write the array to the json file ... [display the results and the comment and links to see all scores and return to menu] end -- end of displayFinalScreen() ----------------------------------- -- 26. FUNCTION: playAgainListener ----------------------------------- playAgainListener = function ( event ) ... [called from the previous function - takes you back to the menu] end -- end of playAgainListener() ----------------------------------- -- 26.5 FUNCTION: seeAllScoresListener ----------------------------------- seeAllScoresListener = function ( event ) ... [deletes final screen group and calls the function that shows all the scores] end -- seeAllScoresListener() ----------------------------------- -- 26.6 FUNCTION: showAllScores -- presents a screen in tabular format displaying all the scores in all the question type categories ----------------------------------- showAllScores = function() showAllScoresGroup = display.newGroup() local gameDataTable = {} gameDataTable = loadTable("game\_data.json") localscoreHeading2 = display.newText(showAllScoresGroup, "high", display.contentWidth \* .6, 50, FONT\_NAME, MENU\_FONT\_SIZE ) scoreHeading2:setFillColor(0,0,0) local scoreHeading3 = display.newText(showAllScoresGroup, "#games", display.contentWidth \* .75, 50, FONT\_NAME, MENU\_FONT\_SIZE ) scoreHeading3:setFillColor(0,0,0) local scoreHeading4 = display.newText(showAllScoresGroup, "avg.", display.contentWidth \* .9, 50, FONT\_NAME, MENU\_FONT\_SIZE ) scoreHeading4:setFillColor(0,0,0) local line\_spacing = 0 local first\_line\_y = display.contentHeight \* .2 local arrayToHoldScores = {{},{},{},{},{},{},{}} for i=1, #gameDataTable - 1 do -- this has to be "#gameDataTable - 1" because the last array element doesn't have these fields arrayToHoldScores[i].question\_type = display.newText(gameDataTable[i].question\_type,display.contentWidth \* .1,first\_line\_y + line\_spacing,FONT\_NAME, SCORES\_FONT\_SIZE) arrayToHoldScores[i].question\_type.anchorX = 0 showAllScoresGroup:insert(arrayToHoldScores[i].question\_type) arrayToHoldScores[i].question\_type:setFillColor(.87,0,0) arrayToHoldScores[i].high\_score = display.newText( gameDataTable[i].high\_score ,display.contentWidth \* .6,first\_line\_y + line\_spacing,FONT\_NAME, SCORES\_FONT\_SIZE) showAllScoresGroup:insert(arrayToHoldScores[i].high\_score) arrayToHoldScores[i].high\_score:setFillColor(.87,0,0) [... etc.] line\_spacing = line\_spacing + (display.contentHeight \* .1) end local play\_again\_prompt\_background = display.newRect( showAllScoresGroup, display.contentWidth,display.contentHeight,display.contentWidth \* .37, display.contentHeight \* .27 ) play\_again\_prompt\_background.strokeWidth = 0 play\_again\_prompt\_background:setFillColor( 1,1,1,.5 ) play\_again\_prompt\_background:addEventListener("touch", playAgainAfterSeeingAllScoresListener) local play\_again\_prompt = display.newText(showAllScoresGroup, "menu", display.contentWidth \* .9, display.contentHeight \* .9, FONT\_NAME, MENU\_FONT\_SIZE) play\_again\_prompt:setFillColor(0,0,0) end -- end of showAllScores() ----------------------------------- -- 26.6 FUNCTION: playAgainAfterSeeingAllScoresListener ----------------------------------- playAgainAfterSeeingAllScoresListener = function ( event ) ... [go back to menu] end -- end of playAgainAfterSeeingAllScoresListener() ----------------------------------- -- 27. FUNCTION saveTable ----------------------------------- saveTable = function(t, filename) local path = system.pathForFile(filename,system.CachesDirectory) local file = io.open(path,"w") if file then local contents = json.encode(t) file:write(contents) io.close(file) return true else return false end end -- saveTable() ----------------------------------- -- 28. FUNCTION loadTable ----------------------------------- loadTable = function(filename) local path = system.pathForFile(filename, system.CachesDirectory) local contents = "" local gameDataTable = {} local file = io.open (path, "r") if file then local contents = file:read("\*a") gameDataTable = json.decode(contents) io.close(file) return gameDataTable end return nil end -- loadTable() ----------------------------------- -- END OF FUNCTION LISTING ----------------------------------- loadQuestions\_and\_createGameDataFile() -- this creates the timestamp file display.setStatusBar( display.HiddenStatusBar ) local splashBackground = display.newImageRect( "pics/score\_background.png",1425,900) splashBackground.anchorX=0 splashBackground.anchorY=0 displaySplash() end -- end of main function main()

Well. Common causes of memory leaks are closures, objects that aren’t deallocated, references that aren’t nilled and listeners that aren’t removed.

Don’t take this the wrong way, but this file is far too long. Could I recommend you break it up into bits - for example your json stuff and put that in a separate file, or class, and keep the bits that belong to those bits (e.g. construct, destroy) with those bits.

Hi

Do you mean too long from the perspective of good programming practices?

In terms of classes, should I create classes for each of my display screens, then create and destroy instance of them? How would that be better than putting each screens objects in a display group and then nilling that out?

thanks,

David

It’s not the length so much - it’s keeping all the bits that function together together as much as possible, and minimising the connections between the two. Ideally so they can be tested separately.

Again with your scenes, if you look at the model used by Composer et al, all the creation and destruction of the scene is in a separate unit and you communicate between the scenes.

I tried to keep the functions  fairly modular but at the same time I realize that a lot of them do do several things at once. There is also a sort of daisy chain quality to the whole thing, so that  function a calls b, b calls c, c calls d, and so on.

I’m imagining that a better way would be to have a central function that controls the game and to which control returns after each ‘scene’. Is this the kind of thing composer does?

Hi

It says here: http://developer.coronalabs.com/node/26628

that objects should be returned like this:

local function createBox() &nbsp; &nbsp; local box = display.newImage("box.png") &nbsp; &nbsp; &nbsp; &nbsp; return box end

otherwise “you have no way of clearing its memory”.

Does this apply to display groups? Should I return these at the end of the functions that create them even if I’m already removing them and nilling them out. My groups are globals, as I need to access them in other functions (i.e. the even listeners that that they call and which in turn remove and nil them out) 

It’s hard to use the current memory foot print as a determination on what’s causing memory to go up or down because when you delete something, it doesn’t get collected right away.  Garbage collection runs periodically behind the scenes and you might see your numbers drop at times that doesn’t seem like you’re deleting anything.  You can always force collection when you want it, but it’s a resource intensive process and you probably shouldn’t make it a habit of forcing collection.

Without seeing the code where you’re creating things, it’s going to be hard to advise much more.

HI

Here is a truncated version (basically 1/3) of my program. Sorry it’s so long. It you care to take a look that would be awesome.

local json = require("json") function main() --------------------------------------------------------- --define FUNCTIONS --------------------------------------------------------- local loadQuestions\_and\_createGameDataFile local decodeJsonFileIntoContents local displaySplash ... [and a bunch more] ------------------------------------------------------ -- Define font name and sizes --------------------------------------------------------- MULTIPLE\_CHOICE\_FONT\_SIZE = 60 -- the text of the 3 answer choices LINK\_FONT\_SIZE = 60 -- for text link "next" and "intro" SCORES\_FONT\_SIZE = 55 -- for the scores on the score page ... [and more] ---------------------------- -- 1. FUNCTION loadQuestions\_and\_createGameDataFile -- load the database of questions from the json file in the resources directory into a table called "questions" -- create the file that will hold the game scores, if it's not already there ------------------------------------ loadQuestions\_and\_createGameDataFile = function () questions = json.decode( decodeJsonFileIntoContents( "table\_of\_questions.json" )) --\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* create the game\_data file in caches if it's not already there local path = system.pathForFile("game\_data.json",system.CachesDirectory) local file = io.open(path,"r") if file then --print("game\_data file exists") else -- it's not there so we must be playing for the first time local savePath = system.pathForFile("game\_data.json",system.CachesDirectory) local saveFile = io.open(savePath,"w") local data = { { question\_type="Who wrote what", high\_score=0, [... and more fields] }, { question\_type="Musical terms", high\_score=0, [... and more fields]}, { question\_type="Score excerpts", high\_score=0, [... and more fields] }, [...more] } local contents = json.encode(data) saveFile:write(contents) io.close(saveFile) end end --------------------------------------------------------- -- 2. FUNCTION decodeJsonFileIntoContents --------------------------------------------------------- decodeJsonFileIntoContents = function( filename) local path = system.pathForFile( filename, system.ResourcesDirectory ) local contents local file = io.open( path, "r" ) if file then contents = file:read( "\*a" ) io.close( file ) -- close the file after using it end return contents end -- end of function jasonFile --------------------------------------------------------- -- 3. FUNCTION displaySplash -- this is the intro splash to the app - only plays once. --------------------------------------------------------- displaySplash = function( ) displaySplashGroup=display.newGroup() local welcomeDisplay1 = display.newText(displaySplashGroup,"Music History Flashcards" , display.contentWidth \* 0.17,display.contentHeight \* 0.3, FONT\_NAME, 80 ) welcomeDisplay1.anchorX=0 welcomeDisplay1:setFillColor(0,0,0) displaySplashGroup.transitionfrom = transition.from( welcomeDisplay1, { time=1000, delay=0, alpha=0 } ) displaySplashGroup.transitionto = transition.to( welcomeDisplay1, { time=1000, delay=4000,alpha=0 } ) local welcomeDisplay2 = display.newText(displaySplashGroup,"Flashcards for the Baroque, Classical, Romantic and Modern Eras" , display.contentWidth \* 0.17, display.contentHeight \* 0.6, display.contentWidth \* .75,0, FONT\_NAME, 70 ) welcomeDisplay2.anchorX=0 welcomeDisplay2:setFillColor(.87,0,0) welcomeDisplay2.transitionfrom = transition.from( welcomeDisplay2, { time=1000, delay=0, alpha=0 } ) welcomeDisplay2.transitionto = transition.to( welcomeDisplay2, { time=1000, delay=4000, alpha=0 } ) ... [lots more text and image objects here] local startPlay = display.newText(displaySplashGroup,"Touch here to start" , display.contentWidth \* .5, display.contentHeight \* .5, FONT\_NAME,100 ) startPlay:setFillColor(0.8,0,0) startPlay.tranfrom = transition.from( startPlay, { time=1000, delay=27000, alpha=0 } ) startPlay:addEventListener("touch", skipIntroOrTouchHereToStartListener) end -- of displaySplash ----------------------------------- -- 12 FUNCTION: chooseQuestionType --this is the 'main menu' of the app where users can choose what type of question to answer ----------------------------------- chooseQuestionType = function() questionChoicesGroup = display.newGroup() local questionType\_repertoire = display.newText(questionChoicesGroup, "Representative works" , display.contentWidth \* .15,display.contentHeight \* .07, FONT\_NAME, MENU\_FONT\_SIZE) questionType\_repertoire.anchorX=0 questionType\_repertoire.anchorY=0 questionType\_repertoire:setFillColor(0,0,0) questionType\_repertoire.type = "repertoire" questionType\_repertoire:addEventListener("touch", chooseQuestionTypeListener) local questionType\_composer\_rep = display.newText(questionChoicesGroup, "Who wrote what" , display.contentWidth \* .15,display.contentHeight \* .19, FONT\_NAME, MENU\_FONT\_SIZE) questionType\_composer\_rep.anchorX=0 questionType\_composer\_rep.anchorY=0 questionType\_composer\_rep:setFillColor(0,0,0) questionType\_composer\_rep.type = "composer\_rep" questionType\_composer\_rep:addEventListener("touch", chooseQuestionTypeListener) ... [and a few more menu choices presented with text objects and listeners] local chooseType = display.newText(questionChoicesGroup, "Pick a topic" , display.contentWidth \* .63, display.contentHeight \* .7 , FONT\_NAME, MENU\_FONT\_SIZE ) chooseType:setFillColor(.87,0,0) end -- of chooseQuestionType ----------------------------------- -- 13. FUNCTION: chooseQuestionTypeListener -- uses global boolean variables to hold the user's choice of question type ----------------------------------- chooseQuestionTypeListener = function ( event ) if ( event.phase == "began" ) then display.getCurrentStage():setFocus(event.target) event.target:setFillColor(.87,0,0) elseif ( event.phase == "ended" ) then display.getCurrentStage():setFocus(nil) questionChoicesGroup:removeSelf() questionChoicesGroup = nil choice\_composer\_rep = false choice\_definitions = false choice\_excerpts = false choice\_bio = false choice\_composer\_style = false choice\_repertoire = false choice\_everything = false if (event.target.type == "composer\_rep") then choice\_composer\_rep = true elseif (event.target.type == "definitions") then choice\_definitions = true elseif (event.target.type == "excerpts") then choice\_excerpts = true ... [etc.] end initializeNewGame() displayCard() end end -- chooseQuestionTypeListener() ----------------------------------- -- 14. FUNCTION: initializeNewGame ----------------------------------- initializeNewGame = function() math.randomseed( os.time() ) questionsAnswered = 0 -- the number of questions answered questionsIgnored = 0 -- the number of question ignored (these are all the questions that aren't the type that the user chose to answer and need to be passed over when we iterate through the table of questions) score = 0 -- the total of correct answers inRegularCard = false -- are we answering a regular type question ... inExcerptCard = false -- ... or an excerpt card (which has a different layout)? -- new array which will start with as many elements as there are questions and hold values thus: array[1] = 1, array[2] = 2, etc. arrayToRandomizeQuestions = {} local rq for rq=1, #questions do arrayToRandomizeQuestions[rq] = rq end end -- end of function initializeNewGame() ----------------------------------- -- 12. FUNCTION: getRandomIndexForQuestions -- this generates a number at random from 1 to the number of questions remaining -- it then assigns this value to questionIndex and takes that number out of the array -- questionIndex is then used to pull the card at that position from the question table ----------------------------------- getRandomIndexForQuestions = function () local randomNumber = math.random(1, #questions - (questionsAnswered + questionsIgnored)) questionIndex = table.remove(arrayToRandomizeQuestions, randomNumber) end -- end of function getRandomIndexForQuestions() ------------------------------------------------ -- 13. FUNCTION displayCard -- this looks at what type of question the user wants to answer -- and calls the appropriate function (display regular card or display excerpt card) ------------------------------------------------ displayCard = function() getRandomIndexForQuestions() displayQuestionCardGroup = display.newGroup() if (choice\_composer\_rep == true) then if (questions[questionIndex].type == "composer\_rep" ) then displayRegularCard() else ignoreCard() end elseif (choice\_definitions == true) then if (questions[questionIndex].type == "definitions" ) then displayRegularCard() else ignoreCard() end elseif (choice\_excerpts == true) then if (questions[questionIndex].type == "excerpts") then displayExcerptCard() else ignoreCard() end ... [and a few more] end end -- end of function displayCard() ----------------------------------- -- 14. FUNCTION: displayRegularCard -- this displays the default type of flashcard with a text question and 3 answer choices ----------------------------------- displayRegularCard = function () number\_of\_guesses = 0 -- you get two guesses so we have to count this inRegularCard = true -- we are in the regular type card inExcerptCard = false -- display the question: local regularCardQuestion = display.newText(displayQuestionCardGroup, questions[questionIndex].question, display.contentWidth \* .1, display.contentHeight \* .33,display.contentWidth \* .88,0, FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardQuestion.anchorX = 0 regularCardQuestion.anchorY = 0.5 regularCardQuestion:setFillColor(.87,0,0) local regularCardQuestionbackground = display.newRect( displayQuestionCardGroup, display.contentWidth \* 0.08,display.contentHeight \* .33,regularCardQuestion.width \* 1.05, regularCardQuestion.height \* 1.08 ) regularCardQuestionbackground.anchorX = 0 regularCardQuestionbackground.anchorY = 0.5 regularCardQuestionbackground.strokeWidth = 0 regularCardQuestionbackground:setFillColor( 1,1,1,.5) regularCardQuestionbackground:toBack() local x,y,z = get3Random() -- these 3 values are used to randomize the placement of the answer choices, so the right answer is not always in the same position local spacing = display.contentHeight \* .15 --display the 3 answer choices: local regularCardChoice1 = display.newText(displayQuestionCardGroup, questions[questionIndex].correct\_answer, display.contentWidth \* .1, display.contentHeight \* .35 + (x \* spacing), FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardChoice1.anchorX = 0 regularCardChoice1.anchorY = 0 regularCardChoice1:setFillColor(0,0,0) regularCardChoice1.isCorrect = true regularCardChoice1:addEventListener( "touch", listenForResponseAndDisplayAnswerListener ) local regularCardChoice2 = display.newText(displayQuestionCardGroup,questions[questionIndex].wrong\_answer\_1, display.contentWidth \* .1, display.contentHeight \* .35 + (y \* spacing), FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardChoice2.anchorX = 0 regularCardChoice2.anchorY = 0 regularCardChoice2:setFillColor(0,0,0) regularCardChoice2.isCorrect = false regularCardChoice2:addEventListener( "touch", listenForResponseAndDisplayAnswerListener ) local regularCardChoice3 = display.newText(displayQuestionCardGroup, questions[questionIndex].wrong\_answer\_2, display.contentWidth \* .1, display.contentHeight \* .35 + (z \* spacing), FONT\_NAME, MULTIPLE\_CHOICE\_FONT\_SIZE ) regularCardChoice3.anchorX = 0 regularCardChoice3.anchorY = 0 regularCardChoice3:setFillColor(0,0,0) regularCardChoice3.isCorrect = false regularCardChoice3:addEventListener( "touch", listenForResponseAndDisplayAnswerListener ) local which\_inquisitor = math.random(1,2) --RANDOMLY Pick one of two images if (which\_inquisitor == 1) then if (regularCardQuestionbackground.height \< display.contentHeight \* .25 ) then regularCardInquisitor = "pics/portraits/larger\_question\_card\_haydn.png" imageWidth = 189 --GLOBAL imageHeight = 180 --GLOBAL ... end -- display the randomly chosen image local inquisitor = display.newImageRect(displayQuestionCardGroup,regularCardInquisitor, imageWidth,imageHeight) inquisitor.anchorY = 1 inquisitor.x = display.contentWidth \* .16 inquisitor.y = (regularCardQuestionbackground.y ) - (regularCardQuestionbackground.height \* .5) inquisitor:toBack() ... end -- end of displayRegularCard() ----------------------------------- -- 15. FUNCTION: displayExcerptCard ----------------------------------- ... similar to the previous function but with different layout ----------------------------------- -- 16. FUNCTION: ignoreCard -- this function is necessary because we need to keep a count of -- how many cards are ignored ----------------------------------- ignoreCard = function () questionsIgnored = questionsIgnored + 1 if (questionsIgnored + questionsAnswered == #questions) then displayFinalScreen() else displayCard() end end -- ignoreCard() ----------------------------------- -- 17. FUNCTION: get3random ----------------------------------- get3Random = function () local a = math.random(1,3) local b = math.random(1,3) local c = math.random(1,3) while (a == b) do b = math.random(1,3) end while (c == b or c == a) do c = math.random(1,3) end return a,b,c end -- end of get3Random() ----------------------------------- -- 20. FUNCTION: listenForResponseAndDisplayAnswerListener ----------------------------------- listenForResponseAndDisplayAnswerListener = function ( event ) if ( event.phase == "began" ) then -- if 1 display.getCurrentStage():setFocus(event.target) event.target:setFillColor(.87, 0, 0) elseif ( event.phase == "ended" ) then display.getCurrentStage():setFocus(nil) if (event.target.isCorrect == false and number\_of\_guesses == 0) then -- the the user got it wrong the first time: give second chance: ... [prompt user to try again - uses text objects with transitions, all added to displayQuestionCardGroup] end else -- they either got it right first (or 2nd) time, or they've used up their 2nd chance displayQuestionCardGroup:removeSelf() displayQuestionCardGroup = nil displayAnswerCardGroup = display.newGroup() ... [increment the score if appropriate, increment the var holding the number of questions answered, and display the correct answer using various text/image objects] end -- end of if 2 end -- of if 1 end -- of function listenForResponseAndDisplayAnswerListener() ----------------------------------- -- 21. FUNCTION displayScoreInCorner ----------------------------------- ... ----------------------------------- -- 22. FUNCTION continueToNextCardOrQuit -- decide whether there are any cards left to display ----------------------------------- continueToNextCardOrQuit = function(event) if ( event.phase == "began" ) then display.getCurrentStage():setFocus(event.target) event.target:setFillColor(.87,0, 0,.3) elseif ( event.phase == "ended" ) then display.getCurrentStage():setFocus(nil) displayAnswerCardGroup:removeSelf() displayAnswerCardGroup = nil if ((questionsAnswered + questionsIgnored) \< #questions) then -- there are question left displayCard() else -- game is over - provide final score and feedback displayFinalScreen() end -- end of 2nd inner if end end -- end of continueToNextCardOrQuit() ----------------------------------- -- 23. FUNCTION: displayFinalScreen -- show the user their final score and provide links to see scores and return to menu -- also updates the score text file in the caches directory ----------------------------------- displayFinalScreen = function () local scoreAsPercentage = math.round(( score / questionsAnswered ) \* 1000) / 10 local gameDataTable = {} -- create the array/table that will hold game data finalScreenGroup = display.newGroup() ... [more text and image objects with listeners] local array\_index = 0 if choice\_composer\_rep == true then array\_index = 1 elseif choice\_definitions == true then array\_index = 2 elseif choice\_excerpts == true then array\_index = 3 elseif choice\_bio == true then array\_index = 4 elseif choice\_composer\_style == true then array\_index = 5 elseif choice\_repertoire == true then array\_index = 6 elseif choice\_everything == true then array\_index = 7 end local comment ... [assign value to 'comment' based on the score] gameDataTable = loadTable("game\_data.json") -- update the game data file: if (scoreAsPercentage \> gameDataTable[array\_index].high\_score) then gameDataTable[array\_index].high\_score = scoreAsPercentage end -- store the new high score if it is a new high gameDataTable[array\_index].cumulative\_score = gameDataTable[array\_index].cumulative\_score + scoreAsPercentage gameDataTable[array\_index].games\_played = gameDataTable[array\_index].games\_played + 1 gameDataTable[array\_index].average = math.round((gameDataTable[array\_index].cumulative\_score / gameDataTable[array\_index].games\_played)\*10)/10 saveTable(gameDataTable,"game\_data.json") -- write the array to the json file ... [display the results and the comment and links to see all scores and return to menu] end -- end of displayFinalScreen() ----------------------------------- -- 26. FUNCTION: playAgainListener ----------------------------------- playAgainListener = function ( event ) ... [called from the previous function - takes you back to the menu] end -- end of playAgainListener() ----------------------------------- -- 26.5 FUNCTION: seeAllScoresListener ----------------------------------- seeAllScoresListener = function ( event ) ... [deletes final screen group and calls the function that shows all the scores] end -- seeAllScoresListener() ----------------------------------- -- 26.6 FUNCTION: showAllScores -- presents a screen in tabular format displaying all the scores in all the question type categories ----------------------------------- showAllScores = function() showAllScoresGroup = display.newGroup() local gameDataTable = {} gameDataTable = loadTable("game\_data.json") localscoreHeading2 = display.newText(showAllScoresGroup, "high", display.contentWidth \* .6, 50, FONT\_NAME, MENU\_FONT\_SIZE ) scoreHeading2:setFillColor(0,0,0) local scoreHeading3 = display.newText(showAllScoresGroup, "#games", display.contentWidth \* .75, 50, FONT\_NAME, MENU\_FONT\_SIZE ) scoreHeading3:setFillColor(0,0,0) local scoreHeading4 = display.newText(showAllScoresGroup, "avg.", display.contentWidth \* .9, 50, FONT\_NAME, MENU\_FONT\_SIZE ) scoreHeading4:setFillColor(0,0,0) local line\_spacing = 0 local first\_line\_y = display.contentHeight \* .2 local arrayToHoldScores = {{},{},{},{},{},{},{}} for i=1, #gameDataTable - 1 do -- this has to be "#gameDataTable - 1" because the last array element doesn't have these fields arrayToHoldScores[i].question\_type = display.newText(gameDataTable[i].question\_type,display.contentWidth \* .1,first\_line\_y + line\_spacing,FONT\_NAME, SCORES\_FONT\_SIZE) arrayToHoldScores[i].question\_type.anchorX = 0 showAllScoresGroup:insert(arrayToHoldScores[i].question\_type) arrayToHoldScores[i].question\_type:setFillColor(.87,0,0) arrayToHoldScores[i].high\_score = display.newText( gameDataTable[i].high\_score ,display.contentWidth \* .6,first\_line\_y + line\_spacing,FONT\_NAME, SCORES\_FONT\_SIZE) showAllScoresGroup:insert(arrayToHoldScores[i].high\_score) arrayToHoldScores[i].high\_score:setFillColor(.87,0,0) [... etc.] line\_spacing = line\_spacing + (display.contentHeight \* .1) end local play\_again\_prompt\_background = display.newRect( showAllScoresGroup, display.contentWidth,display.contentHeight,display.contentWidth \* .37, display.contentHeight \* .27 ) play\_again\_prompt\_background.strokeWidth = 0 play\_again\_prompt\_background:setFillColor( 1,1,1,.5 ) play\_again\_prompt\_background:addEventListener("touch", playAgainAfterSeeingAllScoresListener) local play\_again\_prompt = display.newText(showAllScoresGroup, "menu", display.contentWidth \* .9, display.contentHeight \* .9, FONT\_NAME, MENU\_FONT\_SIZE) play\_again\_prompt:setFillColor(0,0,0) end -- end of showAllScores() ----------------------------------- -- 26.6 FUNCTION: playAgainAfterSeeingAllScoresListener ----------------------------------- playAgainAfterSeeingAllScoresListener = function ( event ) ... [go back to menu] end -- end of playAgainAfterSeeingAllScoresListener() ----------------------------------- -- 27. FUNCTION saveTable ----------------------------------- saveTable = function(t, filename) local path = system.pathForFile(filename,system.CachesDirectory) local file = io.open(path,"w") if file then local contents = json.encode(t) file:write(contents) io.close(file) return true else return false end end -- saveTable() ----------------------------------- -- 28. FUNCTION loadTable ----------------------------------- loadTable = function(filename) local path = system.pathForFile(filename, system.CachesDirectory) local contents = "" local gameDataTable = {} local file = io.open (path, "r") if file then local contents = file:read("\*a") gameDataTable = json.decode(contents) io.close(file) return gameDataTable end return nil end -- loadTable() ----------------------------------- -- END OF FUNCTION LISTING ----------------------------------- loadQuestions\_and\_createGameDataFile() -- this creates the timestamp file display.setStatusBar( display.HiddenStatusBar ) local splashBackground = display.newImageRect( "pics/score\_background.png",1425,900) splashBackground.anchorX=0 splashBackground.anchorY=0 displaySplash() end -- end of main function main()

Well. Common causes of memory leaks are closures, objects that aren’t deallocated, references that aren’t nilled and listeners that aren’t removed.

Don’t take this the wrong way, but this file is far too long. Could I recommend you break it up into bits - for example your json stuff and put that in a separate file, or class, and keep the bits that belong to those bits (e.g. construct, destroy) with those bits.

Hi

Do you mean too long from the perspective of good programming practices?

In terms of classes, should I create classes for each of my display screens, then create and destroy instance of them? How would that be better than putting each screens objects in a display group and then nilling that out?

thanks,

David

It’s not the length so much - it’s keeping all the bits that function together together as much as possible, and minimising the connections between the two. Ideally so they can be tested separately.

Again with your scenes, if you look at the model used by Composer et al, all the creation and destruction of the scene is in a separate unit and you communicate between the scenes.

I tried to keep the functions  fairly modular but at the same time I realize that a lot of them do do several things at once. There is also a sort of daisy chain quality to the whole thing, so that  function a calls b, b calls c, c calls d, and so on.

I’m imagining that a better way would be to have a central function that controls the game and to which control returns after each ‘scene’. Is this the kind of thing composer does?

Hi

It says here: http://developer.coronalabs.com/node/26628

that objects should be returned like this:

local function createBox() &nbsp; &nbsp; local box = display.newImage("box.png") &nbsp; &nbsp; &nbsp; &nbsp; return box end

otherwise “you have no way of clearing its memory”.

Does this apply to display groups? Should I return these at the end of the functions that create them even if I’m already removing them and nilling them out. My groups are globals, as I need to access them in other functions (i.e. the even listeners that that they call and which in turn remove and nil them out)