[Table of contents]

[First part]

In this part we will shape the basic parts of our game - we will add another tank for an enemy (without a real AI, for now) as well as projectiles, which will have the possibility of impacting and killing our tank or the enemy tank. We will allow ourselves, for now, to have ugly code - the point of this part is how to use nalgebra and ncollide.

Streamlining the Object struct

First thing we should do is streamline the object struct. So far, it has several deficiencies. One such deficiency is that we keep our position and rotation "unwrapped". To fix this, we will use another library, nalgebra:

Cargo.toml [dependencies] nalgebra = "*"

Extern it in main.rs:

main.rs extern crate nalgebra ;

We're going to do the use statement in object.rs

object.rs use nalgebra :: Vec2 as Vector2 ; pub type Vec2 = Vector2 < f64 > ;

We will create a struct to contain our position, rotation and scale

object.rs # [ derive ( Copy , Clone )] pub struct Transform { pos : Vec2 , scale : Vec2 , rot : f64 } # [ allow ( dead_code )] impl Transform { fn new () -> Transform { Transform { pos : Vec2 :: new ( 0.0 , 0.0 ), scale : Vec2 :: new ( 1.0 , 1.0 ), rot : 0.0 } } pub fn mov ( & mut self , v : Vec2 ) { self .pos = self .pos + v ; } pub fn mov_to ( & mut self , v : Vec2 ) { self .pos = v ; } pub fn rot ( & mut self , r : f64 ) { self .rot += r ; } pub fn rot_to ( & mut self , r : f64 ) { self .rot = r ; } pub fn fwd ( & mut self , d : f64 ) { self .pos.x += d * ( - self .rot .sin ()); self .pos.y += d * self .rot .cos (); } }

A container for its combination with a sprite, and its implementation including rendering:

object.rs pub struct Component { trans : Transform , sprite : Option < Texture < Resources >> } impl Component { fn new () -> Component { } fn render ( self : & Component , g : & mut GfxGraphics < Resources , CommandBuffer < Resources > , Output > , view : math :: Matrix2d ) { let t : Transform = self .trans ; match self .sprite { Some ( ref sprite ) => { let ( spritex , spritey ) = sprite .get_size (); let ( ocx , ocy ) = ( spritex / 2 , spritey / 2 ); image ( sprite , view .trans ( t .pos.x , t .pos.y ) .scale ( t .scale.x , t .scale.y ) .rot_rad ( t .rot ) .trans ( - ( ocx as f64 ), - ( ocy as f64 )), g ); } _ => {} } } }

Now let's temporarily update our Object struct:

