Настройка проекта

Давайте установим rustup, который проинсталлирует Rust и его компилятор. Теперь проверим, что всё корректно установлено, используя следующие две команды. Версии не слишком важны, так что можете не беспокоиться, если у вас будет другая.

$ rustc --version
rustc 1.40.0
$ cargo --version
cargo 1.40.0

Создание проекта

Cargo — пакетный менеджер Rust, который мы будем использовать для создания проекта нашей игры. Перейдите в директорию, где она будет жить, и выполните следующую команду — она создаст новый проект с названием rust-sokoban.

$ cargo init rust-sokoban

После выполнения команды вы должны увидеть такую структуру директорий:

├── src
│   └── main.rs
└── Cargo.toml

Теперь в этой директории мы можем запустить cargo run, после чего увидим что-то похожее:

$ cargo run
   Compiling rust-sokoban v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 1.30s
     Running `../rust-sokoban/target/debug/rust-sokoban`
Hello, world!

Создание игры

Настало время превратить наш базовый "Hello, World!" в игру! Мы будем использовать ggez — один из популярных 2D-движков для создания игр.

Помните файл Cargo.toml, который мы видели в нашей директории? Этот файл используется для управления зависимостями, так что если мы захотим использовать какие-нибудь крейты, мы должны добавить их туда. Давайте добавим ggez как одну из наших зависимостей.

ЕЩЁ: Узнать больше о Cargo и toml-файлах можно здесь.

[dependencies]
ggez = "0.7"

Теперь снова запустим команду cargo run — вы должны видеть что-то похожее. Её выполнение займёт больше времени, чем обычно, так как она загружает новые зависимости с crates.io, затем компилирует их и, наконец, линкует с нашей библиотекой.

cargo run
    Updating crates.io index
    Downloaded ....
    ....
    Compiling ....
    ....
    Finished dev [unoptimized + debuginfo] target(s) in 2m 15s
    Running `.../rust-sokoban/target/debug/rust-sokoban`
    Hello, world!

Обратите внимание: если вы используете Ubuntu, то, возможно, вам потребуется установить некоторые дополнительные системные зависимости. Если этот шаг потерпел неудачу и вы видите ошибки, связанные с alsa и libudev, то установите их при помощи команды sudo apt-get install libudev-dev libasound2-dev.

Теперь давайте опробуем ggez в главном файле и настроим окно. Это лишь простейший пример программы на ggez, в которой создаётся окно — и больше ничего. Скопируйте это в main.rs и запустите.

use ggez::{conf, event, Context, GameResult};
use std::path;

// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {}

// This is the main event loop. ggez tells us to implement
// two things:
// - updating
// - rendering
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update game logic here
        Ok(())
    }

    fn draw(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update draw here
        Ok(())
    }
}

pub fn main() -> GameResult {
    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = context_builder.build()?;
    // Create the game state
    let game = Game {};
    // Run the main event loop
    event::run(context, event_loop, game)
}

Вы должны увидеть что-то похожее:

Screenshot

Базовые концепции и синтаксис

Теперь, когда у нас есть базовое окно, давайте погрузимся в код и поймём основные концепции и синтаксис Rust.

Импортирование

Если вы уже изучали другие языки программирования, то эта концепция должна быть вам знакома. Для того, чтобы добавить типы и пространства имён в область видимости из наших зависимостей (или крейтов), мы используем слово use.


#![allow(unused)]
fn main() {
// это импортирует `conf`, `event`, `Context` и `GameResult` из пакета ggez
use ggez::{conf, event, Context, GameResult};
}

Объявление структуры


#![allow(unused)]
fn main() {
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {}
}

ЕЩЁ: Узнать больше о структурах вы можете здесь.

Реализация типажа

В других языках аналогом типажей являются интерфейсы, которые позволяют нам привязывать типам определённое поведение. В нашем случае мы хотим реализовать требуемое типажом EventHandler поведение для структуры Game.


#![allow(unused)]
fn main() {
// This is the main event loop. ggez tells us to implement
// two things:
// - updating
// - rendering
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update game logic here
        Ok(())
    }

    fn draw(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update draw here
        Ok(())
    }
}
}

ЕЩЁ: Узнать больше о типажах вы можете здесь.

Функции

Также мы изучим, как в Rust объявляются функции.


#![allow(unused)]
fn main() {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // TODO: update game logic here
        Ok(())
    }
}

Вам может быть интересно, что означает self. В данном случае self означает, что функция update является методом, т. е. принадлежит экземпляру структуры Game и не может быть вызвана в статическом контексте.

ЕЩЁ: Узнать больше о функциях вы можете здесь.

Mut-синтаксис

Ещё вы можете задаться вопросом, что значат &mut в &mut self функции update. Изменяемость (mutablitiy) объекта просто говорит о том, можно ли изменять его или нет. Ознакомьтесь со следующим примером объявления переменных:


#![allow(unused)]
fn main() {
let a = 10; // a не может быть изменена, так как она не объявлена изменяемой
let mut b = 20; // b может быть изменена, так как она объявлена изменяемой
}

Теперь вернёмся к функции update. Если mut используется вместе с self, то оно ссылается на экземпляр структуры, к которой относится функция. Возьмём другой пример:


#![allow(unused)]
fn main() {
// Простая структура X с переменной num внутри
struct X {
    num: u32
}

// Блок реализации для X
impl X {
    fn a(&self) { self.num = 5 }
    // a не может изменить экземпляр структуры X, так как
    // используется &self. Это не скомпилируется

    fn b(&mut self) { self.num = 5 }
    // b может изменять экземпляр структуры X, так как
    // используется &mut self. Эта часть скомпилируется
}
}

ЕЩЁ: Узнать больше про изменяемость вы можете здесь (в этой лекции используется Java, но эти концепции можно применить к любым языкам), а прочитать больше о переменных и изменяемости в Rust можно здесь.

После небольшого введения в синтаксис Rust мы готовы двигаться дальше. Увидимся в следующей главе!

КОД: Увидеть весь код из данной главы можно здесь.