Правене на игри с GGEZ

04 януари 2020

Административни неща

Rust-shooter

Пълен код: https://github.com/AndrewRadev/rust-shooter

Инсталация на ggez

Библиотеката има стабилна версия 0.5.1, но поне под линукс/X11 ударих на panic on mouseover… Това не е свързано с GGEZ, а с една от вътрешните библиотеки, но означава, че ми трябваше непубликувания devel branch.

Инсталация на ggez

Библиотеката има стабилна версия 0.5.1, но поне под линукс/X11 ударих на panic on mouseover… Това не е свързано с GGEZ, а с една от вътрешните библиотеки, но означава, че ми трябваше непубликувания devel branch.

Това, което ползвам в текущата версия на rust-shooter:

1 2
[dependencies]
ggez = { git = "https://github.com/ggez/ggez", rev = "3183367f397aa46fade5912fe23b53ca68b55bb4" }

Или, алтернативно:

1 2
[dependencies]
ggez = { git = "https://github.com/ggez/ggez", branch = "devel" }

Тази версия работи чудесно за мен, но имайте предвид, че може публикуваната документация в docs.rs да не е 100% правилна. Ползвайте cardo doc --open.

Инсталиране от път

Допълнително, ако някой ден се озовете в ситуация, в която искате да оправите бъг локално и да ползвате тази версия, можете да си инсталирате пакет от локален път:

1 2
[dependencies]
ggez = { path = "/home/andrew/src/ggez" }

Скелет на играта

Фреймуърка очаква да дефинирате ваш тип, който да имплементира трейта ggez::event::EventHandler:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
struct MainState { /* ... */ }

impl event::EventHandler for MainState {
    fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
        // Променяме състоянието на играта
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        let dark_blue = graphics::Color::from_rgb(26, 51, 77);
        graphics::clear(ctx, dark_blue);
        // Рисуваме неща
        graphics::present(ctx);
        Ok(())
    }
}

Скелет на играта

В main функцията създаваме инстанция на нашия тип и "контекст" (за рисуване/звуци) с конфигурация, и стартираме event loop-а:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
pub fn main() {
    // Конфигурация:
    let conf = Conf::new().
        window_mode(WindowMode {
            min_width: 1024.0,
            min_height: 768.0,
            ..Default::default()
        });

    // Контекст и event loop
    let (mut ctx, event_loop) = ContextBuilder::new("shooter", "FMI").
        default_conf(conf.clone()).
        build().
        unwrap();

    // ... Подготвяне на ресурси, вижте следващия слайд

    // Пускане на главния loop
    let state = MainState::new(&mut ctx, conf).unwrap();
    event::run(ctx, event_loop, state);
}

Зареждане на ресурси

За да може библиотеката да си намери картинки и звуци при компилация, добре е да добавим локалната директория "resources" (или както искаме да я наречем). Когато разпространяваме играта, тя ще търси по default папка до exe-то, която се казва "resources", но подкарвайки я с cargo run, е по-удобно да използваме друга:

1 2 3 4 5 6 7 8 9 10 11
// ...
// let (mut ctx, event_loop) = ...

if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
    let mut path = path::PathBuf::from(manifest_dir);
    path.push("resources");
    ggez::filesystem::mount(&mut ctx, &path, true);
}

// let state = MainState::new(&mut ctx, &conf).unwrap();
// ...

Update

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
fn update(&mut self, ctx: &mut Context) -> GameResult<()> {
    if self.game_over { return Ok(()); }
    const DESIRED_FPS: u32 = 60;

    while timer::check_update_time(ctx, DESIRED_FPS)? {
        let seconds = 1.0 / (DESIRED_FPS as f32);

        self.time_until_next_enemy -= seconds;
        if self.time_until_next_enemy <= 0.0 {
            // Създаваме следващия противник
            // self.time_until_next_enemy = ...;
        }

        // Обновяваме позиция на играча, на изстрелите, ...
    }
}

Update

Update

Най-простата форма на update би могла да изглежда така:

1 2
self.position.x += self.velocity.x * seconds;
self.position.y += self.velocity.y * seconds;

Променяме velocity в зависимост от, например, задържан клавиш-стрелкичка, или в зависимост от AI-а на противниците, или както си пожелаем. Имаме пълната мощ на библиотеката nalgebra, която вероятно няма да ни трябва за много сложни неща:

1 2 3 4 5 6
#[derive(Debug)]
pub struct Enemy {
    position: Point2<f32>,
    velocity: Vector2<f32>,
    // ... и каквото още ни трябва ...
}

Update

За нещастие, за да се поддържа стабилност на алгебрата, се ползва библиотеката mint, която е доста минимална, така че не можем да събираме точки и вектори примерно -- сметките се правят или по координати, или си имплементираме помощни средства. Или правим операции с nalgebra::Point2 и ги конвертираме до mint::Point2<f32> с .into() като ги подаваме на ggez.

