The release of Bevy 0.6 is exciting. And with each release the ECS engine will become more powerful. Already I’ve become envious of several of key Bevy features1. I can’t bring myself to use the full engine, with the intolerable Windows compile times. Especially compared to a Macroquad / Legion setup.

Framework cargo run Times
Bevy Sprite Example 5.9
My Legion Game 2.3

The release has increase my desire to move completely to the Bevy Game Engine, but not without a significant compile speed improvement I will hold off. However, with the bevy_ecs crate I can jump start my migration. Overall I have found that change to mildly tedious, but simply done.

Differences between Bevy Ecs and Legion

World

Both Bevy ECS and Legion have concepts of a “World”. Which is a container for the entities and components. In legion Resources (non Entity-Component data) are maintained outside the World. As of Bevy version 0.6 the World contains Resources, Entity-Components, and Events in one collection.

Legion Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub struct LegionGame {    
    pub world: legion::World,    
    pub resources: legion::Resources, 
}    

impl LegionGame {    
    pub fn execute(&mut self, scheduler: &mut legion::Schedule) {    
        scheduler.execute(&mut self.world, &mut self.resources);    
    } 
}  

Bevy Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub struct BevyGame {
    pub world: bevy_ecs::world::World, 
} 

impl BevyGame {    
    pub fn execute(&mut self, scheduler: &mut bevy_ecs::schedule::Schedule) {    
        use bevy_ecs::schedule::Stage;
        scheduler.run(&mut self.world)
    }
}  

One gotcha that I ran into what that I needed to recognize that scheduler.run is part of a trait called Stage.

Schedule

Both Legion and Bevy ECS includes a Schedule struct manages the Systems Pipeline. However, there are two keys differences.

With default feature set of Legion will run stages in parallel between flushes.

Legion Example

1
2
3
4
5
6
7
8
pub fn build_systems() -> legion::Schedule {    
    legion::Schedule::builder()    
        .add_system(parallel_a_system())    
        .add_system(parallel_b_system())    
        .flush()    
        .add_system(singlem_c_system())    
        .build() 
}  

In the above example, parallel_a and parallel_b could be run at the same time while single_c system always runs after a and b.

However, Bevy ECS is built for a game engine suite, so has more complicated setup. In fact, there are multiple ways to do the same style of scheduling. However, we are trying to do a simple [[green to green refactor]] in this scenario, so we will pick a pattern that gives us the easier migration

Bevy Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pub fn build_systems() -> bevy_ecs::schedule::Schedule {    
    use bevy_ecs::schedule::Schedule;    
    let mut schedule = Schedule::default();    
    schedule.add_stage("start", SystemStage::parallel());    
    schedule.add_stage_after("start", "single", SystemStage::single_threaded());    

    schedule.add_system_to_stage("start", parallel_a);    
    schedule.add_system_to_stage("start", parallel_b);  
    schedule.add_system_to_stage("single", single_c);    

    schedule
}  

Components

in Legion and BevyESC before 0.6.0 components were simple Structs or Enums that were sized , send, and sync. However, as of Bevy ECS 0.6.0 components require the Component trait which can be satisfied with a simple #[derive(Component)]

Legion Example

1
2
3
4
5
#[derive(Debug, Clone, Copy, PartialEq)]  
pub struct Velocity {  
    pub x: f32,  
    pub y: f32,  
}  

becomes

Bevy Example

1
2
3
4
5
#[derive(Debug, Clone, Copy, PartialEq, Component)]  
pub struct Velocity {  
    pub x: f32,  
    pub y: f32,  
}  

The System Functions

Systems in general, are the major UX differences between Bevy ECS and Legion. Not only how the Systems are Scheduled, but how the systems are written. However, the changes are not so significant that you need to re-write your Systems from the ground up.

The first difference between Legion and Bevy ECS is that Legion requires you to notate a function is a System using the #[system] macro. Bevy ECS relies on traits to decide if a function is a valid system.2 What this means is that every Bevy ECS parameter needs to be a valid implementation of the System Parameter trait.

The full System Parameter List can be found here

Resources in Systems

in Legion resources were annotated with an attribute macro

Legion Example

