over 3 years ago

[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 uses:

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

 
over 3 years ago

[Table of contents]
[First part]

Today we will do a couple of of things:

  • Make movement sane
  • Make our tank be represented by multiple sprites (turret + hull, independently rotated)
  • Use mouse movement for it

Movement and Turret

We'll add a current rotation value for our object:

struct Object
rot: f64,
impl Object
pub fn rot(&mut self, r: f64) {
    self.rot += r;
}
pub fn rot_to(&mut self, r: f64) {
    self.rot = r;
}

We'll add another function to help us move in the direction we're pointing (or in reverse):

impl Object
    pub fn fwd(&mut self, d: f64) {
    self.x += d * (-self.rot.sin());
    self.y += d * self.rot.cos();
}

Finally, we make sure the center of the sprite is in the proper place, and represent the rotation:

Object::render
            let (spritex, spritey) = sprite.get_size();
            let (ocx, ocy) = (spritex / 2, spritey / 2);
            image(sprite, view.trans(self.x, self.y).rot_rad(self.rot).trans(-(ocx as f64), -(ocy as f64)), g);

Now let's add the turret:

struct Object
tur_x: f64,
tur_y: f64,
rot_tur: f64,
turret: Option<Texture<Resources>>
impl Object
pub fn set_turret_sprite(&mut self, sprite: Texture<Resources>) {
    self.turret = Some(sprite);
}
pub fn point_tur_to(&mut self, x: f64, y: f64) {
    self.tur_x = x;
    self.tur_y = y;
}
pub fn update(&mut self, dt: f64) {
    self.calc_tur_pos(dt);
}
pub fn calc_tur_pos(&mut self, dt: f64) {
    let mut new_rot = (-(self.tur_x - self.x)).atan2(self.tur_y - self.y);
    if new_rot == self.rot_tur {
        return;
    }
    if new_rot < self.rot_tur && self.rot_tur - new_rot > new_rot + 2.0 * PI - self.rot_tur {
        new_rot += 2.0 * PI;
    }
    if new_rot > self.rot_tur && new_rot - self.rot_tur > self.rot_tur + 2.0 * PI - new_rot {
        new_rot -= 2.0 * PI;
    }
    let rot_speed = 1.0;
    if new_rot > self.rot_tur {
        if new_rot - self.rot_tur > rot_speed * dt {
            self.rot_tur += rot_speed * dt;
        } else {
            self.rot_tur = new_rot;
        }
    } else {
        if self.rot_tur - new_rot > rot_speed * dt {
            self.rot_tur -= rot_speed * dt;
        } else {
            self.rot_tur = new_rot;
        }
    }
    if self.rot_tur > 2.0 * PI {
        self.rot_tur -= 2.0 * PI;
    }
    if self.rot_tur < 0.0 {
        self.rot_tur += 2.0 * PI;
    }
}
Object::render
    match self.turret {
        None => {}
        Some(ref sprite) => {
            let (spritex, spritey) = sprite.get_size();
            let (ocx, ocy) = (spritex / 2, spritey / 2);
            image(sprite, view.trans(self.x, self.y).rot_rad(self.rot_tur).trans(-(ocx as f64), -(ocy as f64)), g);
        }
    }

We also need to load a separate sprite for the turret and hull. I split E-100_strip2.png into two images named E-100_Base.png and E-100_Turret.png.

impl Game
fn on_load(&mut self, w: &PistonWindow) {
    let assets = find_folder::Search::ParentsThenKids(3, 3).for_folder("assets").unwrap();
    let tank_sprite = assets.join("E-100_Base.png");
    let tank_sprite = Texture::from_path(
            &mut *w.factory.borrow_mut(),
            &tank_sprite,
            Flip::None,
            &TextureSettings::new())
            .unwrap();
    let tank_turret = assets.join("E-100_Turret.png");
    let tank_turret = Texture::from_path(
            &mut *w.factory.borrow_mut(),
            &tank_turret,
            Flip::None,
            &TextureSettings::new())
            .unwrap();
    self.player.set_sprite(tank_sprite);
    self.player.set_turret_sprite(tank_turret);
}

Mouse movement

I've added a use statement for Pi:

object.rs
use std::f64::consts::PI;

From now on we'll keep the center of our screen:

struct Object
scx: f64, scy: f64
impl Object
fn on_draw(&mut self, ren: RenderArgs, e: PistonWindow) {
    self.scx = (ren.width / 2) as f64;
    self.scy = (ren.height / 2) as f64;
    e.draw_2d(|c, g| {
        clear([0.8, 0.8, 0.8, 1.0], g);
        let center = c.transform.trans(self.scx, self.scy);
        self.player.render(g, center);
    });
}

Now let's catch the mouse movement:

        Input::Move(mot) => {
            match mot {
                Motion::MouseCursor(x, y) => {
                    self.player.point_tur_to(x - self.scx, y - self.scy);
                }
                _ => {}
            }
        }

And call the update function:

    self.player.update(upd.dt);

As one last thing, I've scaled down both the turret and body to about a quarter the size:

Object::render
            image(sprite, view.trans(self.x, self.y).scale(0.75, 0.75).rot_rad(self.rot).trans(-(ocx as f64), -(ocy as f64)), g);
            image(sprite, view.trans(self.x, self.y).scale(0.75, 0.75).rot_rad(self.rot_tur).trans(-(ocx as f64), -(ocy as f64)), g);

End result:

You can find the source code for this part here

 
over 3 years ago

[Table of contents]
[First part]

We're going to jump straight into the action this time around. In our src folder, we're going to make a new file named object.rs, containing a new struct - object - in preparation for having multiple game objects:

object.rs
use piston_window::*;

pub struct Object {
    x: f64,
    y: f64,
}

#[allow(dead_code)]
impl Object {
    pub fn new() -> Object {
        Object {x : 0.0, y : 0.0}
    }
    pub fn mov(&mut self, x: f64, y: f64) {
        self.x += x;
        self.y += y;
    }
    pub fn mov_to(&mut self, x: f64, y: f64) {
        self.x = x;
        self.y = y;
    }
    // Now, we also make our object do the rendering

    pub fn render<G>(&self, g: &mut G, view: math::Matrix2d) where G: Graphics {
        let square = rectangle::square(0.0, 0.0, 100.0);
        let red = [1.0, 0.0, 0.0, 1.0];
        rectangle(red, square, view.trans(self.x, self.y).trans(-50.0, -50.0), g); // We translate the rectangle slightly so that it's centered; otherwise only the top left corner would be centered

    }
}

We include the object module in main.rs:

main.rs:5
mod object;
use object::Object;

We remove the x and y members of Game, as they are now redundant and replace it with an Object named player; we'll also modify our on_update and on_draw functions accordingly:

main.rs:8
struct Game {
    rotation: f64,
    player: Object,
    up_d: bool, down_d: bool, left_d: bool, right_d: bool
}
main.rs:15
fn new() -> Game {
        Game { rotation : 0.0, player : Object::new(), up_d: false, down_d: false, left_d: false, right_d: false }
    }
    fn on_update(&mut self, upd: UpdateArgs) {
        self.rotation += 3.0 * upd.dt;
        if self.up_d {
            self.player.mov(0.0, -150.0 * upd.dt);
        }
        if self.down_d {
            self.player.mov(0.0, 150.0 * upd.dt);
        }
        if self.left_d {
            self.player.mov(-150.0 * upd.dt, 0.0);
        }
        if self.right_d {
            self.player.mov(150.0 * upd.dt, 0.0);
        }
    }
    fn on_draw(&mut self, ren: RenderArgs, e: PistonWindow) {
        e.draw_2d(|c, g| {
            clear([0.0, 0.0, 0.0, 1.0], g);
            let center = c.transform.trans((ren.width / 2) as f64, (ren.height / 2) as f64);
            self.player.render(g, center);
        });
    }

We've also removed the constant rotation.

Now let's get a sprite for our tank. For that purpose, I found this. In our project folder, we will make a directory named assets and put a sprite of our preference (I chose E-100_preview.png) in it.

At this point we need to bring in some new crates; find_folder to find the assets folder, and a couple of gfx related libraries (Warning: with version matching the versions required by piston_window) to allow us to deal with the sprites; I've also updated piston_window to a more recent version:

Cargo.toml:6
[dependencies]
piston_window = "0.25.0"
find_folder = "*"
gfx_device_gl = "0.5.0"
gfx = "0.7.0"
piston2d-gfx_graphics = "0.11.0"

We also need to include them:

main.rs:2
extern crate gfx_device_gl;
extern crate find_folder;
extern crate gfx_graphics;
extern crate gfx;

And in object.rs we will add the following:

object.rs:2
use gfx_device_gl::{Resources, Output, CommandBuffer};
use gfx_graphics::GfxGraphics;

We'll add a sprite attribute to our object struct:

object.rs:8
sprite: Option<Texture<Resources>>
object.rs:13
pub fn new() -> Object {
    Object {x : 0.0, y : 0.0, sprite: None}
}
object.rs:36
pub fn set_sprite(&mut self, sprite: Texture<Resources>) {
    self.sprite = Some(sprite);
}

We can now display the sprite, if it is set; first we have to modify the definition of the render() function slightly:

object.rs:24
    pub fn render(&self, g: &mut GfxGraphics<Resources, CommandBuffer<Resources>, Output>, view: math::Matrix2d)

Now let's display the sprite:

Object::render():3
    match self.sprite {
        None => {
            rectangle(red, square, view.trans(self.x, self.y).trans(-50.0, -50.0), g); // We translate the rectangle slightly so that it's centered; otherwise only the top left corner would be centered

        }
        Some(ref sprite) => {
            image(sprite, view.trans(self.x, self.y).trans(-50.0, -50.0), g);
        }
    }

Finally, let's set that up in main.rs. We'll add an on_load() function to load our sprite:

Game::on_load()
fn on_load(&mut self, w: &PistonWindow) {
    let assets = find_folder::Search::ParentsThenKids(3, 3).for_folder("assets").unwrap();
    let tank_sprite = assets.join("E-100_preview.png");
    let tank_sprite = Texture::from_path(
            &mut *w.factory.borrow_mut(),
            &tank_sprite,
            Flip::None,
            &TextureSettings::new())
            .unwrap();
    self.player.set_sprite(tank_sprite);
}

And we're done! Building and running our program should look like this:

The source code for this part can be found here.

 
over 3 years ago

[Table of contents]
[First part]

Last time we had a simple example - our spinning square - but now we want to go one more step and move it with our keyboard. Before we do that though, we should reorganize our code a little.

First, let's create a struct containing the information of our game:

struct Game {
    rotation: f64
}

Now let's create an implementation for it. We're going to move our game and rendering logic into an on_update function and an on_draw function, with some modifications:

impl Game {
    fn new() -> Game {
        Game { rotation : 0.0 }
    }
    fn on_update(&mut self, upd: UpdateArgs) {
        self.rotation += 3.0 * upd.dt;
    }
    fn on_draw(&mut self, ren: RenderArgs, e: PistonWindow) {
        e.draw_2d(|c, g| {
            clear([0.0, 0.0, 0.0, 1.0], g);
            let center = c.transform.trans((ren.width / 2) as f64, (ren.height / 2) as f64);
            let square = rectangle::square(0.0, 0.0, 100.0);
            let red = [1.0, 0.0, 0.0, 1.0];
            rectangle(red, square, center.rot_rad(self.rotation).trans(-50.0, -50.0), g); // We translate the rectangle slightly so that it's centered; otherwise only the top left corner would be centered

        });
    }
}

We have also added a function to easily create instances of Game, essentially a constructor.

The event loop should now look like this:

let mut game = Game::new();
for e in window {
    match e.event {
        Some(Event::Update(upd)) => {
            game.on_update(upd);
        }
        Some(Event::Render(ren)) => {
            game.on_draw(ren, e);
        }
        _ => {

                }
    }
}

One new thing you will notice is Event::Render. This is an event that piston uses to tell us it is time to draw a new frame; it also allows for better decoupling of the render loop from the event loop, such that we don't bog down keyboard input with rendering. It also gives us some more values to work with, such as the current window width and height - which we have used in the modified on_draw code above. We also now capture the Event::Update(UpdateArgs) member, rather than only the delta time; this is to streamline our event function calls.

The moving the square part

Capturing input is similar to other events in piston:

Some(Event::Input(inp)) => {
    game.on_input(inp);
}

Before we write on_input, we need some boolean values so we can do movement based on dt; we'll also add some variables for the x and y of our square:

x: f64,
y: f64,
up_d: bool, down_d: bool, left_d: bool, right_d: bool

Of course, in Game::new() we have to add default values:

Game { rotation : 0.0, x : 0.0, y : 0.0, up_d: false, down_d: false, left_d: false, right_d: false }

Time to write on_input:

fn on_input(&mut self, inp: Input) {
    match inp {
        Input::Press(but) => {
            match but {
                Button::Keyboard(Key::Up) => {
                    self.up_d = true;
                }
                Button::Keyboard(Key::Down) => {
                    self.down_d = true;
                }
                Button::Keyboard(Key::Left) => {
                    self.left_d = true;
                }
                Button::Keyboard(Key::Right) => {
                    self.right_d = true;
                }
                _ => {}
            }
        }
        Input::Release(but) => {
            match but {
                Button::Keyboard(Key::Up) => {
                    self.up_d = false;
                }
                Button::Keyboard(Key::Down) => {
                    self.down_d = false;
                }
                Button::Keyboard(Key::Left) => {
                    self.left_d = false;
                }
                Button::Keyboard(Key::Right) => {
                    self.right_d = false;
                }
                _ => {}
            }
        }
        _ => {}
    }
}

As you can see, we take the Input event argument and match it through one of the possible events - two of which, the ones we use, being Input::Press and Input:Release, representing a button press or release, respectively. We set our *_d variables accordingly.

The last two things we need to do is add movement logic to the update function:

if self.up_d {
    self.y += (-50.0) * upd.dt;
}
if self.down_d {
    self.y += (50.0) * upd.dt;
}
if self.left_d {
    self.x += (-50.0) * upd.dt;
}
if self.right_d {
    self.x += (50.0) * upd.dt;
}

And change the rendering of the square accordingly:

rectangle(red, square, center.trans(self.x, self.y).rot_rad(self.rotation).trans(-50.0, -50.0), g);

You can find the source code for this part here

Next time we'll load and display a tank sprite instead of a square. For that purpose, we will also start keeping object information in a separate struct, and its code in a separate file.

 
over 3 years ago

[Table of contents]
[First part]

Last time, we set up a workspace for rust and cargo. In this part, we are going to go from nothing to a window with a spinning square in the middle.

Creating the project

In a terminal, type the following command:

cargo new --bin piston-tutorial

This creates the folder we are going to work with, as well as an initial configuration for cargo.

Inside the folder, you will find it has created a file, Cargo.toml and a directory, src. Inside src you will find the main.rs file, with the basic rust "Hello World" program inside it.

Cargo.toml is the configuration file of our project. Open it up - you will find something similar to the following inside:

[package]
name = "piston-tutorial"
version = "0.1.0"
authors = ["StelarCF <StelarCF@gmail.com>"]

As you can tell, this section describes the basic infromation of our package - its name, the version it is on and who wrote it (the authors part will be different for you, of course).

Add the following lines at the end of the file:

[dependencies]
piston_window = "0.24.0"

This tells Cargo that our project will depend on piston_window, a package which serves as a convenience package for a couple of other packages for windowing, input and graphics.

Now open up src/main.rs and paste in the following code over the old one:

extern crate piston_window;

use piston_window::*;

fn main() {
    let window: PistonWindow = WindowSettings::new(
        "piston-tutorial",
        [600, 600]
    )
    .exit_on_esc(true)
    .build()
    .unwrap();
    for e in window {

    }
}

Then build it with cargo build or (on atom) Ctrl+Alt+B (WARNING: On Atom I have had issues with build and linter-rust, as the latter also uses cargo and it results in issues as cargo cannot have multiple instances building the same project at the same time). You should get a warning such as unused variable: 'e' - ignore this for now. Run your program by typing, in a terminal, ./target/debug/piston-tutorial (or double clicking it on windows). You should see something like this:

If the window is "transparent" - don't worry, we will fix that in a second. Otherwise, hurray! We have our first window. Close it by pressing esc.

What's going on?

Most of the code is pretty intuitive - we include the crate piston_window, bring its contents into the global namespace, create a window with the title "piston-tutorial" and 600 by 600 pixels and tell it to close when we press esc, as well as some boilerplate for displaying the window and retrieving it from our WindowSettings struct.

What might be a little bit confusing is the line for e in window - it looks like an event loop, but we are iterating over the window?!

The reality is that it's quite simple - by iterating on the window, we get an event (in e.event) but we also get a copy of the window's state, rather than having issues with window becoming a moved value. It's also technically correct to write for e in window.events(), but working with that has some differences.

The Square

Now we want to draw our square. To do this, we will call, inside the event loop, a member function of PistonWindow called draw_2d. It takes, as its only argument, a lambda function with two arguments - the graphical context and the graphical instance.

e.draw_2d(|c, g| {

First, let's clear the screen in a nice black:

clear([0.0, 0.0, 0.0, 1.0], g);

Now, let's get a transform for the center of the screen and describe the size and colour of the square:

let center = c.transform.trans(300.0, 300.0);
let square = rectangle::square(0.0, 0.0, 100.0);
let red = [1.0, 0.0, 0.0, 1.0];

Finally, we can display the square:

rectangle(red, square, center.trans(-50.0, -50.0), g); // We translate the rectangle slightly so that it's centered; otherwise only the top left corner would be centered

Build and run; you should get something like this:

Hurray! One last thing, let's make it spin. We will now use e.event to get the deltaT and do updates on the update event. Your event loop should now look like this:

let mut rotation: f64 = 0.0;
for e in window {
    match e.event {
        Some(Event::Update(UpdateArgs { dt })) => {
            rotation += 3.0 * dt;
        }
        _ => {
        }
    }
    e.draw_2d(|c, g| {
        clear([0.0, 0.0, 0.0, 1.0], g);
        let center = c.transform.trans(300.0, 300.0);
        let square = rectangle::square(0.0, 0.0, 100.0);
        let red = [1.0, 0.0, 0.0, 1.0];
        rectangle(red, square, center.rot_rad(rotation).trans(-50.0, -50.0), g); // We translate the rectangle slightly so that it's centered; otherwise only the top left corner would be centered

    });
}

And that's it! Running it will have your square spinning around:

You can find the finished source code for this tutorial here.

For the next part, where we will deal with keyboard input, go here

 
over 3 years ago

[Table of contents]

Introduction

Welcome to the "first" part of this tutorial!

Before jumping into Piston and making a game, we must first have a workspace with the required tools for building rust programs, as well as know enough rust to use piston.

The workspace

You will need to install the rust compiler and cargo, as I will use both to build rust and download libraries I use. On most linux distributions they are generally available in their respective package managers, but on Windows and Mac OS you might have to download the binaries or build the source code; I will not cover this topic in this tutorial

Additionally, you might want a couple of tools to edit your program - I personally use atom to edit my code, as well as rust-racer, which integrates with atom and other IDEs/editors to form quite a complete IDE for rust. Here is a list of atom packages/plugins I use and recommend to work with atom:

  • language-rust - Language support (highlighting and such) for rust in atom.
  • linter and linter-rust - Does linting for rust (i.e. highlights errors and warnings in your code)
  • racer - This package integrates with rust-racer above, allowing for code completion in rust
  • build and build-cargo - Allows you to build your rust program with cargo with a hotkey
  • atom-debugger - Use the GDB debugger in Atom
  • cargo-test-runner - Runs your cargo tests
  • termrk - A terminal panel inside atom. I find this one very useful, but you might have to fiddle a tiny bit with it to get it working as you intend

Other than these, you might also find the following useful:

  • build-tools - Allows you to choose from a set of commands you choose, so you can for examples start your program by pressing Ctrl+L and then tapping O. It's a bit wonky, so I don't actually recommend this one
  • youtube-pane - Use this if you work better while listening to music, but don't install it if you're easily distracted.
  • https://gist.github.com/colin-kiegel/40432d62c70c2ddf7354 - This is a wrapper for cargo that fixes some odd bugs in the interaction between the linter and build package when using cargo.

Whew! That was a bit of packages. The bright part is, together they form a solid IDE for rust. Keep in mind it's not necessary to install atom and the packages for atom I have described above, you can use anything that suits you best.

What now?

You might want to learn rust before going into this tutorial, if you haven't already. Two excellent resources for that are Rust by Example and the Rust Book (in that order, prefferably).

With that done, you can now proceed to the first part of the tutorial.