Exoframe Autonavigation and Refactoring into a Workspace
Overview
The past month has been full of excitement and tedium. Excitement due to a very cool, essential game feature being added. Tedium due to the massive amount of work that was needed in order to refactor Exofactory from a single crate to a workspace-based project.
On top of this, I did plan to do a bit more, but with the massive historic June heatwave it was difficult to muster the will to do more than the "don't starve today" work in a hot office in which the overworked, underpaid AC was not keeping up.
Regardless, the Exoframes as Agents feature, I think, counts towards the quality-over-quantity style of blog post. I'm pretty proud of it.
In addition to this, I cover the efforts I made in refactoring the game from a single Rust crate into a workspace. I cover the steps I took, why I took them, how I took them, and I give my thoughts on how others might go about it.
As always, if you find this post interesting, I would encourage you to wishlist the game on Steam.
Exoframes as Agents
In Exofactory, quality-of-life upgrades are unlocked as the player makes progress building up their infrastructure.
In a previous post, I covered placing buildings in global mode. Having Exoframes act as agents is a class of quality-of-life upgrades that can be unlocked as things proceed.
Basically, Exoframes can be tasked to do several things automatically, navigating the world and interacting with it without the need for "conscious" control.
Technically, this was done with the help of two excellent Bevy ecosystem crates, namely bevy_rerecast and bevy_landmass.
Bevy Rerecast builds and updates a navigation graph based on the recast algorithm and, amazingly, can just straight up use the existing Avian meshes. Mind-blowingly convenient.
With the navigation graph "solved," the next step was to actually use this graph to move things around.
Looking again at the ecosystem, I considered two potential libraries: vleue_navigator and landmass_rerecast.
To the best of my understanding, vleue_navigator is a lower-level navigation library giving fine-grained control, while landmass_rerecast is a higher-level navigation library that abstracts the control away into a higher-level "agents" concept where the user of the library focuses more on goals and destinations rather than the details of navigation.
Given the name and heading of this section, you may have correctly guessed I went with landmass_rerecast, because it matches the approach I wanted to take while fitting the in-game understanding of how Exoframes might be controlled. (aka subconscious subprocess agents)
It worked quite smoothly, even wiring properly into bevy-tnua. It's cool because that means agent Exoframes navigate and move around using the same code path as players would.
From here I wired up some simple components to mark Exoframes that should act as agents and did some testing.
Progress.
From there, item fetching and requesting was implemented along with a basic UI around it.
Then the last bit, a new dashboard where players can configure all existing Exoframes. Hopefully this will be the last UI I ever write without Bevy Scene Notation, aka BSN.
In the end, we can now summon, request, and designate a pool of Exoframes to be switched around to or used as fetchers.
This is all very cool for sure, but I think the main takeaway point here is that all of this work was only possible because of the excellent, highly interoperable Bevy ecosystem. Crate authors know of other crates and work to ensure there is a smooth, highly ergonomic developer experience.
There are a ton of amazing Bevy crates out there. I recommend you explore them through either the official Bevy asset site or, secondarily, my personal BevyDex project.
Codebase Refactor

The other large chunk of work I did revolved around a major refactor of the game from a single crate to a workspace of crates. The reason why I did this was because I got to the point where even making a small change, even a single const change, would result in annoyingly long compile times, even with dev builds. You can only wait 45 seconds for a dev build while tuning walking wobble constants so many times before you decide something needs to be done. Make that 1:10 for release builds to actually confirm that those consts look good.
Feels even worse seeing all of the idle cores sitting there while one is pegged at 100%. Feels like a waste.
Below is a simplified description of the approach I took to address this.
My Strategy
My plan was to just pull out each of the "root level" Bevy plugins into its own workspace crate. I had been focusing on proper root-level plugin isolation so most of the plugins were pretty easy to move. Just a lot of import changes.

The issue was that there were several sets of Bevy plugins that imported components and resources from each other. The above is an example of the sort of thing I would see. These exceptions that I made were the cause of much pain and suffering. There were a few potential circular dependencies that needed to be prevented. BUT I really wanted to allow as much parallel compilation as possible so I wanted to keep the workspace as flat as possible. Workspace crates should have the absolute minimum intra-workspace dependencies.
Here I took a staged approach. Stage 0 was to just move all the easy, no-changes-needed root-level plugins into their own workspace crates.
Stage 1 - Isolate Shared Components

The first step was to move the shared components/resources/message types, etc. out of the plugins into their own workspace crates, just modifying imports as needed. I did this because I wanted to incrementally split things out without needing to break the game for a long time.
I added the workspace, added the exo_* crates, and moved the relevant items over.
Stage 2 - Move Systems

Here things got a little tricky. I needed to move the systems out of the main crate. The issue was that the shared components would still be needed everywhere, resulting in reduced parallel compilation.
The result I went with was to keep the highly shared components in the relevant crates and to just add dependencies for them everywhere as needed. As long as I don't change these on sequential compiles I still get that parallel work. Given that these core components don't change often (or at all in some cases) this was acceptable.
The plugins and systems that are more subject to change were moved into exo_*_runtime crates.
This resulted in a small handful of Bevy plugins that were split across runtime/non-runtime crates, which feels annoying, but it's a compromise I was ok with.
Also, notably, the workspace crate dependency graph is definitely not flat. I did aim for as flat as possible, but I wanted to avoid rewriting too much. It's definitely MUCH better though.
Post Refactor Results

The above graph is a fresh rebuild after a cargo clean. Down from 1:10 to 45 seconds for a full release build from scratch. Since recording these compile times, even more has been split out of the "main" Exofactory crate, but I have since added features so it's less apples-to-apples.
The bigger win, though, comes when doing actual work on the game. It's rare that I ever touch more than one, or at worst a few crates at a time when working. This means that I get dev builds in a few seconds and release builds in a few more seconds.
A huge quality-of-life improvement. That and it lets me actually get work done on my much weaker laptop.
I'm not sure I went through this refactor in the most efficient way, or even in the way that produced the best possible results. If anyone sees anything dumb I did, please let me know. This all being said, I did meet my goals, these being:
- Fast compile times
- Minimal logic changes
- Crate separation at the Bevy plugin level (mostly)
My Advice
Keeping the caveats above in mind, if I had to give advice to my earlier self (or others) I would narrow it down to:
- Start with one crate and do everything there. No need for preemptive optimization.
- Be 100%, fully, completely, no-joking-around strict about plugin isolation. No plugin should import anything from any other plugin ever.
- All plugin imports should be from either that plugin, or non-plugin, independent structs that are independent of other plugins.
Doing these three points would have let me work on the game at the velocity I wanted while allowing me to refactor the game into a workspace at a later date without pain.
As for when to migrate from a single game crate to a workspace, I think the main thing is to not overthink it. When compiling and testing feels annoying it's time. You know it when you feel it.
Conclusion
Really cool to see the little Exoframes moving around in the game. For me, it was a "This is really cool" moment. I think the interplay between all of the Bevy ecosystem crates made it much easier than it had any right to be.
Really glad to be done with the refactor. I'll be coding, camping, and running a Bevy workshop at Bornhack so it's a good time to be able to work on the game from my laptop.
If we see each other there say hi!
Last off, once again, if you think you are interested in the game or just want to show your support for the game, wishlisting the game on Steam helps a ton.