Настройка проекта
Давайте установим 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) }
Вы должны увидеть что-то похожее:
Базовые концепции и синтаксис
Теперь, когда у нас есть базовое окно, давайте погрузимся в код и поймём основные концепции и синтаксис 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 мы готовы двигаться дальше. Увидимся в следующей главе!
КОД: Увидеть весь код из данной главы можно здесь.