Pull to refresh

Изучаю Rust: Как я игру «Змейка» сделал

Reading time10 min
Views9.2K
image

Недавно начал изучать язык программирования Rust и так как когда я изучаю новый язык я делаю на нем «Змейку» то решил сделать именно ее.

Для 3D графики использовалась библиотека Three.rs которая является портом библиотеки Three.js

Код
Скачать и поиграть

Скриншот игры
image

Код игры
/*Подключаем внешние библиотеки.
Также же в 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<Point>,
    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<Error>> {
        use std::fs::File;
        use std::io::Write;
        let score = ScoreRepository { score: value };
        //Сериализуем структуру в массив байтов с помощью библиотеки bincode
      // Оператор ? пробрасывает ошибку на верх т. е. если тут будет ошибка то она станет результатом метода
        let bytes: Vec<u8> = 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<usize, Box<Error>> {
        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<PointDto> {
        let mut 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 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<Direction>) -> 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(".\\DejaVuSans.ttf");
        //Создаем текст на экране куда будет записывать текущий и максимальный счет
        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(&current_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<Direction> {
        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<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 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();
}


Кроме Three.rs рассматривал еще Piston — набор библиотек для создания игр и Ametist — игровой движок. Выбрал Three.rs потому что он мне показался самым простым и больше всего подходящим для прототипирования.
К сожалению в рамках этой игры не удалось пощупать потоки и работу с сетью. Это уже буду пробовать на следующем проекте. Пока что язык мне нравиться и работать с ним одно удовольствие. Буду благодарен за дельные советы и конструктивную критику.
Tags:
Hubs:
+10
Comments16

Articles

Change theme settings