Designing the states

Now that we've examined all the concepts involved, let's design the Bevy's states.

We'll use the following code as reference, which is the source project as of this step:

// source: mod.rs

pub fn build_input_scheduler() -> Schedule {
    Schedule::builder()
        .add_system(player_input::player_input_system())
        .flush()
        .add_system(map_render::map_render_system())
        .add_system(entity_render::entity_render_system())
        .build()
}

pub fn build_player_scheduler() -> Schedule {
    Schedule::builder()
        .add_system(collisions::collisions_system())
        .flush()
        .add_system(map_render::map_render_system())
        .add_system(entity_render::entity_render_system())
        .add_system(end_turn::end_turn_system())
        .build()
}

pub fn build_monster_scheduler() -> Schedule {
    Schedule::builder()
        .add_system(random_move::random_move_system())
        .flush()
        .add_system(collisions::collisions_system())
        .flush()
        .add_system(map_render::map_render_system())
        .add_system(entity_render::entity_render_system())
        .add_system(end_turn::end_turn_system())
        .build()
}

A first, direct translation

In a direct transation, we create one stage for each transaction:

stage descriptionsource scheduler numberstage count
get player input11
render player input12
handle player collisions23
render player collisions24
move monsters35
handle monster collisions36
render move monsters37

All good! This can work, and it's a very straight model. Something important to keep in mind is that, in the game design, if the player doesn't press any key, the subsequent stages won't be executed!

Simplifying the source project states

Now, let's look at the rendering systems: map_render::map_render_system() and entity_render::entity_render_system(); we notice two things:

  1. they are common to all the schedulers;
  2. they need to be run in their separate transaction, otherwise, they may render a partial state (this would be the equivalent of the "read uncommitted" transaction level in database systems).

However, if we consider that this all happens over a single frame, we now see an opportunity for simplification: we just need to render only once! Let's have a look at the new table:

stage descriptionsource scheduler numberstage count
get player input11
handle player collisions22
move monsters33
handle monster collisions34
render45

We need a small modification here 😉 If the player doesn't send any input, the rendering won't be executed. Fortunately, we can shift the render:

stage descriptionsource scheduler numberstage count
render41
get player input12
handle player collisions23
move monsters34
handle monster collisions35

Excellent. From the user perspective, if we render at the beginning of a frame, or at the end of the previous one, it makes no difference - except that, with the former approach, we've solved the rendering problem.

We can go further! we don't strictly need to sequentially render and get the player input in a sequence; this is a slow game, and rendering a tiny fraction of second after won't make any difference; the new plan is therefore:

stage descriptionsource scheduler numberstage count
render+get player input4+11
handle player collisions22
move monsters33
handle monster collisions34

Notice that when we talk about a tiny fraction of second, it not one frame of delay; the input/rendering systems are rendered in parallel (as opposed to in a sequence), so there is virtually zero delay.

Summary of the design

With the design above, we now have 4 stages:

  • render+get player input
  • handle player collisions
  • move monsters
  • handle monster collisions

We'll also keep the 3 game states of the source project:

  • awaiting input
  • player turn
  • monster turn

Remember: states are used to represent high-level game states, while stages are subdivisions of the states, for transactional purposes.