Iterating in For Loops too Long - Playdate Crashes

Hello! I'm developing a 4x game with sizable maps in Lua. I use both the Linux and Windows SDKs for development, but upload the game to my Playdate device through Windows. So far I've had no performance issues in creating map data, whether it be 100x100 blocks or 10x10 (most likely because there isn't much data in a single tile yet). However; today I started working on creating a pathfinding system for units. I'm using the built-in A* pathfinding system and generate the graphs from scratch. I add a node to the graph with the X-Y coordinates of the tile I've just created. After all tiles have been created, I iterate through all nodes in the graph and draw a connection between valid tiles. It is in this loop where the device crashes.

I ran the loop over with a small-sized map (30x30), and it successfully created the map and displayed it. This means that the crash is not a result of bad code, and I could manage to create a larger map with proper optimization. The first thing I did was check to see how much memory my 30x30 map with a pathfinding graph took up. Curiously, it wasn't taken much at all, not even a Megabyte! This means that, unless the Device Info page is lying to me, the issue with the 100x100 map probably isn't memory related. Are there any issues that arise with iterating through a "for" loop for just too long? Does the playdate kill the program if it doesn't hear back after a set amount of time? What exactly is happening, and how can I circumvent around it?

Here's my code:

activeMap["width"] = width
activeMap["height"] = height
graph = playdate.pathfinder.graph.new()
local newNodeID = 0

local testUnit = true
for x=1,width do
	activeMap[x] = { }
	for y=1,height do			
		local val = math.random(10)
		
		if val >= 5 then
			activeMap[x][y] = {
				type = tileTypes.barren,
				travelType = travelTypes.land,
			}
			
			if testUnit then
				activeMap[x][y]["unit"] = createUnit(unitTypes.test)
				testUnit = false
			end
		end
		if val < 5 then
			activeMap[x][y] = {
				type = tileTypes.water,
				travelType = travelTypes.water,
			}
		end

		graph:addNewNode(newNodeID, x, y)
		newNodeID += 1
	end
end

-- Generates connections for map's graph data
print("Still doing fine!")
for x=1,width do
	for y=1,height do
		for xOffset=-1,1 do
			for yOffset=-1,1 do
				local skip =  false
				if x + xOffset < 1 or y + yOffset < 1 or x + xOffset > width or y + yOffset > height or (xOffset == 0 and yOffset == 0) then

					skip = true
				end

				local type = activeMap[x][y]["type"]
				if type == tileTypes.water then
					skip = true
				end

				if skip == false then
					local weight = 10
					local offsetSum = xOffset + yOffset
					if offsetSum == -2 or offsetSum == 2 or offsetSum == 0 then
						weight = 14
					end


					graph:nodeWithXY(x, y):addConnectionToNodeWithXY(x + xOffset, y + yOffset, weight)
				end
			end
		end
	end
end

print("Finished creating graph!")
activeMap["graph"] = graph

end

1 Like

There is a watchdog on the device (not on sim) that if a call takes longer than 10 seconds, then the game is killed. If it's taking longer than 10 seconds then you may want to 'time slice'. eg. do 20x20 chunks of tiles, then remember where you were up to and continue from there next frame. You could do this with a load screen, as you know how many times 20x20 goes into 100x100 (25 times)
30x30 = 900 tiles
100x100 = 10,000 tiles (more than 10x!)

1 Like

Thank you! I was wondering if it was something like that. I will definitely impotent a load screen tomorrow (it's probably a good idea anyway), you're awesome :blush:

Another way to avoid the watchdog timer is coroutine.yield(), which saves the Lua state while returning back to the system run loop so it can do what it needs to do (check the buttons, update the screen, etc.). In your case you could do that once every pass through the outer loop (or every nth pass, if a single pass takes less than the normal frame update time) and draw a progress bar along the way.

One catch is you can't call coroutine.yield() from the outer main.lua scope, since that code is run at load time instead of the main run loop. To work around that you can move the code into a loader function that gets called the first time through playdate.update():

note: untested code!

local yieldskip <const> = 10

local function setup()
    for x=1,width do
        if x % yieldskip == yieldskip-1 do
            drawProgressBar(x/width)
            coroutine.yield()
        end

        slowSetupFunction(x)
    end
end

function playdate.update()
    if setup ~= nil then
        setup()
        setup = nil
    end

    ...
end