Разработка игры на ggez, часть 3: Обработка ввода

В последней части нашего руководства мы нарисовали пару ракеток для игры в «Пинг-Понг». Думаю, игра закончиться быстро, если они будут стоять на месте! Давайте, заставим наши ракетки двигаться!

Когда мы ранее говорили о EventHandler, мы затрагивали только обязательные методы, но эта черта также обладает другими, определенными по-умолчанию, в частности для обработки ввода: нажатие клавиш, движение мыши и т. д.

Так как для управления ракетками мы будем использовать клавиатуру, нам интересны методы key_down_event и key_up_event, обрабатывающие нажатие и отпуск клавиш, соответственно. Отмечу, что key_down_event имеет реализацию по-умолчанию, которая завершает работу программы по нажатию esc, не стоит об этом забывать при переопределении метода.

В качестве аргументов эти методы принимают: KeyCode - перечисление всех клавиш; KeyMods - различные модификаторы, такие как зажатый shift или ctrl и флаг, определяющий повторное нажатие. Все это позволяет перехватить все возможные комбинации клавиш.

Захват ввода

В начале, создадим структуру, которая будет хранить состояние ввода. Мы могли бы хранить данные прямо в нашем глобальном состоянии, но лучше добавить новую абстракцию, чтобы сохранить читаемость кода.

struct InputState {
    right_yaxis: f32,
    left_yaxis: f32,
}

struct GameState {
    left: Rect,
    right: Rect,
    input: InputState,
}

Реализуем наши методы:

impl EventHandler for GameState {
    fn key_down_event(
        &mut self,
        ctx: &mut Context,
        keycode: KeyCode,
        _keymod: KeyMods,
        _repeat: bool,
    ) {
        match keycode {
            KeyCode::W => {
                self.input.left_yaxis = -1.0;
            }
            KeyCode::S => {
                self.input.left_yaxis = 1.0;
            }
            KeyCode::O => {
                self.input.right_yaxis = -1.0;
            }
            KeyCode::L => {
                self.input.right_yaxis = 1.0;
            }
            KeyCode::Escape => event::quit(ctx),
            _ => (),
        }
    }

    fn key_up_event(&mut self, _ctx: &mut Context, keycode: KeyCode, _keymod: KeyMods) {
        match keycode {
            KeyCode::W | KeyCode::S => {
                self.input.left_yaxis = 0.0;
            }
            KeyCode::O | KeyCode::L => {
                self.input.right_yaxis = 0.0;
            }
            _ => (),
        }
    }

Хотя кода много, он довольно прост. Мы указываем отклонение по оси Y, для левой ракетки при нажатии Wили S, а для правой O или L. А при отпуске тех же клавиш, сбрасываем отклонение к нулю.

Почему мы используем отклонение? Все просто, это делает наш код более переносимым, к примеру, если мы захотим управлять не только по средствам клавиатуры, но и с джойстика, где ввод представлен отклонением стиков.

Движение объектов

Прежде чем двигать наши ракетки, хочется упомянуть FPS (Frames per Second). В играх мы стремимся отрисовать как можно больше кадров, для более плавной картинки. Но иногда, в таких, простых играх как наша, стоит ограничить вычисления. Эти слишком обширная тема и мы не будет затрагивать ее в данной статье. Для ограничения вычислений, мы воспользуемся check_update_time, которая возьмет всю работу на себя.

const DESIRED_FPS: u32 = 60;
const MOVE_SPEED: f32 = 1.9;

impl EventHandler for GameState {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        while timer::check_update_time(ctx, DESIRED_FPS) {
            self.left.y = (self.left.y + self.input.left_yaxis * MOVE_SPEED)
                .min(ARENA_HEIGHT - PADDLE_HEIGHT)
                .max(0.0);

            self.right.y = (self.right.y + self.input.right_yaxis * MOVE_SPEED)
                .min(ARENA_HEIGHT - PADDLE_HEIGHT)
                .max(0.0);
        }
        Ok(())
    }
}

Здесь, мы задаем новое значение координат по оси Y, для каждой из кареток. Чтобы было удобнее управлять скоростью мы ввели новую переменную MOVE_SPEED. А чтобы наши каретки “не уезжали” за границы игрового поля, мы задали границы с помощью минимального и максимального значения.

Готово! Теперь мы можем скомпилировать и запустить наш код с помощью cargo run и проверить, что первый игрок будет управлять ракеткой с одной стороны клавиатуры, а второй - с другой.

Полный пример:

use ggez::event::{self, EventHandler, KeyCode, KeyMods};
use ggez::graphics::{self, Rect};
use ggez::timer;
use ggez::{conf, Context, ContextBuilder, GameResult};

const ARENA_WIDTH: f32 = 500.0;
const ARENA_HEIGHT: f32 = 500.0;

const PADDLE_WIDTH: f32 = 10.0;
const PADDLE_HEIGHT: f32 = 80.0;

const DESIRED_FPS: u32 = 60;
const MOVE_SPEED: f32 = 1.9;

struct InputState {
    right_yaxis: f32,
    left_yaxis: f32,
}

struct GameState {
    left: Rect,
    right: Rect,
    input: InputState,
}

impl EventHandler for GameState {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        while timer::check_update_time(ctx, DESIRED_FPS) {
            self.left.y = (self.left.y + self.input.left_yaxis * MOVE_SPEED)
                .min(ARENA_HEIGHT - PADDLE_HEIGHT)
                .max(0.0);

            self.right.y = (self.right.y + self.input.right_yaxis * MOVE_SPEED)
                .min(ARENA_HEIGHT - PADDLE_HEIGHT)
                .max(0.0);
        }
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx, graphics::BLACK);

        let left = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            self.left,
            graphics::WHITE,
        )?;

        let right = graphics::Mesh::new_rectangle(
            ctx,
            graphics::DrawMode::fill(),
            self.right,
            graphics::WHITE,
        )?;

        graphics::draw(ctx, &left, graphics::DrawParam::default())?;
        graphics::draw(ctx, &right, graphics::DrawParam::default())?;

        graphics::present(ctx)?;
        Ok(())
    }

    fn key_down_event(
        &mut self,
        ctx: &mut Context,
        keycode: KeyCode,
        _keymod: KeyMods,
        _repeat: bool,
    ) {
        match keycode {
            KeyCode::W => {
                self.input.left_yaxis = -1.0;
            }
            KeyCode::S => {
                self.input.left_yaxis = 1.0;
            }
            KeyCode::O => {
                self.input.right_yaxis = -1.0;
            }
            KeyCode::L => {
                self.input.right_yaxis = 1.0;
            }
            KeyCode::Escape => event::quit(ctx),
            _ => (),
        }
    }

    fn key_up_event(&mut self, _ctx: &mut Context, keycode: KeyCode, _keymod: KeyMods) {
        match keycode {
            KeyCode::W | KeyCode::S => {
                self.input.left_yaxis = 0.0;
            }
            KeyCode::O | KeyCode::L => {
                self.input.right_yaxis = 0.0;
            }
            _ => (),
        }
    }
}

impl GameState {
    fn new() -> Self {
        let left = Rect::new(
            0.0,                                      // x
            ARENA_HEIGHT * 0.5 - PADDLE_HEIGHT * 0.5, // y
            PADDLE_WIDTH,
            PADDLE_HEIGHT,
        );
        let right = Rect::new(
            ARENA_WIDTH - PADDLE_WIDTH,               // x
            ARENA_HEIGHT * 0.5 - PADDLE_HEIGHT * 0.5, // y
            PADDLE_WIDTH,
            PADDLE_HEIGHT,
        );

        let input = InputState {
            right_yaxis: 0.0,
            left_yaxis: 0.0,
        };

        Self { left, right, input }
    }
}

fn main() {
    let mut cb = ContextBuilder::new("game01", "author")
        .window_setup(conf::WindowSetup::default().title("My game!"))
        .window_mode(conf::WindowMode::default().dimensions(ARENA_WIDTH, ARENA_HEIGHT));

    let (ctx, event_loop) = &mut cb.build().unwrap();

    let mut state = GameState::new();

    match event::run(ctx, event_loop, &mut state) {
        Ok(_) => println!("Exited cleanly."),
        Err(e) => println!("Error occured: {}", e),
    }
}