I often fiddle around with writing rather barebones game engines in different programming languages, and I've moved my attention back to Rust after a year or so of nearly exclusively using C++.
I began adapting some recent C++ code of mine to Rust, specifically a group of game Systems that may depend on each other:
// c++ code:
sprite::system sprites{};
animation::system animations{sprites}; // "mutable ref" to sprites system
actor::system actors{animations}; // "mutable ref" to animations system
roamer::system roamers{actors}; // "mutable ref" to actors system
// ... etc.
// later...
actors.update(); // Might update some state in Animations and Sprites.
animations.update(); // Might update some state in Sprites.
sprites.update(); // ...and so on
All these systems are rather simple - they're just structs containing a few data containers, e.g. a sprites: Vec<Sprite> in the sprites system, and some logic to update and manipulate them. If some data in one system refers to another container or another system entirely, it does so through dumb newtype integer handles. This is all an attempt to apply some data-oriented design principles where I may previously have found very object-oriented solutions.
I'm finding that this code structure doesn't adapt well to rust. Namely:
Rust is not a fan of structs holding mutable references to each other and also getting mutated in the outer scope (i.e. in update() fns).
Iterating over one member of a system while mutating another is equally frustrating.
I've tried to get around this by making a sloppy sort of service locator. Each structure holds an immutable reference to this locator and uses locator.get::<System>().borrow_mut().whatever as needed.
None of this feels like the "right" way to structure this sort of application in rust, though. The RefCell+Rc workaround, especially, seems like unnecessary overhead for something that I know ought to work fine. I feel like trying to write a group of interconnected systems has me fighting against rust's strict safety rules at every turn.
Any thoughts on what the right path forward may be? I'm happy to hear what you think may be a better way to architect this.
These seem to contradict each other. If you are using integer handles, where are the mutable references arising? Seeing your struct declarations would help.
As a general rule, don't ever build your applicationâs data structures out of references. struct Foo<'a> is an advanced technique which is only useful in very particular circumstances.
Broadly speaking, I would recommend that you look at Rust ECS (Entity-Component-System) libraries. They generally provide data storage that supports the kind of borrowing patterns you want.
There may be ways to do things âfrom scratchâ that are reasonable, too, but itâs hard to say without more detail, and it sounds a lot like youâre already reinventing something sufficiently ECS-like that any Rust ECS will solve your data borrowing problems.
You may try fine grained interior mutability with Cell - wrapping small types in Cell and mutating them via get/set/take/update. This all your methods will be &self, and somewhere at the leafs of the call tree will interior mutability operations take place.
What Rust is trying to prevent is while you call update on A somewhere in it's call tree, somehow, another system will get to mutate A too - while &mut A tells that "I and only I have access to A". So &mut isn't really about mutation, but about exclusiveness - mutation is a pretty side effect that follows.
Additionaly, maybe you will be able to use ghost_cell - Rust. I don't know the structure of your application, but you'll probably be able to pierce the whole thing with a special marker lifetime and give the token to those who you want to mutate. And just like in previous setup, try to wrap GhostCell as fine grainely as you can - but in this case, you may follow this rule less closely.
As a general rule, don't ever build your applicationâs data structures out of references. struct Foo<'a> is an advanced technique which is only useful in very particular circumstances.
I disagree. Yes, it might be challenging for newbies, but structs with lifetimes are amazing and can describe various relationships extremely precisely. I am writing huge embedded applications without allocator at all, and it is not rare to see a struct with 3 lifetimes with different purposes. My std code doesn't seem to have them due to allocator which is a global mutable state.
These seem to contradict each other. If you are using integer handles, where are the mutable references arising? Seeing your struct declarations would help.
To clarify, my structs look something like this - with integer handles to individual members, but references to other systems.
struct Animator {
sprite: SpriteId, // handle to a sprite in the sprite system
// other state here
}
struct AnimationSystem {
sprites: &mut SpriteSystem, // reference to other system - probably bad
animators: Vec<Animator>
}
Broadly speaking, I would recommend that you look at Rust ECS (Entity-Component-System) libraries.
I'm trying to explore a sort of "do-it-yourself" approach here. Normally, I would absolutely be using an ECS for this.
I see. You should remove those references entirely. Even if all lifetime-annotation problems here were solvable, you still would not want to store a sprites: &mut SpriteSystem in AnimationSystem, because having &mut SpriteSystem means exclusive access toSpriteSystem for as long as AnimationSystem exists. You donât want that because then nothing else can update AnimationSystem.[1]
When executing a function that affects multiple âsystemsâ, then you pass &mut references for each system to that function. That keeps the references temporary, not lasting longer than one frame/step, so that you can have more than one use of SpriteSystem every frame.
(This makes the code look more like ECS code. That is not a coincidence.)
In some ways, it's the same thing as deciding that AnimationSystemownsSpriteSystem, except that final responsibility for deallocation is elsewhere. âŠď¸