object.rs pub struct Object { pub hull : Component , pub turret : Component , point_to : Vec2 } # [ allow ( dead_code )] impl Object { pub fn new () -> Object { Object { hull : Component :: new (), turret : Component :: new (), point_to : Vec2 :: new ( 0.0 , 0.0 ) } } pub fn mov ( & mut self , pos : Vec2 ) { self .hull.trans .mov ( pos ); } pub fn mov_to ( & mut self , pos : Vec2 ) { self .hull.trans .mov_to ( pos ); } pub fn rot ( & mut self , r : f64 ) { self .hull.trans .rot ( r ); self .turret.trans .rot ( r ); } pub fn rot_to ( & mut self , r : f64 ) { self .turret.trans .rot ( r - self .hull.trans.rot ); self .hull.trans .rot_to ( r ); } pub fn fwd ( & mut self , d : f64 ) { self .hull.trans .fwd ( d ); self .turret.trans.pos = self .hull.trans.pos ; } pub fn point_tur_to ( & mut self , x : f64 , y : f64 ) { self .point_to = Vec2 :: new ( x , y ); } pub fn calc_tur_pos ( & mut self , dt : f64 ) { let mut new_rot = ( - ( self .point_to.x - self .hull.trans.pos.x )) .atan2 ( self .point_to.y - self .hull.trans.pos.y ); if new_rot == self .turret.trans.rot { return ; } if new_rot < self .turret.trans.rot && self .turret.trans.rot - new_rot > new_rot + 2.0 * PI - self .turret.trans.rot { new_rot += 2.0 * PI ; } if new_rot > self .turret.trans.rot && new_rot - self .turret.trans.rot > self .turret.trans.rot + 2.0 * PI - new_rot { new_rot -= 2.0 * PI ; } let rot_speed = 1.0 ; if new_rot > self .turret.trans.rot { if new_rot - self .turret.trans.rot > rot_speed * dt { self .turret.trans.rot += rot_speed * dt ; } else { self .turret.trans.rot = new_rot ; } } else { if self .turret.trans.rot - new_rot > rot_speed * dt { self .turret.trans.rot -= rot_speed * dt ; } else { self .turret.trans.rot = new_rot ; } } if self .turret.trans.rot > 2.0 * PI { self .turret.trans.rot -= 2.0 * PI ; } if self .turret.trans.rot < 0.0 { self .turret.trans.rot += 2.0 * PI ; } } pub fn update ( & mut self , dt : f64 ) { self .turret.trans.pos = self .hull.trans.pos ; self .calc_tur_pos ( dt ); } pub fn render ( & self , g : & mut GfxGraphics < Resources , CommandBuffer < Resources > , Output > , view : math :: Matrix2d ) { self .hull .render ( g , view ); self .turret .render ( g , view ); } }

I honestly feel dirty having written that code; the issue is, we have to make hull and turret public because at the moment we have to set the sprite from our on_load function:

impl Game::on_load self .player.hull .set_sprite ( tank_sprite ); self .player.turret .set_sprite ( tank_turret );

Our changes in Object::rot have also fixed turret rotation to be relative to the body, rather than to the world.

Phew.

Take a breath, we're not done.

Next library we need (unless we want to complicate our code beyond the scope of this tutorial) - ncollide

Cargo.toml ncollide = "*"

main.rs extern crate ncollide ;

And let's do some use s:

object.rs use nalgebra :: Vec1 as Vector1 ; use nalgebra :: Vec2 as Vector2 ; use nalgebra :: Rot2 as Rotate2 ; use nalgebra :: Pnt2 as Point2 ; use ncollide :: point :: PointQuery ; use ncollide :: shape :: Cuboid2 ; pub type Vec1 = Vector1 < f64 > ; pub type Vec2 = Vector2 < f64 > ; pub type Rot2 = Rotate2 < f64 > ; pub type Pnt2 = Point2 < f64 > ; pub type Cuboid2f = Cuboid2 < f64 > ;

Now, let's start making the actual gameplay. First, we will create an Object trait:

object.rs pub trait Object { pub fn mov ( & mut self , pos : Vec2 ); pub fn mov_to ( & mut self , pos : Vec2 ); pub fn rot ( & mut self , r : f64 ); pub fn rot_to ( & mut self , r : f64 ); pub fn fwd ( & mut self , d : f64 ); pub fn update ( & mut self , dt : f64 ); pub fn render ( & self , g : & mut GfxGraphics < Resources , CommandBuffer < Resources > , Output > , view : math :: Matrix2d ); }

We're going to rename our old Object struct to Tank, and do the changes required; the above functions will go into impl Object for Tank , while the rest will go into impl Tank . Additionally, we will add collision information to the Tank struct, as well as information for whether it is destroyed or not:

struct Tank collider : Cuboid2 , pub is_destroyed : bool ,

impl Tank pub fn new () -> Tank { Tank { hull : Component :: new (), turret : Component :: new (), collider : Cuboid2f :: new ( Vec2 :: new ( 38.0 , 65.0 )), point_to : Vec2 :: new ( 0.0 , 0.0 ), is_destroyed : false } }

We will also create a new struct called Bullet :

object.rs pub struct Bullet { pub bullet : Component , pub to_be_removed : bool } # [ allow ( dead_code )] impl Object for Bullet { fn mov ( & mut self , pos : Vec2 ) { self .bullet.trans .mov ( pos ); } fn mov_to ( & mut self , pos : Vec2 ) { self .bullet.trans .mov_to ( pos ); } fn rot ( & mut self , r : f64 ) { self .bullet.trans .rot ( r ); } fn rot_to ( & mut self , r : f64 ) { self .bullet.trans .rot_to ( r ); } fn fwd ( & mut self , d : f64 ) { self .bullet.trans .fwd ( d ); } fn update ( & mut self , dt : f64 ) { let bullet_speed = 200.0 ; self .fwd ( bullet_speed * dt ); } fn render ( & self , g : & mut GfxGraphics < Resources , CommandBuffer < Resources > , Output > , view : math :: Matrix2d ) { self .bullet .render ( g , view ); } }

In the Tank impl, we will also add a function for checking collision with a bullet:

pub fn collides ( & mut self , b : & Bullet ) -> bool { let bpnt = Pnt2 :: new ( b .bullet.trans.pos.x , b .bullet.trans.pos.y ); self .collider .contains_point ( & self .hull.trans.pos , & bpnt ) }

Additionally, we will add a function for firing a bullet in front of our cannon:

pub fn fire ( & self , sprite : Texture < Resources > ) -> Bullet { let mut bul = Bullet { bullet : Component :: new (), to_be_removed : false }; bul .mov_to ( self .turret.trans.pos ); bul .rot_to ( self .turret.trans.rot ); bul .fwd ( 125.0 ); bul .bullet .set_sprite ( sprite ); bul }

Now, let's move to main.rs :

main.rs use object :: Bullet ;

We will add another player (we will call him player2); for now we won't control this player, and leave him somewhere to the right of player1. Additionally, we will add a vector of bullets:

struct Game player1 : Tank , player2 : Tank , bullets : Vec < Bullet > ,

impl Game fn new () -> Game { Game { player1 : Tank :: new (), player2 : Tank :: new (), bullets : Vec :: < Bullet > :: new (), up_d : false , down_d : false , left_d : false , right_d : false , scx : 300.0 , scy : 300.0 } }

Of course, also load set the same sprite for player2, and change all uses of player to player1. Additionally, let us position player2 to the right:

Game::on_load() self .player2 .mov_to ( Vec2 :: new ( 300.0 , 0.0 ));

We've also created two sprites for the hull and turret, when they are destroyed. We load them and store them for easy cloning. For more info see the finished code for this part. Additionally, we have added a simple sprite for a bullet.

Let us move to the on_update function. We will check to see if any bullet touches a tank, and if so set its is_destroyed bool to true and its sprites to the ones we have created:

on_update for bul in & mut self .bullets { if self .player1 .collides ( & bul ) { self .player1.is_destroyed = true ; self .player1.hull .set_sprite ( self .hull_destroyed .clone () .unwrap ()); self .player1.turret .set_sprite ( self .turret_destroyed .clone () .unwrap ()); bul .to_be_removed = true ; } if self .player2 .collides ( & bul ) { self .player2.is_destroyed = true ; self .player2.hull .set_sprite ( self .hull_destroyed .clone () .unwrap ()); self .player2.turret .set_sprite ( self .turret_destroyed .clone () .unwrap ()); bul .to_be_removed = true ; } } self .bullets .retain (| ref bul | bul .to_be_removed == false );

Aaaaaaaaaaaaaaand.. FIRE!

Game::on_input match inp { //(...) Input :: Release ( but ) => { match but { //(...) Button :: Mouse ( MouseButton :: Left ) => { self .player1 .fire ( self .bullet .clone () .unwrap ()); } //(...) } } //(...) }

We'll also add rendering:

Game::on_draw for bul in & self .bullets { bul .render ( g , center ); }

Result:

http://i.imgur.com/DRe446D.gifv

Code available here