"Don't write games in Rust" and how fast and easy can you access mutable data?

Or if you can, the Iterator methods that do that for you, safely!

On that note, I watched a Mark Darah video recently talking about how the Frostbite engine was hard to work with because they prioritized performance above all and made it harder for the end users as they could easily make mistakes and brake things.

I wondered if with a strong type system and systems programming language that can be reasonably used as a high level language if that gap could be filled somehow.

Yes, I think it may be the case. I've seen some comments in blogs saying basically "in Rust, you have to constantly fight the borrow checker", which is obvious newbie's experience, but unfortunately for some it may turn into sort of common sense judgement.

Regarding cultural considerations -- I'm curious of sociology, and think there's a lot of to study and discover here, and ideas/hypothesis to test.

But just saying that this is cultural setup will be too simplistic. This statement also implies that Rust should be definitely superior, but people don't switch to it for network effects -- ecosystem, habits learned from others.

But I think more realistic is to say that there are trade-offs, as others pointed out. Or maybe there's a concrete wall I'll run into if I try developing games in Rust? So I want Karl Popper's approach: a hypothesis -- that you can develop games -- and try actively to disprove it by finding the most unfavorable condition.

I'm sorry for speculations, because I have no game dev experience, but here's a case where I see unsafe has a relative advantage.

Suppose, we're rewriting Doom in (embed) Rust (e.g. to run on pregnancy test :)). In Doom, if a monster is hit by another one, it will retaliate (cool feature, it gave the game much more life back then). And this happens not just with bullets, but with fireballs too. So a fireball object must also indicate who fired it. Then the monster object will need to have a record of who it's attacking, to aim at.

I thought of what if we just create all objects and then turn visibility flag on/off, or change state (alive -> corpse). But there's Nightmare difficulty, when we have to create new monsters every once in a while (as well as the temporary teleport flash sprites). So preparing everything in advance is too hard. (Maybe even impossible, depending on requirements.)

So, how do we store objects and refer to them?

I leave smart pointers aside, because they're obviously slow.

If we store the objects in Vec<T>, we can't have indices as IDs, because they'll be constantly changing.

If we choose Vec<Option<T>> or even [Option<T>; n], IDs are stable. But with array there's a compile-time hard limit on objects count, and we'll have to make a complex logic to check and reuse free slots. With Vec, we'll need this logic too, plus maybe some vacuum.

I can think of a procedure to check if a referenced unit is gone. Instead of deleting a unit, we mark it as deleted and put a simple counter, that it expires in 3-4 frames (maybe more), and reduce it every frame. During these cycles, every unit that needs a lookup can check if the target is dead, and remove its pointer. Then there will be no references, and the unit is cleaned up, and the slot freed.

If we decide to use unique Id generator (let's say just a u16 counter), and HashMap<UnitId, Unit>, all the mentioned problems are gone, but the look-ups become an order of magnitude slower than with vec/array (IDK, maybe there's a less universal and faster hash function for smaller numbers of object), and it becomes a problem if we need a look up on every frame.

OTOH, for the case of monster following and attacking another monster, we can do a look-up for aiming once in 20-40 frames, and the players won't notice. But a game like racing simulator will require lookups on every frame, e.g. collision checks, or tire-ground contact.

So in C we could just look up once and then store a reference to the data inside hashmap. But I think in Rust it shouldn't be much harder -- we could get a ref to a HashMap item, and then maybe cast it as usize? (the hash map has to be Pin'ned, of course) And the rest should be the same. Casting usize into a ref takes several cycles, but still should be faster than executing the hash function and jumping ram addresses.

Early video games on consoles or arcade machines, with extremely limited memory by modern standards, would in fact work this way. Actions that spawn new objects would simply not execute (or do something else) if all object slots are already occupied/alive. But even without memory constraints, this is also relevant to computation; if you have too many objects you can’t afford to update all of them, so you can and should stop before then. And you often don’t want an unbounded number of objects for gameplay reasons either; e.g. even if the unbounded logic would lead to the player being killed by a massive cluster of enemies, you might decide to rule that out for game balance. So, designed limits on object counts are still highly relevant today.

I would suggest that you look at and play around with Rust ECS libraries. They can be highly efficient, present an API based on “unique ID”s used only when necessary, and are a quite common way to write games.

Note that this is a classic way to end up with use-after-free bugs when the game objects are deleted/despawned. If you have such references you must always be careful to clean them up.

ECS libraries solve this by having IDs which are harmless to keep around after the entity they refer to is gone.

Are they? Put Rc's Weak reference on a fireball; that has the same cycles overhead as a counter, but has the right semantics by default.

As mentioned, you're wandering in the direction of an ECS here, but you don't need to go that far to get most of the value.

Many games, including both Doom and modern games, do pretty much keep entities as a big flat list and reference them by index. Handling dead enemies and the like is often much simpler than you might think: just an equivalent to dead: bool or type: Type::None,.

There's lots of ways to handle removing stale references reliably (not always used correctly leading to "interesting" bugs!) - some examples are a side-list/bit set of removed entities that get cleared next tick (eg if killed.has(self.target) { self.target = self.find_new_target(); }), or a generational reference (basically ids are a tuple of the index and a "generation" of how many times that index has been removed, and it's treated as empty if the generation doesn't match)

There's lots of these tricks that Rust pushes you towards that turn out to just be simpler, easier, and well performing.

also note that you have to be careful to notice if/when you accidentally start reinventing a ECS (in which case pulling in a ECS library is a good idea),

once you have 1 big array of things you may (or may not) want different arrays for different categories of unrelated things to speed up iteration over those categories,
then you may want a way to conveniently pull data from different arrays,
and a way to schedule functions to run at specific points in time,
and suddenly you're developing a ECS instead of a game.

"My game Is not yeet finished but now I have two 2D libraries, one 3D framework and four ECS sistems..." :smiley:

In normal programming, we would say that an ECS is emerging. In GPU compute, that's just how you do things normally :slight_smile: