Introduction

Hi, during one year now I tried to make a game in Rust. From this attempt a game, Ruga, as emerged and though I'm quite happy with the architecture, the game is aborted because I don't like the gameplay and I don't want to create some maps. However I learned a lot for the next game.

First and second Architecture

After a few weeks fighting with the compiler I was able to use the language. Because Rust doesn't have inheritance I used one struct for units with some fields that are enum that specialize the behavior of the unit.

The result wasn't very handy as there was lot of match expression, I finally moved to a second architecture with traits.

So every units now implement a unique trait. when there was interaction between units it only have access to the trait. it results in big trait BodyTrait and I used a macro delegate to encapsulate things.

in the following example I implement BodyTrait for Boid. Almost all methods are directly implemented by a field of Boid named body and the last 3 ones are implemented by hand.

impl BodyTrait for Boid {
    delegate!{
        body:
            id() -> usize,
            body_type() -> BodyType,
            width() -> f64,
            height() -> f64,
            x() -> f64,
            mut set_x(x: f64) -> (),
            y() -> f64,
            mut set_y(y: f64) -> (),
            weight() -> f64,
            velocity() -> f64,
            mut set_velocity(v: f64) -> (),
            angle() -> f64,
            mut set_angle(a: f64) -> (),
            mask() -> u32,
            group() -> u32,
            collision_behavior() -> CollisionBehavior,
    }

    fn dead(&self) -> bool {
        self.life <= 0.0
    }

    fn on_collision(&mut self, other: &mut BodyTrait) {
        if other.body_type() != BodyType::Boid {
            other.damage(DAMAGE);
        }
    }

    fn damage(&mut self, damage: f64) {
        self.life -= damage;
    }
}

The macro come from Maniagnosis post

This design is actually very inspired by inheritance pattern and it doesn't fit in rust well. As the architecture grows this is way too much verbose.

Final architecture

Then I heard of Entity/Component/System (ECS) architecture through a rust game engine project called Amethyst.

I use specs crate for ECS.

  • there is components: PhysicalType (Shape and mask), PhysicalState (position, velocity and acceleration), Life, Behavior ...

  • there is entity: a set of components,

pub fn add_monster(world: &mut specs::World, pos: [isize;2]) {
//    create a new entity in the world
//    vvvvvvvvvv
world.create_now()
//      Component
//      vvvvvvvvvvv
.with::<PhysicState>(PhysicState::new(pos))
.with::<PhysicDynamic>(PhysicDynamic)         // flag for the physic engine
.with::<PhysicType>(PhysicType::new_movable(  // basic physic properties
        config.entities.monster_group.val,
        config.entities.monster_mask.val,
        Shape::Circle(config.entities.monster_radius),
        CollisionBehavior::Persist,
        config.entities.monster_velocity,
        config.entities.monster_time,
        config.entities.monster_weight))
.with::<PhysicForce>(PhysicForce::new())       // force that pull the monster
.with::<Life>(Life::new(config.entities.monster_die_snd)) // life of the monster
.with::<Graphic>(Graphic::new(                 // graphic properties
        config.entities.monster_color,
        config.entities.monster_layer))
.with::<MonsterControl>(MonsterControl::new()) // kind of behavior of the monster
.with::<Killer>(Killer {                       // another kind of behavior
    kamikaze: true,
    mask: config.entities.monster_killer_mask.val,
    kill_snd: config.entities.monster_kill_snd,
})
.with::<DynPersistentSnd>(DynPersistentSnd::new( // sound of the monster
        config.entities.monster_persistent_snd))
.build();
}
  • there is system: iterates over components and process them.
pub struct KillerSystem;
impl specs::System<app::UpdateContext> for KillerSystem {
  fn run(&mut self, arg: specs::RunArg, _context: app::UpdateContext) {
    // list of components that the system use
    let (mut lives, states, types, physic_world, killers, entities) = arg.fetch(|world| {
      (
        world.write::<Life>(),
        world.read::<PhysicState>(),
        world.read::<PhysicType>(),
        world.read_resource::<PhysicWorld>(),
        world.read::<Killer>(),
        world.entities(),
      )
    });

    // iterate over the entities that have all those components
    for (killer, state, typ, entity) in (&killers, &states, &types, &entities).iter() {
      let mut kill = false;
      physic_world.apply_on_shape(&state.position, killer.mask, &typ.shape, &mut |other_entity,_| {
        // kill all other entities that have life components in an area
        if let Some(life) = lives.get_mut(*other_entity) {
          // audio not part of ECS, just accessed through a mutex
          baal::effect::short::play(killer.kill_snd,state.position.into_3d());
          life.kill();
          kill = true;
        }
      });
      // kill itself if kamikze
      if kill && killer.kamikaze {
        lives.get_mut(entity).expect("killer kamikaze expect life component").kill();
      }
    }
  }
}

Compilation time

When the architecture was OK, I started to add complexity to the game. The compile time was rapidly consequent, in debug mode it was ~10 seconds at the time and it is ~30 seconds now (I don't have a very powerful computer).

It was very problematic when adjusting gameplay constant so I used configuration file in toml. I used lazy_static so constant are accessed as standard constant but the first access actually read the configuration file and allocate the structure.

Cross-compilation

I didn't work on the gameplay so the last thing I had to do was deployement. I wanted to support as many platform as possible and I wanted to compile everything from linux.

I had issues for cross-compiling portaudio from linux to windows so I deciding to use only rust dependencies. I used rusttype instead of freetype and rodio instead of portaudio.

I had some difficulties to use Rodio because I didn't find any example that correspond to my usecase (interactive sounds). However I implemented it in a crate named baal still very unstable.

Also note that I used glium for opengl and gilrs for gamepad inputs which are both cross-platform.

Finally I made binaries for linux and windows.

What's next

As said before this game is over. On the next game :

  • all configuration will be made in lua and a console will be accessible in game

  • move from self-made physic engine to collider-rs : my needs are very little only axis aligned bounding box and circle, collider-rs is a continuous physic engine that provide those features

  • deploy to android and deploy to the web with emscripten : compile to the web would be very useful but I'm waiting for threads in the web.

  • and most of all new graphics and new gameplay

Some pointers

  • arewegameyet at the time it didn't exist but it is cool,

  • amethyst was very inspiring to me even if I prefer create my own engine which is based on glium (opengl) + rodio (audio) + gilrs (gamepad)

  • games in Piston where I discover lazy_static.