{
"path": "/bevy-ecs-rust-type-system.html",
"site": "at://did:plc:x67qh7v3fd7znbdhauc45ng3/site.standard.publication/3mjcd2t6afe25",
"$type": "site.standard.document",
"title": "The Rust features that make Bevy’s systems work",
"updatedAt": "2022-09-15T00:00:00.000Z",
"publishedAt": "2022-09-15T00:00:00.000Z",
"textContent": "[Bevy][bevyengine] is a game engine written in [Rust][rust-lang] that is known for featuring a very ergonomic entity-component-system.\nIn the ECS pattern entities are unique things (e.g. objects in a game world) that are made up of components.\nSystems process these entities and control the behavior of the application.\nWhat makes Bevy’s API so elegant is that users can write regular functions in Rust,\nand Bevy will know how to call them by their type signature,\ndispatching the correct data.\n\n[bevyengine]: https://bevyengine.org/ \"Bevy Engine\"\n[rust-lang]: https://www.rust-lang.org/ \"Rust Programming Language\"\n\nThere is already a good amount of documentation on how to use this to build your own game (e.g. in [here][ecs-intro] in the Unofficial Bevy Cheat Book).\nInstead, this post will explain how this is implemented in Bevy itself.\nTo do so, we’re going to build a small Bevy-like API from scratch that accepts arbitrary system functions.\n\n[ecs-intro]: https://bevy-cheatbook.github.io/programming/ecs-intro.html \"Intro to ECS - Unofficial Bevy Cheat Book\"\n\nThis pattern is very generic and you can apply it to your own Rust projects.\nTo illustrate this,\nthe last section of this post goes into more detail on how the Axum web framework uses this pattern for its route handler methods.\n\nThis post is for you if\nyou are interested in type system tricks and are familiar with Rust.\nYou can see it as a follow-up to my previous post on [the implementation of Bevy's labels][bevy-labels].\n\n[bevy-labels]: https://deterministic.space/bevy-labels.html \"How Bevy uses Rust traits for labeling\"\n\nNote: This post uses Bevy version 0.8.\n\nThe user-facing API of Bevy’s system functions\n\nFirst off, let's look at how Bevy's API is used\nso that we can work backward from it to recreate it ourselves.\nHere's a small Bevy app with an example system:\n\nWhat you can see here is that we can pass a regular Rust function to add_system and Bevy knows what to do with it. Even better, our function parameters are used to tell Bevy which components we want to query: We want the Transforms from all entities that also have the custom Player component.\nBehind the scenes, Bevy even infers which systems can run in parallel based on the function signature.\n\nLet’s start humble: We just want add_system\n\nBevy has a lot of API surface; after all it is a full game engine with a scheduling system, 2D and 3D renderer, and many other things in addition to its entity-component-system. We’re gonna ignore most of this and instead just focus on one thing: We want to add functions as systems and call them.\n\nFollowing Bevy’s example, we’re gonna call the item we add the systems to App, and give it just two methods, new, and add_system:\n\nOh, this leads to the first problem: What is a system? In Bevy, we can just call the method with a function that has some useful arguments, but how do we do that in our own code?\n\nAdd functions as systems\n\nOne of the main abstractions in Rust is traits. They are similar to interfaces or type classes in other languages. We can define a trait and then implement it for arbitrary types so that the trait’s methods become available on these types. Let’s create a System trait that allows us to run arbitrary systems:\n\nNow we have a trait for our systems, but to implement it on functions we need to use two additional features of the type systems.\n\n> Rust type system tricks:\n> Rust uses “traits” for abstracting over behavior.\n> Functions implement some traits like [FnMut][trait-fnmut] automatically.\n> We can implement traits for all types that fulfill a “constraint”.\n\n[trait-fnmut]: https://doc.rust-lang.org/1.62.1/std/ops/trait.FnMut.html \"FnMut in std::ops\"\n\nLet’s use this:\n\nIf you’re not used to Rust, this might look quite unreadable. That’s okay, this is not something you see in an everyday Rust code base. You can read the first line as “Implement the system trait for all types that are functions without arguments that return nothing” and the following as “the run function takes the item itself and — since that is a function — calls it”.\n\nThis works, but is quite useless — you can only call a function without arguments. But before we go deeper into this, let’s fix up this example and make it runnable.\n\nInterlude: Runnable example\n\nThe definition of App above was just a quick draft. To make it use our new System trait, we need to make it a bit more complex.\n\nSince System is now a trait and not a type, we can’t directly store it anymore. Why? Because we can’t even know the size of what a System is because it could be anything!\nInstead, we need to put it behind a pointer, or, as Rust calls it, put it in a Box. This means that instead of storing the concrete thing that implements System, you just store a pointer.\n\n> Rust type system tricks:\n> You can use “trait objects” to store arbitrary items that implement a specific trait.\n\nFirst, our App now needs to store a list of boxes that contain things that are Systems. In practice it looks like this:\n\nOur add_system method now also needs to accept anything that implements the System trait, and then put it into that list. The argument type is now generic: We use S as a placeholder for anything that implements System; and since Rust wants us to make sure that it is a thing valid for the entirety of the program, we are also asked to add 'static.\nAnd while we’re at it, let’s also add a method to actually run the app!\n\nWith this, we can now write a small example:\n\nYou can play with the full code so far [here][play].\nNow, back to the problem of having more complex system functions.\n\n[play]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3fe777f4a178aac4568c05dd621644b6 \"Rust Playground\"\n\nSystem functions with parameters\n\nLet’s make this function a valid System:\n\nThe seemingly easy option would be to add another implementation for System to add functions with one parameter. But sadly, the Rust compiler will tell us that there’s two issues:\n\n1. If we add an implementation for a concrete function signature, the two implementations would conflict (code [here,][play-2] press run to see the with error).\n2. If we made the function they accept generic, it would be an “unconstrained type parameter” (code [here][play-3]).\n\n[play-2]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=851bba8bbe9b29df018b2d30c8d9f838 \"Rust Playground\"\n[play-3]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ddc7b3af90e6af418bc99fd9b351c9ee \"Rust Playground\"\n\nWe’ll need to approach this differently.\n\nLet’s first introduce a trait for the parameters we accept:\n\nTo distinguish the different System implementations, we can add type parameters, which become part of its signature:\n\nBut now the issue becomes that in all the places where we accept System, we need to add this type parameter! And, even worse, when we try to store the Box<dyn System>, we’d have to add one there, too:\n\n(By the way: If you make all instances System<()> and comment out the .add_system(another_example_system), this compiles.)\n\nStoring generic systems\n\nOur challenge is now this — get all three:\n\n1. We need to have a generic trait that knows its parameters.\n2. We need to store generic systems in a list.\n3. We need to be able to call these systems when iterating over them.\n\nThis is a good place to look at Bevy’s code. When you start digging in, you’ll see:\n\n- Functions do not implement [System][trait-system], but [SystemParamFunction][trait-systemparamfunction]!\n- [add_system][struct-app] does not take an impl System, but an [impl IntoSystemDescriptor][trait-intosystemdescriptor]. This in turn uses a [IntoSystem][trait-intosystem] trait.\n- And actually, the thing that does implement System is [FunctionSystem][struct-functionsystem], a struct.\n\n[trait-system]: https://docs.rs/bevy/0.8.0/bevy/ecs/system/trait.System.html \"System in bevy::ecs::system\"\n[trait-systemparamfunction]: https://docs.rs/bevy/0.8.0/bevy/ecs/system/trait.SystemParamFunction.html \"SystemParamFunction in bevy::ecs::system\"\n[struct-app]: https://docs.rs/bevy/0.8.0/bevy/app/struct.App.html#method.add_system \"App in bevy::app\"\n[trait-intosystemdescriptor]: https://docs.rs/bevy/0.8.0/bevy/ecs/schedule/trait.IntoSystemDescriptor.html \"IntoSystemDescriptor in bevy::ecs::schedule\"\n[trait-intosystem]: https://docs.rs/bevy/0.8.0/bevy/ecs/system/trait.IntoSystem.html \"IntoSystem in bevy::ecs::system\"\n[struct-functionsystem]: https://docs.rs/bevy/0.8.0/bevy/ecs/system/struct.FunctionSystem.html \"FunctionSystem in bevy::ecs::system\"\n\nLet’s take inspiration from that and make our System trait simple again. The code from above gets to continue on as a new trait called SystemParamFunction.\nWe’ll also introduce an IntoSystem trait which our add_system function will accept:\n\n> Rust type system tricks:\n> We use an [associated type][ch19-03-advanced-traits] to define what kind of System type this conversion will output.\n\n[ch19-03-advanced-traits]: https://doc.rust-lang.org/1.62.1/book/ch19-03-advanced-traits.html \"Advanced Traits - The Rust Programming Language\"\n\nThis conversion trait still outputs a concrete “system”… but what is that? Here comes the magic: We add a struct FunctionSystem that will implement System and we’ll add an IntoSystem implementation that creates it:\n\n(As you can see, SystemParamFunction is the generic trait we called System in the last chapter.)\n\nNote: As you can see, we’re not doing anything with the function parameters yet. We’ll just keep them around so everything is generic and then “store” them in the [PhantomData][struct-phantomdata] type.\n\n[struct-phantomdata]: https://doc.rust-lang.org/1.62.1/core/marker/struct.PhantomData.html \"PhantomData in core::marker\"\n\nTo fulfill the constraint from IntoSystem that its output has to be a System, we now implement the trait on our new type:\n\nNow we’re almost ready! Let’s update our add_system function and then we can see how this all works:\n\nOur function now accepts everything that imple",
"canonicalUrl": "https://deterministic.space/bevy-ecs-rust-type-system.html"
}