[recovery mode] Изучаю Rust: Как я игру «Змейка» сделал
Недавно начал изучать язык программирования Rust и так как когда я изучаю новый язык я делаю на нем «Змейку» то решил сделать именно ее.
Для 3D графики использовалась библиотека Three.rs которая является портом библиотеки Three.js
→ Код
→ Скачать и поиграть
Скриншот игры
/*Подключаем внешние библиотеки.
Также же в Cargo.toml
[dependencies]
rand="*"
three="*"
serde="*"
bincode="*"
serde_derive="*"
прописываем
*/
extern crate rand;
extern crate three;
extern crate bincode;
extern crate serde;
#[macro_use]
extern crate serde_derive;
// Добавляем нужные нам вещи в нашу область видимости.
use rand::Rng;
use three::*;
use std::error::Error;
//Entities ------------------------------------------------------------------
/*
Это макросы. Они генерируют какой ни будь код автоматически.
В нашем конкретном случае:
Debug - Создаст код который позволить выводить нашу структуру в терминал
Clone - Создаст код который будет копировать нашу структуру т. е. у нашей структуры появиться метод clone()
Eq и PartialEq позволять сравнивать наши Point с помощью оператора ==
*/
#[derive(Debug, Clone, Eq, PartialEq, Default)]
//Обьявление структуры с двумя полями. Она будет играть роль точки
struct Point {
x: u8,
y: u8,
}
//Методы нашей структуры
impl Point {
// Можно было использовать просто оператор == В общем, это метод который проверяет пересекаются ли наши точки
pub fn intersects(&self, point: &Point) -> bool {
self.x == point.x && self.y == point.y
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
//Эта структура будет хранить объектное представление границ фрейма в пределах которого будет двигаться наша змейка
struct Frame {
min_x: u8,
min_y: u8,
max_x: u8,
max_y: u8,
}
impl Frame {
pub fn intersects(&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 вариантами
//Оно будет отвечать за то куда в данный момент повернута голова змейки
enum Direction {
Left,
Right,
Top,
Bottom,
}
//Реализация трейта (в других языках это еще называется интерфейс)
// для нашего перечисления.
//Обьект реализующий этот трейт способен иметь значение по умолчанию.
impl Default for Direction {
fn default() -> Direction {
return Direction::Right;
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
//Собственно наша змейка
struct Snake {
direction: Direction,
points: std::collections::VecDeque,
start_x: u8,
start_y: u8,
}
impl Snake {
//Статический метод конструктор для инициализации нового экземпляра нашей змейки
pub fn new(x: u8, y: u8) -> Snake {
let mut points = std::collections::VecDeque::new();
for i in 0..3 {
points.push_front(Point { x: x + i, y: i + y });
}
Snake { direction: Direction::default(), points, start_x: x, start_y: y }
}
//Увеличивает длину нашей змейки на одну точку
pub fn grow(mut self) -> Snake {
if let Some(tail) = self.points.pop_back() {
self.points.push_back(Point { x: tail.x, y: tail.y });
self.points.push_back(tail);
}
self
}
//Сбрасывает нашу змейку в начальное состояние
pub fn reset(self) -> Snake {
Snake::new(self.start_x, self.start_y)
}
//Поворачивает голову змейки в нужном нам направлении
pub fn turn(mut self, direction: Direction) -> Snake {
self.direction = direction;
self
}
//Если голова змейки достает до еды то увеличивает длину змейки на один и возвращает информацию о том была ли еда съедена
pub fn try_eat(mut self, point: &Point) -> (Snake, bool) {
let head = self.head();
if head.intersects(point) {
return (self.grow(), true);
}
(self, false)
}
//Если голова змейки столкнулась с фреймом то возвращает змейку в начальное состояние
pub fn try_intersect_frame(mut self, frame: &Frame) -> Snake {
let head = self.head();
if frame.intersects(&head) {
return self.reset();
}
self
}
//Если голова змейки столкнулась с остальной частью то возвращает змейку в начальное состояние.
pub fn try_intersect_tail(mut self) -> Snake {
let head = self.head();
let p = self.points.clone();
let points = p.into_iter().filter(|p| head.intersects(p));
if points.count() > 1 {
return self.reset();
}
self
}
//Дает голову змейки
pub fn head(&self) -> Point {
self.points.front().unwrap().clone()
}
//Перемещает змейку на одну точку в том направление куда в данный момент смотрит голова змейки
pub fn move_snake(mut self) -> Snake {
if let Some(mut tail) = self.points.pop_back() {
let head = self.head();
match self.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)]
//Структура для создания новой еды для змейки
struct FoodGenerator {
frame: Frame
}
impl FoodGenerator {
//Создает новую точку в случайном месте в пределах фрейма
pub fn generate(&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)]
//Хранит текущий и максимальный счет игры
struct ScoreRepository {
score: usize
}
impl ScoreRepository {
//Статический метод для сохранения текущего счета в файле
// Result это перечисление которое может хранить в себе либо ошибку либо результат вычислений
fn save(value: usize) -> Result<(), Box> {
use std::fs::File;
use std::io::Write;
let score = ScoreRepository { score: value };
//Сериализуем структуру в массив байтов с помощью библиотеки bincode
let bytes: Vec = bincode::serialize(&score)?;
//Создаем новый файл или если он уже сушествует то перезаписываем его.
let mut file = File::create(".\\score.data")?;
match file.write_all(&bytes) {
Ok(t) => Ok(t),
//Error это трейт а у трейт нет точного размера во время компиляции поэтому
// нам надо обернуть значение в Box и в результате мы работает с указателем на
//кучу в памяти где лежит наш объект а не с самим объектом а у указателя есть определенный размер
// известный во время компиляции
Err(e) => Err(Box::new(e))
}
}
//Загружаем сохраненный результат из файла
fn load() -> Result> {
use std::fs::File;
let mut file = File::open("./score.data")?;
let data: ScoreRepository = bincode::deserialize_from(file)?;
Ok(data.score)
}
}
//Business Logic Layer------------------------------------------------------------
#[derive(Debug, Clone, Default)]
//Объектное представление логики нашей игры
struct Game {
snake: Snake,
frame: Frame,
food: Point,
food_generator: FoodGenerator,
score: usize,
max_score: usize,
total_time: f32,
}
impl Game {
//Конструктор для создания игры с фреймом заданной высоты и ширины
fn new(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,
}
}
// Проверяем, прошло ли достаточно времени с момента когда мы в последний раз
//двигали нашу змейку и если да то передвигаем ее
// и проверяем столкновение головы змейки с остальными объектами игры
// иначе ничего не делаем
fn update(mut self, 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
}
}
//Проверяем, настало ли время для того чтобы передвинуть змейку.
fn is_time_to_move(mut self, time_delta_in_seconds: f32) -> (Game, bool) {
let time_to_move: f32 = 0.030;
self.total_time += time_delta_in_seconds;
if self.total_time > time_to_move {
self.total_time -= time_to_move;
(self, true)
} else {
(self, false)
}
}
//Проверяем, съела ли наша змейку еду и если да
// то создаем новую еду, начисляем игроку очки
// иначе сбрасываем игроку текущий счет
fn try_eat(mut self) -> Game {
let initial_snake_len = 3;
if self.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;
if self.max_score < self.score {
self.max_score = self.score;
ScoreRepository::save(self.max_score);
}
};
self
}
// Поворачиваем змейку в нужном направлении
fn handle_input(mut self, input: Direction) -> Game {
let snake = self.snake.turn(input);
self.snake = snake;
self
}
}
//Application Layer--------------------------------------------------------------
// --- Model ----
#[derive(Debug, Clone, Eq, PartialEq)]
enum PointDtoType {
Head,
Tail,
Food,
Frame,
}
impl Default for PointDtoType {
fn default() -> PointDtoType {
PointDtoType::Frame
}
}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
//Модель котору будет видеть представление для отображения пользователю.
struct PointDto {
x: u8,
y: u8,
state_type: PointDtoType,
}
//------------------------------Controller -----------------------------
#[derive(Debug, Clone, Default)]
// Контроллер который будет посредником между представлением и логикой нашей игры
struct GameController {
game: Game,
}
impl GameController {
fn new() -> GameController {
GameController { game: Game::new(30, 30) }
}
//Получить коллекцию точек которые нужно от рисовать в данный момент
fn get_state(&self) -> Vec {
let mut vec: Vec = 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 in self.game.snake.points.iter().filter(|p| **p != head) {
vec.push(PointDto { x: p.x, y: p.y, state_type: PointDtoType::Tail });
}
//горизонтальные линии фрейма
for x in self.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 in self.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
}
//Обновляем состояние игры
fn update(mut self, time_delta: f32, direction: Option) -> GameController {
let game = self.game.clone();
self.game = match direction {
None => game,
Some(d) => game.handle_input(d)
}
.update(time_delta);
self
}
pub fn get_max_score(&self) -> usize {
self.game.max_score.clone()
}
pub fn get_score(&self) -> usize {
self.game.score.clone()
}
}
//------------------------View ---------------
//Представление для отображение игры для пользователю и получение от него команд
struct GameView {
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 {
fn new() -> GameView {
let controller = GameController::new();
//Создаем окно в котором будет отображаться наша игра
let mut 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);
//Создаем направленный свет
let mut 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(format!("{}/DejaVuSans.ttf", env!("CARGO_MANIFEST_DIR")));
//Создаем текст на экране куда будет записывать текущий и максимальный счет
let current_score = window.factory.ui_text(&font, "0");
let mut 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 }
}
//Считываем клавишу которую последней нажал пользователь и на основании ее выбираем новое направление
fn get_input(&self) -> Option {
match self.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,
}
}
}
//Преобразуем модель полученную от контроллера в набор сеточных объектов нашей сцены
fn get_meshes(mut self) -> (Vec, 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 as f32, state.y as f32, 0.0]);
m
}
PointDtoType::Tail => {
let m = self.window.factory.mesh(sphere.clone(), yellow.clone());
m.set_position([state.x as f32, state.y as f32, 0.0]);
m
}
PointDtoType::Head => {
let m = self.window.factory.mesh(sphere.clone(), red.clone());
m.set_position([state.x as f32, state.y as f32, 0.0]);
m
}
PointDtoType::Food => {
let m = self.window.factory.mesh(sphere.clone(), green.clone());
m.set_position([state.x as f32, state.y as f32, 0.0]);
m
}
}
}).collect();
(meshes, self)
}
//Обновляем наше представление
fn update(mut self) -> 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
}
//Отображаем наше представление игроку
fn draw(mut self) -> 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
}
// Запускаем бесконечный цикл обновления и от рисовки игры
pub fn run(mut self) {
while self.window.update() && !self.window.input.hit(three::KEY_ESCAPE) {
self = self.update().draw();
}
}
}
fn main() {
let mut view = GameView::new();
view.run();
}
К сожалению в рамках этой игры не удалось пощупать потоки и работу с сетью. Это уже буду пробовать на следующем проекте. Пока что язык мне нравиться и работать с ним одно удовольствие. Буду благодарен за дельные советы и конструктивную критику.