Hacker News new | ask | show | jobs
by noelwelsh 830 days ago
I've been using Bevy recently so here are some thoughts on this:

Firstly, the overall quality is high and seeing this attention being paid to the project's organization is another good sign.

Documentation is not great. The Bevy book runs out of content very quickly. The "Cheat Book" has additional useful information: https://bevy-cheatbook.github.io/. With these plus the examples I've been able to figure out everything I need, but it's slow going.

I'm not 100% sold on ECS. It loses a lot of type safety and there doesn't seem to be any way to ensure cleanup of entities and their components.

5 comments

Thanks (I am the aforementioned project manager) :) Docs are a high priority for me, especially beginner content, assets, and rendering for me. Rust's in-repo examples are really helpful for ensuring they stay up to date, but aren't a replacement for a guided tour.

As for the ECS, I've absolutely felt that: the flexibility of the polymorphism created by "just add another component!" can be both chaotic and freeing. Can you say more about what you're looking for with entity cleanup? I haven't heard that complaint before and want to make sure I understand what you mean.

Most games have different states. For example, you have the main menu screen and perhaps different levels. Each of these could be a state. I think Bevy already has an abstraction for this[0] but it only records the current state. There is no other information associated with it IIUC.

What I think would be useful is essentially an arena allocator associated with a state. So any components and entities created when a state is active would be removed when that state is exited. Otherwise it seems that one must do manual GC, which is error-prone and will almost certainly lead to slow leaks over time.

[0]: https://docs.rs/bevy/latest/bevy/ecs/prelude/struct.State.ht...

Ah okay! I can see the need for this. This is a large part of why the States machinery exists, but I see your point about it being error-prone to remember to tag and then despawn everything.

I'll chew on solutions here: we might be able to get away with a nice well-documented convention in user-space code, or we may need to add a small extension to the internals to make this more robust.

Isn't the typical way to do this by using a janitor? This would typically need to be done in user space (the framework doesn't know what cleanup you want to do), but Bevy could provide an interface for it with callbacks or whatever is more idiomatic in Rust/Bevy.
Last time I was using Bevy I had a cleanup system to solve exactly that issue. It queried for entities with the AssociatedWithState component, or whatever I called it, and destroyed any where the was a mismatch.

I could see an efficient version of that being a nice thing to have built in, though. It’s definitely a very common thing.

The release notes are great btw. Usually if I’m doing something for the first time I look it up in the bevy cheat book, get something that doesn’t work (because it’s two or three versions out of date), then check the intervening release notes. Usually there’s enough direction to figure it out.
They're so, so much work. Carefully going over the log, and patiently explaining / showcasing and contextualizing them is even more work than writing docs for the same feature, since you don't have the API to help structure things.

I'm glad it pays off! Seeing them come together makes my day every time, even when I'm writing half of it.

Oh, I know! I’m a maintainer of another open source project and the release notes are such a chore. For the quality of Bevy’s I could easily see it being a full time job, albeit a mostly thankless one. So thank you for the effort you’ve put in!
And I know so many people who think you could automate them from commit messages. Hell, no!
ECS is an architecture proposed by the same group that has said "if you know you're not going to use that memory again, just allocate over it". For certain domains, this is a superior solution to management than more costly cleanup. Re-initializing an allocator and leaving behind old data in now uninitialized memory is a lot faster than doing anything with that memory.

For type safety, entities define mapping of types [components are types]. The whole point of ECS is effectively managing types and ensuring correctly typed data is operated on.

A note on the ECS: This is largely due to poor understanding on my part, but I moved my computational chemistry (egui-based) GUI program (Visualizes various wave function properties) from Bevy to WGPU directly, because the ECS was difficult to grok and debug. It felt like more work to reinvent wheels regarding rendering than manipulate the ECS. I had it working in Bevy, but ported. Not a canonical use case for a game engine!

It's been ~2 years, but my main reason was wanting to write plain rust, vice using a DSL.

I moved my game away from Bevy to WGPU too because I needed tighter, more efficient control of the data than ECS could provide. Though being able to query data with types is amazing and really demonstrates how powerful Rust's type system is, it's not a solution for everything, and feels more geared towards one-off arcade style video game design.
How does it lose type safety?

I was under the impression that bevy's Query system is type safe.

ECS is basically a very simple relational database. There is no equivalent of foreign keys or other constraints between components. I can't say, for example, "every entity that has a location must also have an associated mesh". If I forget to add a component it's just a silent failure. Everything runs fine, just the expected result doesn't happen.
I wonder if a compile-time array-of-structs (AoS) to struct-of-arrays (SoA) transformation like the one done by Zig [1] could fix that issue.

[1] https://zig.news/kristoff/struct-of-arrays-soa-in-zig-easy-i...

You can probably write runtime tests for this. Some system which queries for component Location, and then does an assert for each component that the associated entity also has a mesh.

Granted, this is annoying and not a particularly great solution, but it would serve to give you a reminder for when you forget. I don't mean to diminish from your overall point either, it's a valid and good one I think.

Yeah, runtime checks are not very desirable.

I have this vague thought that Rust needs a different kind of type system.

Standard type systems[0] basically rely on references between values[1]. I seem to read about numerous Rust systems that don't use references for various reasons. Usually the borrow checker gets in the way or they want a different memory layout[2]. This makes me think that what's needed is a type system that allows constraints to be expressed between values without requiring a reference between them. I don't know what this type system looks like, though.

[0]: Rust's type system is a fairly standard type system from an academic POV. The novelty is that it's the first language with traction to use a linear / affine type system, but these are relatively well studied in academia.

[1]: The theory is not formulated this way, but the representation they compile into is a bunch of references.

[2]: ECS, as I understand it, is not a reaction to Rust's type system but an architecture driven by memory layout, parallelism, and breaking-out-of-OO-hierarchy concerns. However the net result is the same.

Totally agreed, like when using an int to reference into an array, there's no way to guarantee that the int won't be out of range, so it needs a check every time

Also no ideas here, but its fun to think about anyways

In theory this is what Bundles are for, though it's easy to not realize that there's a bundle for what you want to do.

I should note that this is a problem with most game engines, not something unique to Bevy. For example, Unity is infamous for requiring arcane combinations of components to avoid silent failures.

Personally i think the flexibility of ECS is worth the tradeoffs, but that’s just me.

I’m not sure what you mean by cleanup entities. Whenever i load a level i put it under a root entity and just despawn the root when im done.