Hacker News new | ask | show | jobs
by halpert 1624 days ago
I spent a few days making a (very) basic 2D game engine in Rust for fun. One thing I found is that I didn't find Rust that helpful in preventing bugs due to the nature of game engine code. For instance, I was using a generational array to store components. At some point I had a use-after-free bug in the generational array code that the Rust compiler could never catch. Also, components tend to have circular references to one another, which was very annoying to program in Rust. I'm curious if you've experienced something similar and what your thoughts are now, after many months in the trenches, on Rust's suitability for a game engine.
2 comments

Although I'm not _cart, I had a similar experience in making a (hobbyist) 2D game engine both in Rust and C++, and the problem you're facing (similar to the ABA problem) is not a memory safety error but more of a logical bug inherent in naively programmed object pools and is totally language-agnostic. When you create an object pool as Vec<Option<T>> and use a single array index as resource IDs, you risk this scenario: "X has a reference to resource A from object pool, A is destroyed and later reused by the object pool for resource B, now X has a reference to resource B". The problem is that the resource IDs will become invalidated as the object pool reuses its slots. The incremental generational counter is a way to check object lifetimes in object pools at runtime, and this is a solution to a logical error (which can be applied regardless if your language has a borrow checker or not). If you've had weird errors while using generational arrays, chances are that 1) you've exhausted your generational counter and it has overflowed 2) your generational array code is incorrect.

The verdict: Rust's lifetimes does not make you safe from non-memory-safety related bugs. It still gives you some really powerful abstractions to fight these bugs (like enums, traits, Option<T> and Result<T, Err> types), but other than that you're on your own.

(About circular references between components... doesn't this also get solved by generational indices? With Arc<T> types you're going to have circular dependencies that don't get freed because of reference counting, but with generational indices you're free from that issue since you're manually managing resource lifetimes anyway. And if you're having trouble figuring out how to manage these dependencies, the solution might be to refactor your code. My experience of using generational arrays was that it will naturally move your code-base towards centrally managing resources in a unified fashion, which is rather different than the usual Rust/C++ model of every object having its own independent ownership. After embracing it I tend to have less of those resource management dependency headaches.)

I want to point out to unfamiliar readers that use-after-free in a rust generational array is a logic bug, where as use-after-free in just about any other non-rust context is undefined behavior. Rust's safety is helping here, not by preventing the bug altogether, but by severely limiting the damage it can cause.
Are there any good learning resources on how to program to get around these "logic bugs"?

I had a runtime error with my first Bevy game too, because I forgot to add a resources to my app before I used it.