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

Давайте установим 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.9.3"

Теперь снова запустим команду 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 мы готовы двигаться дальше. Увидимся в следующей главе!

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