1
nalgebra = { version = "0.23.2", features = ["mint"] }

Алтернативна библиотека за алгебра, която май се използва в примерите днешно време, е glam, която също има features = ["mint"] за съвместимост.

Input

Има още два метода, които могат да се имплементират за event::EventHandler:

1 2 3 4 5 6 7 8 9 10 11
fn key_down_event(&mut self,
                  _ctx: &mut Context,
                  keycode: event::KeyCode,
                  _keymod: input::keyboard::KeyMods,
                  _repeat: bool) {
    match keycode {
        event::KeyCode::Space => self.input.fire = true,
        // ... Други клавиши ...
        _ => (), // Do nothing
    }
}

И еквивалентния за key up …

Input

Има още два метода, които могат да се имплементират за event::EventHandler:

1 2 3 4 5 6 7 8 9 10
fn key_up_event(&mut self,
                _ctx: &mut Context,
                keycode: event::KeyCode,
                _keymod: input::keyboard::KeyMods) {
    match keycode {
        event::KeyCode::Space => self.input.fire = false,
        // ... Други клавиши ...
        _ => (), // Do nothing
    }
}

Drawing

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
    let dark_blue = graphics::Color::from_rgb(26, 51, 77);
    graphics::clear(ctx, dark_blue);

    if self.game_over {
        // Рисуваме "край на играта"
        graphics::present(ctx)?;
        return Ok(())
    }

    // Рисуваме противници, играч, куршум, и т.н.

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

Drawing

Просто викане на методи в модула graphics:: Когато имаме координатите и състоянието на противници, играч, изстрели, сцена, фон, и прочее, всичко се свежда до това да извикаме методи, които казват на графичната система какво да нарисува и къде.

Collision detection

Не ни трябва нищо сложно за тази конкретна игра. За всеки противник и всеки изстрел, проверяваме дали изстрела е в противника:

1 2 3 4 5 6 7 8 9 10
for enemy in &mut self.enemies {
    for shot in &mut self.shots {
        if enemy.bounding_rect(ctx).contains(shot.pos) {
            shot.is_alive = false;
            enemy.is_alive = false;
            self.score += 1;
            let _ = self.assets.boom_sound.play(ctx);
        }
    }
}

Тестване

Инициализиране на контекст може да се направи само веднъж, което може да затрудни тестването. Решението е decoupling -- вместо конкретен тип, използваме trait, който можем да варираме:

1 2 3 4 5
pub trait Sprite: Debug {
    fn draw(&mut self, center: Point2<f32>, ctx: &mut Context) -> GameResult<()>;
    fn width(&self, ctx: &mut Context) -> f32;
    fn height(&self, ctx: &mut Context) -> f32;
}

Тестване

В истинския код, имаме нещо истински използваемо, което използва assets, fonts, drawing:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#[derive(Debug)]
pub struct TextSprite {
    text: graphics::Text,
}

impl TextSprite {
    pub fn new(label: &str, ctx: &mut Context) -> GameResult<TextSprite> {
        let font = graphics::Font::new(ctx, "/DejaVuSerif.ttf")?;
        let mut text = graphics::Text::new(label);
        text.set_font(font, graphics::PxScale::from(18.0));
        Ok(TextSprite { text })
    }
}

impl Sprite for TextSprite {
    fn draw(&mut self, top_left: Point2<f32>, ctx: &mut Context) -> GameResult<()> {
        // ...
    }

    fn width(&self, ctx: &mut Context) -> f32 { self.text.width(ctx) }
    fn height(&self, ctx: &mut Context) -> f32 { self.text.height(ctx) }
}

Тестване

В тестовете, спокойно можем да си сложим един "фалшив" sprite:

1 2 3 4 5 6 7 8 9 10 11 12
#[derive(Debug)]
struct MockSprite {
    width: u32,
    height: u32,
}

impl Sprite for MockSprite {
    fn draw(&mut self, _center: Point2<f32>, _ctx: &mut Context) -> GameResult<()> { Ok(()) }

    fn width(&self, _ctx: &mut Context) -> f32 { self.width }
    fn height(&self, _ctx: &mut Context) -> f32 { self.height }
}

Категории игри

Обекти, които се движат по екрана и имат контакт:

Категории игри

Игри на дъска с обекти, които движете в определена форма (rust-memory-game):

Категории игри

Игри на дъска с обекти, които движете в определена форма:

Съвети

Ресурси

Ресурси

Ресурси

Други варианти за игри

ECS

Други варианти за игри

ECS

Минималистични, подобно на GGEZ

Други варианти за игри

ECS

Минималистични, подобно на GGEZ

Сравнение

Въпроси