Hello! Welcome once again to the WalkScape dev blog.
For the past week or so, following our most recent update, Iāve been laying the groundwork for our Quality of Life (QoL) improvements. Before introducing any new QoL features or changes (with Gear Sets being the first on our list) I decided to start by optimizing and overhauling the game engine itself. This has been in the planning stages for quite some time and will likely take a couple more weeks to complete.
I know we have many programmers and game developers in our community, so this dev blog will feature some technical details especially geared towards you. I hope youāll find it interesting! But before we dive into the tech stuff, Iāve got some teasers and general info for those who prefer the less technical side of things!
Teasers and general info
Alright ā letās start with the teasers! We have two new skills coming to the game, hopefully in the next major content update. Hereās some art to help you figure out what they might be:
The design and art process for these new skills, related activities, and items is already underway. However, it will take at least a few months to complete, as much of the designed content depends on the Quality of Life (QoL) updates.
In general news, if you missed it, we released a minor update and hotfixes to the game last week:
These fixes addressed many of the remaining issues, though a few minor ones still persist. Weāll tackle those in future updates.
Lastly, myzozoz has been hard at work creating and designing the āParty system.ā Progress on this front has been great!
Why the engine overhaul is needed?
There are four major reasons for this overhaul:
- To improve game performance, reducing loading times and eliminating UI lag.
- To enable processing the game loop for notifications, on the server and potential future companion apps (such as for smartwatches) more easily.
- To enhance stability and introduce elements that increase the gameās scalability and flexibility for planned features and content.
- To address the issue of uncounted steps by implementing a game state rollback feature when step loading is interrupted (e.g., by closing the game or losing internet connection).
Currently, the main cause of lag (especially when loading a bunch of steps) is that the game processes all heavy calculations on the same CPU thread that renders the game (Flutter Isolates provides more information). This approach worked well for a long time, as the game loopās calculations were simple enough to avoid lag. However, as the gameās complexity has grown, particularly when loading thousands of steps, it can now cause several seconds of āfreezing.ā
The solution? Run the processing on a separate thread while optimizing the code as much as possible.
Game loading time
Iāve begun this overhaul by reworking how the game loads, which will likely be the most significant āvisible changeā for our players upon release.
By implementing multi-threading, Iāve reduced the gameās loading time from a 5+ seconds to be around 500ms (excluding server calls, which Iāll optimize soon). Previously, the gameās data had to be processed synchronously (loading one file at a time). Now, using multi-threading, or āisolatesā in Flutter, we can load all game data simultaneously. This approach doesnāt compete with the UI thread for resources, making it even faster in practice. Our goal is to reduce the loading time so the game is ready before the Not a Cult logo fades away, as long as thereās no update to download and you have solid connection.
The main limitation of using isolates in Flutter is that they donāt share memory. This means the game needs to load in the isolate, which saves the data to its memory and then sends the necessary parts back to the UI thread for rendering. This brings us to our next point: decoupling!
Decoupling game logic from the application
Previously, the application and the game processing logic were tightly coupled. This is common in many games, as decoupling isnāt always necessary, although it is a good practice. For WalkScape, several factors have necessitated decoupling the logic from the game into an entirely independent module.
The primary reason for decoupling is to enable notifications. We can then run the game loop quickly in the background to process steps and provide accurate information about your progress. This can also be used in things like home screen widgets, smartwatch companion apps, and whatnot. Additionally, we can move some game processing to the server side where necessary. Since WalkScape server also uses Dart (the programming language for Flutter), sharing the code is seamless. This approach allows us to process everything locally for the single-player version of WalkScape as well.
To achieve this, Iāve created a separate module (known as packages in Flutter development) for the game. This self-contained module includes all the gameās classes, logic, and messaging between the main thread and the āgame processingā isolate threads. We can add this package to any application, effectively sharing it between the game, development tools, server, and potential future companion apps.
The main challenge in creating such a module is ensuring that it remains agnostic to whatās running it, thus achieving proper decoupling. Moreover, when the server processes game logic, it must be able to handle multiple players concurrently, necessitating a stateless design (for a detailed explanation, see this Stack Overflow comment). The concept that can tie it together is called an Event Queue, which you can read more about here.
While most games implement a similar structure, and WalkScapeās engine already had a comparable system, it wasnāt fully decoupled or stateless. So, our next challenge is: how do we achieve that?
Making a stateless Event Queue
Iāve been preparing the engine for this overhaul for months, as such a massive change couldnāt be achieved quickly. Most components needed modification to become stateless.
To achieve statelessness, every Event in WalkScape (such as ācomplete action,ā āgain item,ā āarrive at location,ā āopen treasure chest,ā or āgain XPā) is its own, isolated function. These then receive a state (typically the Player Character) and return the updated state. Hereās a simplified example:
- Player loads 1000 steps.
- This is sent to the Event Queue with the current Player Character as āLoad Stepsā Event.
- Event Queue processes the āLoad Stepsā Event with the received Player Character (currently doing the āCut Birch Treesā activity for example) and 1000 steps as inputs.
- It processes ācomplete actionā Events, forwarding the Player Character and returning updated versions as needed.
- After processing all steps or encountering an interruption (e.g., becoming overencumbered), the Event stops and returns the fully processed Player Character.
- The updated Player Character is then saved to the server, stored locally, and sent to the UI thread to render the new game state.
This approach pretty much guarantees that as long as a Player Character is given as input itāll work similarly anywhere and allows simultaneous processing of multiple Player Characters.
Other considerations and optimizations
That was a rather technical explanation, and I hope some of you find it interesting!
All of this comes with some considerations and caveats, as seasoned programmers might already know.
- For many game events, the order of processed events is crucial (avoiding race conditions). To address this, Iāve implemented two event queues: ordered and orderless. The ordered event queue maintains the sequence and links events to other events that theyāre dependent on. If an error occurs, all linked events are cancelled, and the system returns to the original state with an error.
- In WalkScape, events are completed asynchronously (on a separate thread or server), so the UI must account for this. If the response with an updated state takes a significant amount of time, the UI needs to reflect this. Some events, like loading steps, must also be able to block UI interactions during processing.
For players, this overhaul not only makes the game load and run faster but should also fix the remaining issues with step loading. Currently, if you interrupt step loading by closing the game, putting it in the background, or losing internet connection, the steps are often lost entirely. With the new system, if an interruption occurs during processing, the game state wonāt change or save, ensuring the steps will load correctly the next time you open the game.
Iāve also implemented some basic but effective optimizations. When I started working on WalkScape, I used many ordered lists as data structures. This was convenient and wasnāt problematic when the game had less data to process. However, as the gameās complexity increased, these lists began to contain hundreds of objects, causing performance issues when searching for items (which takes linear time, O(n)).
A simple fix for this is using key-value maps for data structures where weāre not searching, but only getting or setting values. This reduces operations to constant time, O(1). For data structures that require searching, using ones that allow for logarithmic time operationsāsuch as binary search trees, for which Dartās closest equivalent is SplayTreeSetāmakes a huge difference in performance.
Until next time
Phew, that was a lot of technical stuff! I hope you found it interesting and useful. As always, Iām happy to answer any questions or address comments you might have. And even if the tech details arenāt your cup of tea, I hope the teasers made it worthwhile!
Happy walking, everyone, and donāt forget to stay hydrated! ā¤ļøļø
Exciting! I wish more devs spent time optimizing their games.