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.

← Part 1: Hello, Piston! Part 3: From a square to a tank →