1
2
#[system]  
pub fn print_time(#[resource] frame_time: FrameTime) { /* ... */ }

However, like we mention Bevy uses System Parameters to identifiy the types. In our case we can use Res, MutRes, Option<Res<>>, and Option<ResMut<<>>

Bevy Example

1
pub fn print_time(frame_time: Res<FrameTime>) { /* ... */ }  

The Res container does rely on the deref trait to have easy access to wrapped resource, so one downside of this pattern is you cannot do destructuring in Legion. If you enjoy feeling clever, like me, by destructing only the parts you need from a Struct in a function parameter, this will obviously be one downside of Bevy for you. When converting from Legion to Bevy ECS I would suggest doing a refactor where you remove any Parameter destructing for you systems.

1
2
// This is a a Bad time, and doesn't work  
pub fn print_time(FrameTime { time, .. }: Res<FrameTime>) { /* ... */ }  

Queries in Systems

Queries for Bevy remarkably close to Legion queries, however you will notice they are in the System Parameter List which means you get to skip the two different steps from the legion example.

Legion use proc macros to identify which components to load into the sub world. Bevy derives similar behavior from the descriptors of the Query<> params

Legion

1
2
3
4
5
6
7
8
#[system]  
#[write_component(Position)]  
#[write_component(Player)]  
#[read_component(Velocity)]  
pub fn move_position(ecs: &mut SubWorld) {   
    let mut querty = <(Entity, &Velocity, &mut Position)>::query().filter(component::<Player>())  
    /* ... */
}  

I did come across this in the Game Engine examples, I had hoped that I could use that for a more 1:1 refactor. After some code spelunking, there was no obvious way to do that with just bevy_ecs crate.

I feel this also breaks the spirit of refactoring to Bevy exercise for me.

Instead, with Bevy ECS the queries go in the parameter. The Legion example becomes something much simpler.

Bevy ECS

1
2
3
pub fn move_position(mut query: Query<(Entity, &Velocity, &mut Position), With<Player>>) {  
    /* ... */
}  

Personally, I like this better, and overall the Bevy ECS.

Commands in Systems

The Legion command pattern in systems was interesting. It was relatively easy to insert into the Entity-Component system, but for anything bigger you had to use an exec_mut. With Bevy ECS no callbacks are found.

Spawn new Entity

Both examples are straight forward

Legion

1
2
3
4
#[system]
pub fn spawn_player(commands: &mut CommandBuffer) {
	commands.push(( Player, Health { current: 20, max: 20 }));  
}

Bevy ECS

1
2
3
pub fn spawn_player(mut commands: Commands) {
	commands.spawn().insert_bundle(( Player, Health { current: 20, max: 20 }));  
}

Insert Component into existing Entity

In both cases, the interface for inserting components are different, but also straight forward.

Legion

1
2
3
pub fn spawn_player(commands: &mut CommandBuffer) {
    commands.add_component(entity, Health { current: 20, max: 20 });
}

Bevy ECS

1
2
3
4
pub fn spawn_player(mut commands: Commands) {
    let entity = query.iter().next().unwrap();
    commands.entity().insert(Health { current: 20, max: 20 });
}

Insert a new Resources

Ths is the case where Bevy has a much simpler interface. Callbacks are not too difficult to manage, sometimes there can trouble with lifecycles.

Legion

1
2
3
4
5
pub fn insert_resource(commands: &mut CommandBuffer) {
    commands.exec_mut(move |_, resources| {
        resources.insert(mouse);
    });
}

Bevy ECS

1
2
3
pub fn insert_resource(commands: &mut CommandBuffer) {
    commands.insert_resource(mouse);
}

Commands Outside of Systems

If you’re using the whole Bevy Engine you don’t really need to think about the Commands outside the context of systems. However, when you are migrating from macroquad / legion you might have spawned entities directly. However, if you are like me and planned for needing to spawn the entities inside the Scheduler at a later point you made your spawn functions take the CommandBuffer and not world.

Legion

1
2
3
let cmd = &mut CommandBuffer::new(&self.ecs)
spawners::player(cmd);
cmd.flush(&mut self.ecs, &mut self.resources);

Bevy ECS

1
2
3
4
5
let command_queue = &mut CommandQueue::default();
let cmd = &mut Commands::new(command_queue, &self.world);
spawners::player(cmd);

command_queue.apply(&mut self.world);

Closing Thoughts

My biggest hope that that Bevy ECS would give me a significant improvement on compile times. However, I will admit I have not noticed the improvement I desired.

The Scheduler for Bevy is significantly more powerful than Legion’s. I have barely scratched the surface, but I’m excited to get yak shave that for a minute.

It appears that Bevy has become the most popular game framework, and even as I’ve added a few more systems to my own game. There are so many great examples of doing awesome work in Bevy which has been helpful for me to learn how to work with the Bevy ECS system. Such as flock-rs, Pyrite Box, and the lovely Country Slice.


  1. Iter Combinations, Advance System Ordering, Events ↩︎

  2. Bevy Labels Lightly goes over some of this, but mostly from the Label metaprogramming aspect. ↩︎