I study Rust: How I made the game "Snake"

Recently I began to learn the programming language Rust and since when I study a new language I make “Snake” on it , I decided to make it exactly.
For 3D graphics, the Three.rs library was used, which is the port of the Three.js library
→ Code
→ Download and play
Screenshot game

Game code
/*Подключаем внешние библиотеки.
Также же в Cargo.toml
[dependencies]
rand="*"
three="*"
serde="*"
bincode="*"
serde_derive="*"
прописываем
*/externcrate rand;
externcrate three;
externcrate bincode;
externcrate serde;
#[macro_use]externcrate serde_derive;
// Добавляем нужные нам вещи в нашу область видимости.use rand::Rng;
use three::*;
use std::error::Error;
//Entities ------------------------------------------------------------------/*
Это макросы. Они генерируют какой ни будь код автоматически.
В нашем конкретном случае:
Debug - Создаст код который позволить выводить нашу структуру в терминал
Clone - Создаст код который будет копировать нашу структуру т. е. у нашей структуры появиться метод clone()
Eq и PartialEq позволять сравнивать наши Point с помощью оператора ==
*/#[derive(Debug, Clone, Eq, PartialEq, Default)]//Обьявление структуры с двумя полями. Она будет играть роль точкиstructPoint {
x: u8,
y: u8,
}
//Методы нашей структурыimpl Point {
// Можно было использовать просто оператор == В общем, это метод который проверяет пересекаются ли наши точкиpubfnintersects(&self, point: &Point) -> bool {
self.x == point.x && self.y == point.y
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]//Эта структура будет хранить объектное представление границ фрейма в пределах которого будет двигаться наша змейкаstructFrame {
min_x: u8,
min_y: u8,
max_x: u8,
max_y: u8,
}
impl Frame {
pubfnintersects(&self, point: &Point) -> bool {
point.x == self.min_x
|| point.y == self.min_y
|| point.x == self.max_x
|| point.y == self.max_y
}
}
#[derive(Debug, Clone, Eq, PartialEq)]//Объявление перечисления с 4 вариантами//Оно будет отвечать за то куда в данный момент повернута голова змейкиenumDirection {
Left,
Right,
Top,
Bottom,
}
//Реализация трейта (в других языках это еще называется интерфейс)// для нашего перечисления.//Обьект реализующий этот трейт способен иметь значение по умолчанию.implDefaultfor Direction {
fndefault() -> Direction {
return Direction::Right;
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]//Собственно наша змейкаstructSnake {
direction: Direction,
points: std::collections::VecDeque<Point>,
start_x: u8,
start_y: u8,
}
impl Snake {
//Статический метод конструктор для инициализации нового экземпляра нашей змейкиpubfnnew(x: u8, y: u8) -> Snake {
letmut points = std::collections::VecDeque::new();
for i in0..3 {
points.push_front(Point { x: x + i, y: i + y });
}
Snake { direction: Direction::default(), points, start_x: x, start_y: y }
}
//Увеличивает длину нашей змейки на одну точкуpubfngrow(mutself) -> Snake {
ifletSome(tail) = self.points.pop_back() {
self.points.push_back(Point { x: tail.x, y: tail.y });
self.points.push_back(tail);
}
self
}
//Сбрасывает нашу змейку в начальное состояниеpubfnreset(self) -> Snake {
Snake::new(self.start_x, self.start_y)
}
//Поворачивает голову змейки в нужном нам направленииpubfnturn(mutself, direction: Direction) -> Snake {
self.direction = direction;
self
}
//Если голова змейки достает до еды то увеличивает длину змейки на один и возвращает информацию о том была ли еда съеденаpubfntry_eat(mutself, point: &Point) -> (Snake, bool) {
let head = self.head();
if head.intersects(point) {
return (self.grow(), true);
}
(self, false)
}
//Если голова змейки столкнулась с фреймом то возвращает змейку в начальное состояниеpubfntry_intersect_frame(mutself, frame: &Frame) -> Snake {
let head = self.head();
if frame.intersects(&head) {
returnself.reset();
}
self
}
//Если голова змейки столкнулась с остальной частью то возвращает змейку в начальное состояние.pubfntry_intersect_tail(mutself) -> Snake {
let head = self.head();
let p = self.points.clone();
let points = p.into_iter().filter(|p| head.intersects(p));
if points.count() > 1 {
returnself.reset();
}
self
}
//Дает голову змейкиpubfnhead(&self) -> Point {
self.points.front().unwrap().clone()
}
//Перемещает змейку на одну точку в том направление куда в данный момент смотрит голова змейкиpubfnmove_snake(mutself) -> Snake {
ifletSome(mut tail) = self.points.pop_back() {
let head = self.head();
matchself.direction {
Direction::Right => {
tail.x = head.x + 1;
tail.y = head.y;
}
Direction::Left => {
tail.x = head.x - 1;
tail.y = head.y;
}
Direction::Top => {
tail.x = head.x;
tail.y = head.y - 1;
}
Direction::Bottom => {
tail.x = head.x;
tail.y = head.y + 1;
}
}
self.points.push_front(tail);
}
self
}
}
//Data Access Layer ----------------------------------------------------------------#[derive(Debug, Clone, Eq, PartialEq, Default)]//Структура для создания новой еды для змейкиstructFoodGenerator {
frame: Frame
}
impl FoodGenerator {
//Создает новую точку в случайном месте в пределах фреймаpubfngenerate(&self) -> Point {
let x = rand::thread_rng().gen_range(self.frame.min_x + 1, self.frame.max_x);
let y = rand::thread_rng().gen_range(self.frame.min_y + 1, self.frame.max_y);
Point { x, y }
}
}
#[derive(Serialize, Deserialize)]//Хранит текущий и максимальный счет игрыstructScoreRepository {
score: usize
}
impl ScoreRepository {
//Статический метод для сохранения текущего счета в файле// Result это перечисление которое может хранить в себе либо ошибку либо результат вычисленийfnsave(value: usize) -> Result<(), Box<Error>> {
use std::fs::File;
use std::io::Write;
let score = ScoreRepository { score: value };
//Сериализуем структуру в массив байтов с помощью библиотеки bincode// Оператор ? пробрасывает ошибку на верх т. е. если тут будет ошибка то она станет результатом методаlet bytes: Vec<u8> = bincode::serialize(&score)?;
//Создаем новый файл или если он уже сушествует то перезаписываем его.letmut file = File::create(".\\score.data")?;
match file.write_all(&bytes) {
Ok(t) => Ok(t),
//Error это трейт а у трейт нет точного размера во время компиляции поэтому// нам надо обернуть значение в Box и в результате мы работает с указателем на//кучу в памяти где лежит наш объект а не с самим объектом а у указателя есть определенный размер// известный во время компиляцииErr(e) => Err(Box::new(e))
}
}
//Загружаем сохраненный результат из файлаfnload() -> Result<usize, Box<Error>> {
use std::fs::File;
letmut file = File::open("./score.data")?;
let data: ScoreRepository = bincode::deserialize_from(file)?;
Ok(data.score)
}
}
//Business Logic Layer------------------------------------------------------------#[derive(Debug, Clone, Default)]//Объектное представление логики нашей игрыstructGame {
snake: Snake,
frame: Frame,
food: Point,
food_generator: FoodGenerator,
score: usize,
max_score: usize,
total_time: f32,
}
impl Game {
//Конструктор для создания игры с фреймом заданной высоты и шириныfnnew(height: u8, width: u8) -> Game {
let frame = Frame { min_x: 0, min_y: 0, max_x: width, max_y: height };
let generator = FoodGenerator { frame: frame.clone() };
let food = generator.generate();
let snake = Snake::new(width / 2, height / 2);
Game {
snake,
frame,
food,
food_generator: generator,
score: 0,
max_score: match ScoreRepository::load() {
Ok(v) => v,
Err(_) => 0
},
total_time: 0f32,
}
}
// Проверяем, прошло ли достаточно времени с момента когда мы в последний раз//двигали нашу змейку и если да то передвигаем ее// и проверяем столкновение головы змейки с остальными объектами игры// иначе ничего не делаемfnupdate(mutself, time_delta_in_seconds: f32) -> Game {
let (game, is_moving) = self.is_time_to_move(time_delta_in_seconds);
self = game;
if is_moving {
self.snake = self.snake.clone()
.move_snake()
.try_intersect_tail()
.try_intersect_frame(&self.frame);
self.try_eat()
} else {
self
}
}
//Проверяем, настало ли время для того чтобы передвинуть змейку.fnis_time_to_move(mutself, time_delta_in_seconds: f32) -> (Game, bool) {
let time_to_move: f32 = 0.030;
self.total_time += time_delta_in_seconds;
ifself.total_time > time_to_move {
self.total_time -= time_to_move;
(self, true)
} else {
(self, false)
}
}
//Проверяем, съела ли наша змейку еду и если да// то создаем новую еду, начисляем игроку очки// иначе сбрасываем игроку текущий счетfntry_eat(mutself) -> Game {
let initial_snake_len = 3;
ifself.snake.points.len() == initial_snake_len {
self.score = 0
}
let (snake, eaten) = self.snake.clone().try_eat(&self.food);
self.snake = snake;
if eaten {
self.food = self.food_generator.generate();
self.score += 1;
ifself.max_score < self.score {
self.max_score = self.score;
ScoreRepository::save(self.max_score);
}
};
self
}
// Поворачиваем змейку в нужном направленииfnhandle_input(mutself, input: Direction) -> Game {
let snake = self.snake.turn(input);
self.snake = snake;
self
}
}
//Application Layer--------------------------------------------------------------// --- Model ----#[derive(Debug, Clone, Eq, PartialEq)]enumPointDtoType {
Head,
Tail,
Food,
Frame,
}
implDefaultfor PointDtoType {
fndefault() -> PointDtoType {
PointDtoType::Frame
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]//Модель котору будет видеть представление для отображения пользователю.structPointDto {
x: u8,
y: u8,
state_type: PointDtoType,
}
//------------------------------Controller -----------------------------#[derive(Debug, Clone, Default)]// Контроллер который будет посредником между представлением и логикой нашей игрыstructGameController {
game: Game,
}
impl GameController {
fnnew() -> GameController {
GameController { game: Game::new(30, 30) }
}
//Получить коллекцию точек которые нужно от рисовать в данный моментfnget_state(&self) -> Vec<PointDto> {
letmut vec: Vec<PointDto> = Vec::new();
vec.push(PointDto { x: self.game.food.x, y: self.game.food.y, state_type: PointDtoType::Food });
let head = self.game.snake.head();
vec.push(PointDto { x: head.x, y: head.y, state_type: PointDtoType::Head });
//Все точки за исключением головы змеиfor p inself.game.snake.points.iter().filter(|p| **p != head) {
vec.push(PointDto { x: p.x, y: p.y, state_type: PointDtoType::Tail });
}
//горизонтальные линии фреймаfor x inself.game.frame.min_x..=self.game.frame.max_x {
vec.push(PointDto { x: x, y: self.game.frame.max_y, state_type: PointDtoType::Frame });
vec.push(PointDto { x: x, y: self.game.frame.min_y, state_type: PointDtoType::Frame });
}
//Вертикальные линии фреймаfor y inself.game.frame.min_y..=self.game.frame.max_y {
vec.push(PointDto { x: self.game.frame.max_x, y: y, state_type: PointDtoType::Frame });
vec.push(PointDto { x: self.game.frame.min_x, y: y, state_type: PointDtoType::Frame });
}
vec
}
//Обновляем состояние игрыfnupdate(mutself, time_delta: f32, direction: Option<Direction>) -> GameController {
let game = self.game.clone();
self.game = match direction {
None => game,
Some(d) => game.handle_input(d)
}
.update(time_delta);
self
}
pubfnget_max_score(&self) -> usize {
self.game.max_score.clone()
}
pubfnget_score(&self) -> usize {
self.game.score.clone()
}
}
//------------------------View ---------------//Представление для отображение игры для пользователю и получение от него командstructGameView {
controller: GameController,
window: three::Window,
camera: three::camera::Camera,
ambient: three::light::Ambient,
directional: three::light::Directional,
font: Font,
current_score: Text,
max_score: Text,
}
impl GameView {
fnnew() -> GameView {
let controller = GameController::new();
//Создаем окно в котором будет отображаться наша играletmut window = three::Window::new("3D Snake Game By Victorem");
//Создаем камеру через которую игрок будет видеть нашу игруlet camera = window.factory.perspective_camera(60.0, 10.0..40.0);
//Перемещаем камеру в [x, y, z]
camera.set_position([15.0, 15.0, 30.0]);
//Создаем постоянное окружающее освещениеlet ambient_light = window.factory.ambient_light(0xFFFFFF, 0.5);
window.scene.add(&ambient_light);
//Создаем направленный светletmut dir_light = window.factory.directional_light(0xffffff, 0.5);
dir_light.look_at([350.0, 350.0, 550.0], [0.0, 0.0, 0.0], None);
window.scene.add(&dir_light);
//Загружаем из файла шрифт которым будет писать текстlet font = window.factory.load_font(".\\DejaVuSans.ttf");
//Создаем текст на экране куда будет записывать текущий и максимальный счетlet current_score = window.factory.ui_text(&font, "0");
letmut max_score = window.factory.ui_text(&font, "0");
max_score.set_pos([0.0, 40.0]);
window.scene.add(¤t_score);
window.scene.add(&max_score);
GameView { controller, window, camera, ambient: ambient_light, directional: dir_light, font, current_score, max_score }
}
//Считываем клавишу которую последней нажал пользователь и на основании ее выбираем новое направлениеfnget_input(&self) -> Option<Direction> {
matchself.window.input.keys_hit().last() {
None => None,
Some(k) =>
match *k {
three::Key::Left => Some(Direction::Left),
three::Key::Right => Some(Direction::Right),
three::Key::Down => Some(Direction::Top),
three::Key::Up => Some(Direction::Bottom),
_ => None,
}
}
}
//Преобразуем модель полученную от контроллера в набор сеточных объектов нашей сценыfnget_meshes(mutself) -> (Vec<Mesh>, GameView) {
//Создаем сферуlet sphere = &three::Geometry::uv_sphere(0.5, 24, 24);
//Создаем зеленое покрытие для нашей сферы с моделью освещения по Фонгуlet green = &three::material::Phong {
color: three::color::GREEN,
glossiness: 30.0,
};
let blue = &three::material::Phong {
color: three::color::BLUE,
glossiness: 30.0,
};
let red = &three::material::Phong {
color: three::color::RED,
glossiness: 30.0,
};
let yellow = &three::material::Phong {
color: three::color::RED | three::color::GREEN,
glossiness: 30.0,
};
// Преобразуем нашу модель в сеточные объектыlet meshes = self.controller.clone().get_state().iter().map(|s| {
let state = s.clone();
match state.state_type {
PointDtoType::Frame => {
let m = self.window.factory.mesh(sphere.clone(), blue.clone());
m.set_position([state.x asf32, state.y asf32, 0.0]);
m
}
PointDtoType::Tail => {
let m = self.window.factory.mesh(sphere.clone(), yellow.clone());
m.set_position([state.x asf32, state.y asf32, 0.0]);
m
}
PointDtoType::Head => {
let m = self.window.factory.mesh(sphere.clone(), red.clone());
m.set_position([state.x asf32, state.y asf32, 0.0]);
m
}
PointDtoType::Food => {
let m = self.window.factory.mesh(sphere.clone(), green.clone());
m.set_position([state.x asf32, state.y asf32, 0.0]);
m
}
}
}).collect();
(meshes, self)
}
//Обновляем наше представлениеfnupdate(mutself) -> GameView {
//Количество времени прошедшее с последнего обновления игрыlet elapsed_time = self.window.input.delta_time();
let input = self.get_input();
let controller = self.controller.update(elapsed_time, input);
self.controller = controller;
self
}
//Отображаем наше представление игрокуfndraw(mutself) -> GameView {
let (meshes, view) = self.get_meshes();
self = view;
//Добавляем меши на сцену.for m in &meshes {
self.window.scene.add(m);
}
//Отображаем сцену на камеруself.window.render(&self.camera);
//Очищаем сценуfor m in meshes {
self.window.scene.remove(m);
}
//Отображаем пользователю текущий счетself.max_score.set_text(format!("MAX SCORE: {}", self.controller.get_max_score()));
self.current_score.set_text(format!("CURRENT SCORE: {}", self.controller.get_score()));
self
}
// Запускаем бесконечный цикл обновления и от рисовки игрыpubfnrun(mutself) {
//Прерываешь цикл есть окно игры закрылось или пользователь нажал клавишу эскейпwhileself.window.update() && !self.window.input.hit(three::KEY_ESCAPE) {
self = self.update().draw();
}
}
}
fnmain() {
letmut view = GameView::new();
view.run();
}
In addition to Three.rs, I also considered Piston - a set of libraries for creating games and Ametist - a game engine. I chose Three.rs because it seemed to me the simplest and most suitable for prototyping.
Unfortunately, within the framework of this game, it was not possible to touch the streams and work with the network. I will try it on the next project. So far I like the language and working with him is a pleasure. I would be grateful for practical advice and constructive criticism.