I’ve been playing around with Bevy recently, and decided to make a snake clone. The final product looks like this:

It’s about 300 lines of Rust, so buckle in; this is a lengthy walkthrough. If you want to fast-forward to the finished code, the code lives here. For each section there’s a diff at the top, which should make it easier to determine where to place snippets of code, when it’s not clear.

An empty bevy app

See the diff for this section

We’ll start how the bevy book starts, with an app that does nothing. Run cargo new bevy-snake, then put the following in your main.rs:

use bevy::prelude::*;

fn main() {
    App::build().run();
}

We’ll need to add bevy as a dependency in our Cargo.toml, and because I have the ability to predict the future of this tutorial, let’s go ahead and add rand in there too so we have it when the time comes.

// ...

[dependencies]
bevy = "0.2.1"
rand = "0.7.3"

Creating a window

See the diff for this section

We’re going to be creating a 2D game, which requires a lot of different systems; one to create a window, one to do a render loop, one to handle input, one to handle sprites, etc. Luckily Bevy’s default plugins gives us all of that:

fn main() {
    App::build()
        .add_default_plugins()
        .run();
}

Bevy’s default plugins don’t include a camera though, so let’s insert a 2d camera, which we’ll set up by creating our first system:

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dComponents::default());
}

Commands is used to queue up commands to mutate the world and resources. In this case, we’re spawning a new entity, with 2D camera components. Prepare for some bevy magic:

App::build()
    .add_startup_system(setup.system()) // <--
    .add_default_plugins()
    .run();

All we have to do is call .system() on our function, and Bevy will auto-magically call it at startup with the commands param. Run the app again, and you should get an empty window like this:

The beginnings of a snake

See the diff for this section

Let’s try to get a snake head in there. We’ll define a couple structs:

struct SnakeHead;
struct HeadMaterial(Handle<ColorMaterial>);

SnakeHead is just an empty struct that we’ll use as a component, it’s sort of like a tag, we’ll put on an entity, and then we can find that entity later by querying for entities with the SnakeHead component. Empty structs like these are a common pattern in Bevy, components often don’t need any state of their own. HeadMaterial is going to be a resource, which stores the material that we’ll use for the snake head sprite.

That HeadMaterial handle should be created when the game gets set up, so let’s do that next, by modifying our setup function:

fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    commands.spawn(Camera2dComponents::default());
    commands.insert_resource(HeadMaterial(
        materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
    ));
}
Bevy requires a specific ordering to the params when registering systems. Commands → Resources → Components/Queries. If you get a mysterious compile-time error after messing with a system, check your order.

materials.add will return a Handle<ColorMaterial>. We create a HeadMaterial, with that handle as the only member. Later, when we try to access a resource with the type of HeadMaterial, Bevy will find this struct we’ve created. We’ll now create our snake head entity, and you can see what using that resource looks like:

fn game_setup(mut commands: Commands, head_material: Res<HeadMaterial>) {
    commands
        .spawn(SpriteComponents {
            material: head_material.0,
            sprite: Sprite::new(Vec2::new(10.0, 10.0)),
            ..Default::default()
        })
        .with(SnakeHead);
}

Here we’ve got a new system, which will look for a resource of type HeadMaterial. It will then spawn a new entity, with SpriteComponents and SnakeHead as components. To create the SpriteComponents, we pass in the handle to the color material we created earlier, and give the sprite a size of (10,10). Let’s just add that system into our app builder:

        .add_startup_system(setup.system())
        .add_startup_stage("game_setup") // <--
        .add_startup_system_to_stage("game_setup", game_setup.system()) // <--

The reason we need a new stage instead of just calling add_startup_system again, is that we need to use the material that gets inserted in the setup function. After running this, you should see a snake head in the middle of the screen:

Okay maybe calling it a snake head is a stretch, you’ll see a 10x10 white sprite.

Moving the snake

See the diff for this section

Snake isn’t much of a game without movement, so let’s get that head moving. We’ll worry about input later, for now our goal is just to get the head to move. So let’s create a system that will move all snake heads up:

fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) {
    for (_head, mut transform) in &mut head_positions.iter() {
        *transform.translation_mut().y_mut() += 10.;
    }
}

