devlog 7

whoops! I forgot about this devlog and I had a long haitus. in summary: depression.

I’m back. hi. here’s a discord server.

what’s new

I rewrote the level loader again. previously the world was loaded all at once when you opened the game. this is now done dynamically!

level loader

the way this works is simple enough. first, remember that we have one main goal for this game’s performance: stutters must never happen. there are two different bottlenecks when loading a room.

first bottleneck is the actual act of loading the room file into memory, where the os and speed of the hard drive ultimately decide how long this takes. if the player has a slow hard drive, loading the file could take some milliseconds worst case scenario. at 60fps, 16.7ms per frame, something taking even just one millisecond is a massive hit to the performance budget. even if the player had a fast hard drive, the os could be in a bad mood and defer providing access to the file for a long time. the obvious solution is simply using an async file load function. this would avoid the stutter, but there’s a particular edge case in that the player could find themselves inside a room who’s file hasn’t finished loading yet. how could you handle this? ideally, we could somehow convert the async process into a blocking one, but Gamemaker provides no such construct.

second bottleneck is unpacking the room file into tilemap and entity data. this is more dependant on implementation and cpu speed, so we actually have control on this front.

here’s the solution I arrived at. there are two “loading zones”: the “prep” zone, which if a level is inside, its data starts getting loaded. this has a large radius around the camera. then inside that zone is the “load” zone, which is where the level’s entities are created. we have to make sure that the level’s data has been fully prepped when a level enters here.

the level loader maintains a priority queue of tasks. individual tasks simply have a process() function, that return a status of complete (signifying that the task is complete and should be discarded), running (signifying that the task hasn’t finished processing), or wait (signifying that the task hasn’t finished processing, and at least one frame should pass before process() is called again). each task has a numeric priority, where smaller numbers are prioritized. a priority of 0 means that the task must be fully complete when it is in the load zone.

the basic gist of the system itself is as follows. when a level boundary enters the “prep” zone, a “load_file” task is created (priority 0). as of writing this, all this does is synchronously open the file, then create a “parse” task for the file. the plan is to replace this with an asynchronous open, and while the file is being open, the task will return a status of wait. this doesn’t mean much until the room the task is for enters the load zone, where the loader will simply freeze the game until the task is complete. a “parse” task (priority 0) just quickly goes through the file and figure out where each layer is stored (without parsing the layer itself). it then creates a bunch of mini tasks that actually parse each of these layers into usable data. the layers for entity data and tile collision data are given a priority of 0, the rest are given higher priorities since they are technically not neccessary. when a level boundary enters the “load” zone, the “load” task is created (priority 0), which simply creates all the level’s entities. when a level exits the load zone, an “unload” task is created, which marks the level as unloaded, thus flagging it’s entities as deletable. when a level exits the prep zone, a “destroy” task is created, which deletes all the tilemap data the level kept.

that’s about it. the actual implementation obviously handles more complex situations… for example, what happens if a level entered the load zone before it entered the prep zone? but this gets to the point of how it all works.

I’m quite proud of this. the game now only manages around 60mbs of memory, which is much, much better than previously. it still needs some optimization and gc taming, but it works. hopefully this will be the last time you hear me rant about level loading.

otherwise,

I also reworked most of the game’s game loop, to decouple it from the existence of nova. that was a lot of work.

otherwise, it’s just tweaks and tweaks and tweaks. world 1 is around 33% complete. nova doesn’t automatically grab anymore, that’s new. also they try to avoid getting squished when moving platforms crush them. I wrote new music. etc.