DevBlog #48: Engine optimizations, and some teasers!
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!
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! ❤️️