The main new concept here is that Query type. We can use it to iterate through all entities that have both the SnakeHead component and the Transform component. We don’t have to worry about actually creating that query, bevy will take care of creating it and calling our function with it, part of the ECS magic. So let’s add that system in and see what happens:

.add_startup_system_to_stage("game_setup", game_setup.system())
.add_system(snake_movement.system()) // <--
.add_default_plugins()

Here’s what we get, a snake head moving off-screen:

You may be wondering about that Transform component. When we spawned the SnakeHead, we didn’t give it a Transform, so how come we’re able to find an entity that has a SnakeHead and a Transform component? What’s going on there is that SpriteComponents is a Bundle of components. For SpriteComponents, that means we get a Transform component, among a bunch of others (Sprite, Mesh, Draw, Rotation, Scale, etc).

Controlling the snake

See the diff for this section

Let’s modify our snake movement system to actually allow us to direct the snake:

fn snake_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut head_positions: Query<(&SnakeHead, &mut Transform)>,
) {
    for (_head, mut transform) in &mut head_positions.iter() {
        if keyboard_input.pressed(KeyCode::Left) {
            *transform.translation_mut().x_mut() -= 10.;
        }
        if keyboard_input.pressed(KeyCode::Right) {
            *transform.translation_mut().x_mut() += 10.;
        }
        if keyboard_input.pressed(KeyCode::Down) {
            *transform.translation_mut().y_mut() -= 10.;
        }
        if keyboard_input.pressed(KeyCode::Up) {
            *transform.translation_mut().y_mut() += 10.;
        }
    }
}

Now we can control the snake, albeit in very un-snake-like fashion:

Slapping a grid on it

See the diff for this section

So far we’ve been using window coordinates, and the way that works is that (0,0) is the middle, and the units are pixels. Snake games generally use grids, so if we made our snake game 10x10, our window would be really small. Let’s make our lives a bit easier by using our own positioning and sizing. Then we can use systems that deal with transforming these to window coordinates.

We’ll make our grid 40x40. We’ll define those as constants up at the top of the file:

const ARENA_WIDTH: u32 = 40;
const ARENA_HEIGHT: u32 = 40;

Here are our new structs for positioning/sizing:

#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
struct Position {
    x: i32,
    y: i32,
}

struct Size {
    width: f32,
    height: f32,
}
impl Size {
    pub fn square(x: f32) -> Self {
        Self {
            width: x,
            height: x,
        }
    }
}

Fairly straightforward, with a helper method to get a Size with equal width and height. Position derives some traits that will be useful later, so we don’t have to keep going back to it. Size could really just have one float, since all objects will end up having equal width and height, but it feels wrong so I’m giving it a width and a height. Let’s add those components to the snake head we spawn:

    commands
        .spawn(SpriteComponents {
            material: head_material.0,
            sprite: Sprite::new(Vec2::new(10.0, 10.0)),
            ..Default::default()
        })
        .with(SnakeHead)
        .with(Position { x: 10, y: 10 }) <--
        .with(Size::square(0.8)); <--

These components aren’t doing anything currently, let’s start with transforming our sizes into sprite sizes:

fn size_scaling(windows: Res<Windows>, mut q: Query<(&Size, &mut Sprite)>) {
    for (size, mut sprite) in &mut q.iter() {
        let window = windows.get_primary().unwrap();
        sprite.size = Vec2::new(
            size.width as f32 / ARENA_WIDTH as f32 * window.width as f32,
            size.height as f32 / ARENA_HEIGHT as f32 * window.height as f32,
        );
    }
}

The sizing logic goes like so: if something has a width of 1 in a grid of 40, and the window is 400px across, then it should have a width of 10. Next we can do the positioning system:

fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) {
    fn convert(p: f32, bound_window: f32, bound_game: f32) -> f32 {
        p / bound_game * bound_window - (bound_window / 2.)
    }
    let window = windows.get_primary().unwrap();
    for (pos, mut transform) in &mut q.iter() {
        transform.set_translation(Vec3::new(
            convert(pos.x as f32, window.width as f32, ARENA_WIDTH as f32),
            convert(pos.y as f32, window.height as f32, ARENA_HEIGHT as f32),
            0.0,
        ))
    }
}

The position translation: if an item’s x coordinate is at 5 in our system, the width in our system is 10, and the window width is 200, then the coordinate should be 5 / 10 * 200 - 200 / 2. We subtract half the window width because our coordinate system starts at the bottom left, and Translation starts from the center.

Then we’ll add these systems to the app builder:

.add_system(snake_movement.system())
.add_system(position_translation.system()) <--
.add_system(size_scaling.system()) <--
.add_default_plugins()
.run();

Now when you run it, you should get a squished little snake in the bottom left:

We've broken our input handling. Don't worry about it, we'll fix it in the next section.

Using our grid

See the diff for this section

Now that we’ve got our grid setup, it’s easy to update our snake_movement system. Where we were using Transform before, we use Position, the full function looks like this:

fn snake_movement(
    keyboard_input: Res<Input<KeyCode>>,
    mut head_positions: Query<(&SnakeHead, &mut Position)>,
) {
    for (_head, mut pos) in &mut head_positions.iter() {
        if keyboard_input.pressed(KeyCode::Left) {
            pos.x -= 1;
        }
        if keyboard_input.pressed(KeyCode::Right) {
            pos.x += 1;
        }
        if keyboard_input.pressed(KeyCode::Down) {
            pos.y -= 1;
        }
        if keyboard_input.pressed(KeyCode::Up) {
            pos.y += 1;
        }
    }
}

Now our squished snake will follow the grid we’ve created:

Resizing the window

See the diff for this section

The reason you get a squished snake in the previous step is because the default window size isn’t square but our grid is, so each coordinate in our grid is wider than it is tall. There’s a simple fix for that, and that’s creating a WindowDescriptor resource when we build the app:

    App::build()
        .add_resource(WindowDescriptor { // <--
            title: "Snake!".to_string(), // <--
            width: 2000,                 // <--
            height: 2000,                // <--
            ..Default::default()         // <--
        })
        .add_startup_system(setup.system())

While we’re at it, let’s change the clear color (aka background color), just to make it look a bit nicer, insert this use statement to get the ClearColor component:

use bevy::render::pass::ClearColor;

Then add it to the app builder as a resource:

.add_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))

Now we’re back to having a square, and now with a darker background:

More snake-like movement

See the diff for this section

We’re going to tackle the snake movement. Specifically, we want the snake to move regardless of whether we’re currently pressing down any keys, and we want it to move every X seconds, not every frame. We’ll be making changes in quite a few areas, so if you’re not sure where something goes, check out the diff button above.

The first thing we need to add is a direction enum:

#[derive(PartialEq, Copy, Clone, Debug)]
enum Direction {
    Left,
    Up,
    Right,
    Down,
}

impl Direction {
    fn opposite(self: &Self) -> Self {
        match self {
            Self::Left => Self::Right,
            Self::Right => Self::Left,
            Self::Up => Self::Down,
            Self::Down => Self::Up,
        }
    }
}

We’ll add this direction to our SnakeHead struct, so it knows which way it’s going:

struct SnakeHead {
    direction: Direction,
}

We’ll have to instantiate the SnakeHead component with a direction now, let’s say it starts going up:

.with(SnakeHead { direction: Direction::Up })

Here’s how our new snake_movement system will look:

fn snake_movement(
    time: Res<Time>,
    keyboard_input: Res<Input<KeyCode>>,
    mut snake_timer: ResMut<SnakeMoveTimer>,
    mut head_positions: Query<(&mut SnakeHead, &mut Position)>,
) {
    snake_timer.0.tick(time.delta_seconds);
    for (mut head, mut head_pos) in &mut head_positions.iter() {
        let mut dir: Direction = head.direction;
        if keyboard_input.pressed(KeyCode::Left) {
            dir = Direction::Left;
        }
        if keyboard_input.pressed(KeyCode::Down) {
            dir = Direction::Down;
        }
        if keyboard_input.pressed(KeyCode::Up) {
            dir = Direction::Up;
        }
        if keyboard_input.pressed(KeyCode::Right) {
            dir = Direction::Right;
        }
        if dir != head.direction.opposite() {
            head.direction = dir;
        }
        if snake_timer.0.finished {
            match &head.direction {
                Direction::Left => {
                    head_pos.x -= 1;
                }
                Direction::Right => {
                    head_pos.x += 1;
                }
                Direction::Up => {
                    head_pos.y += 1;
                }
                Direction::Down => {
                    head_pos.y -= 1;
                }
            };
        }
    }
}

There’s a few new things going on. The first is that we’ve now got two time-related resources. The time resource will tell us how much time has passed since the last frame. The other is a repeating timer, we’ll keep adding to it on every frame then check whether it has finished, so we get more step-like movement.

We referenced a few things in that function that don’t actually exist yet, so let’s create them. The SnakeMoveTimer resource doesn’t exist yet, so let’s create a wrapper struct for that:

struct SnakeMoveTimer(Timer);

Which we’ll need to insert into our app builder:

.add_resource(SnakeMoveTimer(Timer::new(
    Duration::from_millis((150.) as u64),
    true,
)))

Duration is from std::time, so go ahead and use it:

use std::time::Duration;

After this you’ll have a snake head that moves a bit more… snake-like:

Adding a tail

See the diff for this section

The tail of the snake is somewhat complex. For each segment, we need to know where it needs to go next. The way we’re going to approach this is to make the snake segments a linked-list, with each segment storing a reference to the next segment. That way, when we’re updating the position of a segment, we can iterate through all the segments and set the position of each to the position of the segment before it.

Let’s create a struct to hold the segment color material:

struct SegmentMaterial(Handle<ColorMaterial>);

A component for our snake segments:

#[derive(Default)]
struct SnakeSegment {
    next_segment: Option<Entity>,
}

Add a next_segment field to the SnakeHead component:

struct SnakeHead {
    direction: Direction,
    next_segment: Entity, // <--
}

In the setup function we’ll need to insert the SegmentMaterial resource. Nothing new here, just the same stuff as the HeadMaterial resource:

commands.insert_resource(SegmentMaterial(
    materials.add(Color::rgb(0.3, 0.3, 0.3).into()),
));

Since we’re going to be spawning segments from a couple places (when you eat food and when you initialize the snake), we’ll create a helper function:

fn spawn_segment(commands: &mut Commands, material: Handle<ColorMaterial>, position: Position) {
    commands
        .spawn(SpriteComponents {
            material,
            ..Default::default()
        })
        .with(SnakeSegment { next_segment: None })
        .with(position)
        .with(Size::square(0.65));
}

This should look very similar to the spawning of the SnakeHead, but instead of a SnakeHead component, it’s got a SnakeSegment component. Now, we’ll need to modify our game setup function. Instead of just a head, it’s also going to spawn… a snake segment (insert shocked pikachu meme):

fn game_setup(
    mut commands: Commands,
    head_material: Res<HeadMaterial>,
    segment_material: Res<SegmentMaterial>,
) {
    spawn_segment(&mut commands, segment_material.0, Position { x: 10, y: 9 });
    let first_segment = commands.current_entity().unwrap();
    commands
        .spawn(SpriteComponents {
            material: head_material.0,
            ..Default::default()
        })
        .with(SnakeHead {
            direction: Direction::Up,
            next_segment: first_segment,
        })
        .with(Position { x: 10, y: 10 })
        .with(Size::square(0.8));
}

We’re using our spawn_segment function to create an entity with a SnakeSegment component. Something new here is that we’re then getting that Entity (which is really just an id), by using the current_entity function, and passing it into the SnakeHead component, so that later we can update that segment’s position to follow the head. At this point you should have a detached little tail:

Making the tail follow the snake

See the diff for this section

One essential part of the game of snake as I remember it is that the head didn’t immediately detach from the tail. Let’s see how we can modify our snake_movement function to be more true to the original game. The first thing we need to do is add a couple new queries to our snake_movement function:

fn snake_movement(
    time: Res<Time>,
    keyboard_input: Res<Input<KeyCode>>,
    mut snake_timer: ResMut<SnakeMoveTimer>,
    mut head_positions: Query<(&mut SnakeHead, &mut Position)>,
    segments: Query<&mut SnakeSegment>, // <--
    positions: Query<&mut Position>, // <--

Directly after the timer snake_timer.0.finished check, we can add the logic to get the segments to follow the head:

if snake_timer.0.finished {
    let mut last_position = *head_pos;
    let mut segment_entity = head.next_segment;
    loop {
        let segment = segments.get::<SnakeSegment>(segment_entity).unwrap();
        let mut segment_position = positions.get_mut::<Position>(segment_entity).unwrap();
        std::mem::swap(&mut last_position, &mut *segment_position)
        if let Some(n) = segment.next_segment {
            segment_entity = n;
        } else {
            break;
        }
    }

We’re putting this before the logic to modify the head’s position, because the segment directly after the head should occupy the head’s current position, not its next position. First we get the position of the head (head_pos), and the first segment entity (head.next_segment), then we need to get the SnakeSegment component and the Position component of that entity (unless we’ve messed something up with the spawning, these should be guaranteed to be components of next_entity, so we unwrap like the maniacs we are). We swap the position of the entity with the last_position, then loop until we just can’t loop no more.

There are more efficient wasy to do this. One way would be to take the last segment and put it where the head is currently, that way we only have 1 move instead of N, where N is the number of segments. This game can probably run on a toaster though, so I’m not worried about trading off some performance for a more intuitive algorithm.

Here’s what we should have now:

Spawning some food

See the diff for this section

Let’s get some food on the screen. You may not be surprised that we’re going to make a new struct for the Food component, a new color material resource, and a new timer:

struct FoodSpawnTimer(Timer);
struct FoodMaterial(Handle<ColorMaterial>);
struct Food;

Like with the other two materials, we’ll need to insert the material as a resource in our setup function:

    commands.insert_resource(FoodMaterial(
        materials.add(Color::rgb(1.0, 0.0, 1.0).into()),
    ));

We’ll create a new system for the food spawning:

fn food_spawner(
    mut commands: Commands,
    food_material: Res<FoodMaterial>,
    time: Res<Time>,
    mut timer: ResMut<FoodSpawnTimer>,
) {
    timer.0.tick(time.delta_seconds);
    if timer.0.finished {
        commands
            .spawn(SpriteComponents {
                material: food_material.0,
                ..Default::default()
            })
            .with(Food)
            .with(Position {
                x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
                y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
            })
            .with(Size::square(0.8));
    }
}

We’re using the random function from the rand crate that we added as a dependency at the start of the project, so add that use statement to the top of the file:

use rand::prelude::random;

Now, in the app builder, we need to add the FoodTimer resource and the food_spawner system:

.add_resource(FoodSpawnTimer(Timer::new(
    Duration::from_millis(1000),
    true,
)))
.add_system(food_spawner.system())

After these are added, you should be able to run the app and get something like this:

Growing the snake

See the diff for this section

As you can see, the snake just passes right under the food. We’re going to add logic to the snake_movement system, to grow when food is eaten. This system is getting a bit overloaded right now, but we’d have to duplicate some logic if we put it in a new system, so we’ll keep it in there for now. We have a few params we’ll need to add:

fn snake_movement(
    mut commands: Commands, // <-- To despawn food
    ...
    mut segment_material: Res<SegmentMaterial>, // <-- To spawn a new segment
    ...
    mut food_positions: Query<(Entity, &Food, &Position)>, // <-- To check when the snake head has collided w/ food
) {

Keep in mind the required order of system params when adding these. Commands -> resources -> components/queries. This query looks a bit different because on top of getting the components, we need to get the underlying Entity. This is because we’re going to be despawning the food, which requires an Entity. We’ll add the logic for eating food and growing right below the logic to adjust the snake head position:

for (ent, _food, food_pos) in &mut food_positions.iter() {
    if food_pos == &*head_pos {
        spawn_segment(&mut commands, segment_material.0, last_position);
        let new_segment = commands.current_entity();
        let mut segment = segments.get_mut::<SnakeSegment>(segment_entity).unwrap();
        segment.next_segment = new_segment;
        commands.despawn(ent);
    }
}

So for each piece of food, we check whether its position is the same as the snake head, and if it is then we spawn a new segment at the position the last snake segment used to be, and despawn the food entity. Here’s what that gives us:

Hitting the wall (or our tail)

See the diff for this section

Let’s make it so that hitting walls and the snake’s tail triggers game over. Although we could do it with just the concepts covered so far, we’re going to introduce the concept of events, partly because our snake_movement system is getting a bit unwieldy. In bevy you can register your structs as events. Systems can then send and receive them. For example, you may have one system that sends jump events, then a separate system that processes them. In our case, we’ll have a system that sends game over events, and a game over system to process them. Let’s create an empty struct that will serve as our event:

struct GameOverEvent;

Our struct is empty but it could have whatever members we wanted. For example, if we wanted to implement a system where we show a tip after game over depending on how the player lost, we could add a reason_for_game_over member to this struct. Now let’s register it as an event in our app builder:

.add_event::<GameOverEvent>()

In our snake_movement system, we want access to the game over event, so we can send events:

fn snake_movement(
    // ...
    mut game_over_events: ResMut<Events<GameOverEvent>>, // <--
    // ...
) {

Let’s just take the case of running into a wall first:

if head_pos.x < 0
    || head_pos.y < 0
    || head_pos.x as u32 > ARENA_WIDTH
    || head_pos.y as u32 > ARENA_HEIGHT
{
    game_over_events.send(GameOverEvent);
}

Alright, so we’ve got our snake_movement system sending game over events. Let’s create a new system that listens for these events:

fn game_over_system(
    mut commands: Commands,
    mut reader: Local<EventReader<GameOverEvent>>,
    game_over_events: Res<Events<GameOverEvent>>,
    segment_material: Res<SegmentMaterial>,
    head_material: Res<HeadMaterial>,
    mut segments: Query<(Entity, &SnakeSegment)>,
    mut food: Query<(Entity, &Food)>,
    mut heads: Query<(Entity, &SnakeHead)>,
) {
    if reader.iter(&game_over_events).next().is_some() {
        for (ent, _segment) in &mut segments.iter() {
            commands.despawn(ent);
        }
        for (ent, _food) in &mut food.iter() {
            commands.despawn(ent);
        }
        for (ent, _head) in &mut heads.iter() {
            commands.despawn(ent);
        }
        spawn_initial_snake(commands, head_material.0, segment_material.0);
    }
}

The Local thing is new. When bevy encounters a system that requires a Local resource, it will use the Default implementation of that type to create a new instance, which will get re-used with each run of that system. It can be more ergonomic than registering resources manually. This lets us iterate through the game over events using our local event reader. If there are any, we’ll despawn all food/segments/heads, and spawn a new snake. Let’s add that system to our app builder:

App::build()
    // ...
    .add_system(game_over_system.system())
    // ...
    .run();

The spawn_initial_snake function that we referenced in game_over_system hasn’t been created yet. Let’s create that:

fn spawn_initial_snake(
    mut commands: Commands,
    head_material: Handle<ColorMaterial>,
    segment_material: Handle<ColorMaterial>,
) {
    spawn_segment(&mut commands, segment_material, Position { x: 10, y: 9 });
    let first_segment = commands.current_entity().unwrap();
    commands
        .spawn(SpriteComponents {
            material: head_material,
            ..Default::default()
        })
        .with(SnakeHead {
            direction: Direction::Up,
            next_segment: first_segment,
        })
        .with(Position { x: 10, y: 10 })
        .with(Size::square(0.8));
}

If you’ve made it this far, you may notice that this looks pretty much identical to the game_setup function. It is pretty much the same code, and we can update the body of game_setup to use this, so we’re not repeating ourselves:

fn game_setup(
    mut commands: Commands,
    head_material: Res<HeadMaterial>,
    segment_material: Res<SegmentMaterial>,
) {
    spawn_initial_snake(commands, head_material.0, segment_material.0);
}

We should now have a snake that will reset itself when it hits the bounds of the window. We’re one step away from a fully-functioning snake game, and that’s to trigger a game over event when the snake hits its own tail, within our loop put it right above the mem::swap call:

loop {
    // ...
    if *head_pos == *segment_position {
        game_over_events.send(GameOverEvent);
    }
    std::mem::swap(...) //
    // ...
}

At last, we have our final result: