Перейти к содержанию

ООП SFML#

Для иллюстрации принципов ООП мы рассмотрим код игры "Змейка", где объекты, такие как змейка, еда и игровое поле, представлены классами.

Презентация

Описание игры#

Основные правила игры:
- Змейка движется по экрану в одном из четырех направлений: вверх, вниз, влево, вправо.
- Если змея съедает яблоко (красный квадрат), она увеличивается в длину.
- Игра заканчивается, если змея сталкивается с собой или с границами игрового поля.

Что необходимо реализовать:
- Логику движения змейки.
- Логику взаимодействия змейки с яблоками.
- Обработку столкновений с границами экрана и телом змейки.
- Обработку нажатий клавиш для изменения направления змейки.

Программа состоит из нескольких файлов:

Snake.h — заголовочный файл, в котором определяется структура змейки и ее поведение.
Snake.cpp — реализация методов, описанных в Snake.h, для управления змейкой.
Game.cpp — основной файл игры, где создается окно, обрабатываются события и выполняются обновления состояния игры.

Константы и параметры игры#

Для правильной работы игры нам нужно определить несколько базовых констант:

// Константы, определяющие размеры и параметры игры
const int WIDTH = 800;        // Ширина окна игры
const int HEIGHT = 600;       // Высота окна игры
const int SIZE = 20;          // Размер одного сегмента змейки и яблока
const int NUM_X = WIDTH / SIZE;  // Количество ячеек по горизонтали
const int NUM_Y = HEIGHT / SIZE; // Количество ячеек по вертикали
const float MOVE_INTERVAL = 0.1f; // Интервал между движениями змейки (в секундах)
enum Direction { UP, DOWN, LEFT, RIGHT };

Размеры экрана определяются в пикселях, размер каждого сегмента змейки установлен равным 20 пикселям. Количество ячеек по осям рассчитывается делением размера экрана на размер одного сегмента.

Для управления направлением змейки удобно использовать перечисления (enum). Перечисление позволяет задать набор значений для различных направлений.

Заголовоный файл#

Заголовочный файл (header) в языке программирования C++ — это механизм для организации кода и повторного использования.

Он содержит объявления функций, классов и переменных, позволяя разделять интерфейс и реализацию кода.

Защита от повторного включения заголовочных файлов#

Иногда может возникнуть ситуация, когда один заголовочный файл включён не один, а несколько раз. Посмотрим, к каким проблемам это может привести. Если в файле содержались только объявления, то ничего страшного не произойдёт (стол останется столом, сколько об этом не объявляй), но не всегда всё так удачно. Действительно, ведь заголовочный файл кроме объявлений файлов может содержать и определения (например констант или классов). Тогда его повторное включение приведет к ошибке компоновки. Чтобы этого избежать, все заголовочные файлы следует защищать от повторного включения.

https://ratcatcher.ru/media/inf/pr/pr8/с_p.png

Делается это так:

#ifndef 1_H
#define 1_H
void foo(int k);
#endif //1_H

Здесь директива #ifndef указывает препроцессору, что участок кода до #endif следует компилировать только в случае, если объявления 1_H не было. Директива #define же указывает, что 1_H следует объявить.

Допустим, что есть файл 1_1.cpp.

#include "1.h"
#include "1.h"
Что произойдет при препроцессинге этого файла?

Вместо каждого #include "1.h" будет подставлено содержимое соответствующего файла. На момент первой подстановки 1_H еще не определено, по этому произойдут подстановка объявления функции и объявление 1_H . На момент же, когда препроцессор перейдет ко второму включению, 1_H уже определено, и потому подстановка выполнена не будет.

https://ratcatcher.ru/media/inf/pr/pr8/build.png

Так как для совершение столь распространенного действия приходится писать целых три директивы, а к тому же следить за уникальностью объявляемых констант — была придумана директива #pragma once, которая, будучи помещенной в начало заголовочного файла , позволяет добиться того же результата. Однако пользоваться ей надо осторожно, так как в стандарт она не вошла, и потому поддерживается не всеми компиляторами (gcc и компилятор компании Microsoft — поддерживают).

Snake.h#

Для того чтобы избежать проблем с повторным подключением заголовочных файлов, используем директивы препроцессора #ifndef, #define и #endif:

#ifndef SNAKE_H
#define SNAKE_H

// Код для класса Snake

#endif // SNAKE_H

Змейка состоит из фрагментов, причем каждый фрагмент имеет положение по Х и У.

https://ratcatcher.ru/media/inf/pr/pr8/snake.png

Структура SnakeSegment представляет собой один сегмент змейки. Каждый сегмент имеет два основных параметра — координаты x и y, которые определяют его позицию на игровом поле.

Каждый сегмент будет иметь уникальные значения координат, что позволяет точно определять положение каждого фрагмента на экране. Важно, что голова змейки будет находиться в первом элементе списка, а хвост — в последнем.

Конструктор структуры инициализирует координаты сегмента при его создании. Таким образом, каждый новый сегмент получает уникальные значения x и y в момент добавления в цепочку змейки.

struct SnakeSegment {
    int x, y;
    SnakeSegment(int x, int y) : x(x), y(y) {}
};

Класс Snake#

Классы в C++ — это конструкции, которые позволяют объединять данные и методы, работающие с этими данными, в одном объекте. Классы позволяют инкапсулировать информацию, делая ее доступной только через публичные методы и скрывая детали реализации от пользователя. Это ключевая концепция объектно-ориентированного программирования (ООП).

В C++ ключевое слово class используется для определения класса. Классы обычно содержат атрибуты (данные) и методы (функции), которые работают с этими данными. Атрибуты и методы разделяются по уровням доступа: public и private.

Вот как можно разделить на атрибуты и методы в классе на примере класса Snake.

class Snake {
public:
    // Атрибуты (данные)
    std::vector<SnakeSegment> segments;  // Вектор сегментов змейки
    Direction direction;  // Направление змейки

    // Методы (функции)
    Snake();  // Конструктор класса
    void move();  // Метод для перемещения змейки
    void grow();  // Метод для роста змейки
    void changeDirection(Direction newDirection);  // Метод для смены направления
    std::vector<SnakeSegment> getSegments() const;  // Метод для получения сегментов змейки

private:
    std::vector<SnakeSegment> segments;
    Direction direction;
};

Атрибуты
segments — вектор, который хранит все сегменты змейки. Каждый сегмент — это объект типа SnakeSegment, который содержит координаты.
direction — переменная, которая хранит текущее направление змейки. Тип Direction представляет собой перечисление (enum), которое может иметь значения Up, Down, Left, Right и None (если змейка не движется).
Методы
Snake() — конструктор класса. Этот метод вызывается при создании объекта Snake и инициализирует начальные значения атрибутов, например, позицию начальных сегментов змейки и направление.
move() — метод, который обновляет положение змейки, перемещая каждый сегмент в новое положение, а голову змейки — в новое, основанное на текущем направлении.
grow() — метод для добавления нового сегмента в конец змейки.
changeDirection(Direction newDirection) — метод для изменения направления движения змейки.
getSegments() — метод, который возвращает все сегменты змейки в виде вектора, что позволяет, например, отрисовывать их на экране.

Snake.cpp#

Конструктор Snake::Snake():

#include "Snake.h"

Snake::Snake() {
    direction = RIGHT;
    segments.push_back(SnakeSegment(NUM_X / 2, NUM_Y / 2));
}

Конструктор инициализирует начальное состояние змейки.
Вектор segments заполняется одним сегментом, который размещается в центре поля, исходя из значений NUM_X и NUM_Y, которые определяют размеры игрового поля.
Направление змейки изначально установлено в RIGHT (вправо).

Метод Snake::grow():

Метод добавляет новый сегмент на место последнего сегмента змейки.
Новый сегмент инициализируется с теми же координатами, что и последний сегмент, таким образом, он будет следовать за ним.

void Snake::grow() {
    segments.push_back(SnakeSegment(segments.back().x, segments.back().y));
}

Метод Snake::changeDirection(Direction newDirection):

void Snake::changeDirection(Direction newDirection) {
    // Избегание обратного направления
    if ((direction == UP && newDirection != DOWN) ||
        (direction == DOWN && newDirection != UP) ||
        (direction == LEFT && newDirection != RIGHT) ||
        (direction == RIGHT && newDirection != LEFT)) {
        direction = newDirection;
    }
}

Этот метод используется для изменения направления движения змейки.
Важно, что сменить направление можно только на то, которое не является противоположным текущему. Например, если змейка движется вверх, она не может сразу изменить направление на вниз (это предотвратит самопересечение).

Метод Snake::getSegments():

std::vector<SnakeSegment> Snake::getSegments() const {
    return segments;
}

Этот метод возвращает вектор сегментов змейки.
С помощью этого метода можно получить все сегменты змейки, например, для их отрисовки на экране.

Логика nake::move()#

Метод Move() перемещает змейку в зависимости от текущего направления. Каждый сегмент змейки "переносится" на место предыдущего, создавая эффект движения.

https://ratcatcher.ru/media/inf/pr/pr8/snake_2.png

Иллюстрация: При движении змейки, каждый сегмент "следует" за предыдущим. На схеме показано, как сегменты движутся друг за другом в зависимости от направления:

https://ratcatcher.ru/media/inf/pr/pr8/snake_1.png

void Snake::move() {
    // Сдвиг позиций сегментов
    for (int i = segments.size() - 1; i > 0; --i) {
        segments[i].x = segments[i - 1].x;
        segments[i].y = segments[i - 1].y;
    }

    // Перемещение головы
    if (direction == UP) segments[0].y -= 1;
    if (direction == DOWN) segments[0].y += 1;
    if (direction == LEFT) segments[0].x -= 1;
    if (direction == RIGHT) segments[0].x += 1;

    // Проверка выхода за границы
    if (segments[0].x < 0) segments[0].x = NUM_X - 1;
    if (segments[0].x >= NUM_X) segments[0].x = 0;
    if (segments[0].y < 0) segments[0].y = NUM_Y - 1;
    if (segments[0].y >= NUM_Y) segments[0].y = 0;
}

Main.cpp#

#include <SFML/Graphics.hpp>
#include <cstdlib> // Для rand()
#include <ctime>   // Для time()
#include "Snake.h"

class Game {
public:
    Game() : window(sf::VideoMode(WIDTH, HEIGHT), "Snake Game"), snake(), clock() {
        food.setPosition(rand() % NUM_X * SIZE, rand() % NUM_Y * SIZE);
    }

    void run() {
        sf::Time timeSinceLastMove = sf::Time::Zero;

        while (window.isOpen()) {
            sf::Time deltaTime = clock.restart();
            timeSinceLastMove += deltaTime;

            handleEvents();
            if (timeSinceLastMove.asSeconds() >= MOVE_INTERVAL) {
                timeSinceLastMove = sf::Time::Zero;
                update();
            }
            render();
        }
    }

private:
    sf::RenderWindow window;
    Snake snake;
    sf::RectangleShape food;
    sf::Clock clock;

    void handleEvents() {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
            if (event.type == sf::Event::KeyPressed) {
                if (event.key.code == sf::Keyboard::W) snake.changeDirection(UP);
                if (event.key.code == sf::Keyboard::S) snake.changeDirection(DOWN);
                if (event.key.code == sf::Keyboard::A) snake.changeDirection(LEFT);
                if (event.key.code == sf::Keyboard::D) snake.changeDirection(RIGHT);
            }
        }
    }

    void update() {
        snake.move();

        // Проверка столкновения с едой
        if (snake.getSegments().front().x * SIZE == food.getPosition().x &&
            snake.getSegments().front().y * SIZE == food.getPosition().y) {
            snake.grow();
            food.setPosition(rand() % NUM_X * SIZE, rand() % NUM_Y * SIZE);
        }
    }

    void render() {
        window.clear();

        // Рисуем змейку
        for (const auto& segment : snake.getSegments()) {
            sf::RectangleShape rectangle(sf::Vector2f(SIZE, SIZE));
            rectangle.setPosition(segment.x * SIZE, segment.y * SIZE);
            window.draw(rectangle);
        }

        // Рисуем еду
        food.setSize(sf::Vector2f(SIZE, SIZE));
        food.setFillColor(sf::Color::Red);
        window.draw(food);

        window.display();
    }
};

int main() {
    srand(static_cast<unsigned>(time(0))); // Инициализация генератора случайных чисел
    Game game;
    game.run();
    return 0;
}

Итоговый проект#

Snake.h

#ifndef SNAKE_H
#define SNAKE_H

#include <vector>

// Константы
const int WIDTH = 800;
const int HEIGHT = 600;
const int SIZE = 20;
const int NUM_X = WIDTH / SIZE;
const int NUM_Y = HEIGHT / SIZE;
const float MOVE_INTERVAL = 0.1f; // Интервал движения в секундах (чем меньше, тем быстрее игра)

enum Direction { UP, DOWN, LEFT, RIGHT };

struct SnakeSegment {
    int x, y;
    SnakeSegment(int x, int y) : x(x), y(y) {}
};

class Snake {
public:
    Snake();
    void move();
    void grow();
    void changeDirection(Direction newDirection);
    std::vector<SnakeSegment> getSegments() const;

private:
    std::vector<SnakeSegment> segments;
    Direction direction;
};

#endif // SNAKE_H

Snake.сpp
#include "Snake.h"

Snake::Snake() {
    direction = RIGHT;
    segments.push_back(SnakeSegment(NUM_X / 2, NUM_Y / 2));
}

void Snake::move() {
    // Сдвиг позиций сегментов
    for (int i = segments.size() - 1; i > 0; --i) {
        segments[i].x = segments[i - 1].x;
        segments[i].y = segments[i - 1].y;
    }

    // Перемещение головы
    if (direction == UP) segments[0].y -= 1;
    if (direction == DOWN) segments[0].y += 1;
    if (direction == LEFT) segments[0].x -= 1;
    if (direction == RIGHT) segments[0].x += 1;

    // Проверка выхода за границы
    if (segments[0].x < 0) segments[0].x = NUM_X - 1;
    if (segments[0].x >= NUM_X) segments[0].x = 0;
    if (segments[0].y < 0) segments[0].y = NUM_Y - 1;
    if (segments[0].y >= NUM_Y) segments[0].y = 0;
}

void Snake::grow() {
    segments.push_back(SnakeSegment(segments.back().x, segments.back().y));
}

void Snake::changeDirection(Direction newDirection) {
    // Избегание обратного направления
    if ((direction == UP && newDirection != DOWN) ||
        (direction == DOWN && newDirection != UP) ||
        (direction == LEFT && newDirection != RIGHT) ||
        (direction == RIGHT && newDirection != LEFT)) {
        direction = newDirection;
    }
}

std::vector<SnakeSegment> Snake::getSegments() const {
    return segments;
}

Main.сpp

#include <SFML/Graphics.hpp>
#include <cstdlib> // Для rand()
#include <ctime>   // Для time()
#include "Snake.h"

class Game {
public:
    Game() : window(sf::VideoMode(WIDTH, HEIGHT), "Snake Game"), snake(), clock() {
        food.setPosition(rand() % NUM_X * SIZE, rand() % NUM_Y * SIZE);
    }

    void run() {
        sf::Time timeSinceLastMove = sf::Time::Zero;

        while (window.isOpen()) {
            sf::Time deltaTime = clock.restart();
            timeSinceLastMove += deltaTime;

            handleEvents();
            if (timeSinceLastMove.asSeconds() >= MOVE_INTERVAL) {
                timeSinceLastMove = sf::Time::Zero;
                update();
            }
            render();
        }
    }

private:
    sf::RenderWindow window;
    Snake snake;
    sf::RectangleShape food;
    sf::Clock clock;

    void handleEvents() {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
            if (event.type == sf::Event::KeyPressed) {
                if (event.key.code == sf::Keyboard::W) snake.changeDirection(UP);
                if (event.key.code == sf::Keyboard::S) snake.changeDirection(DOWN);
                if (event.key.code == sf::Keyboard::A) snake.changeDirection(LEFT);
                if (event.key.code == sf::Keyboard::D) snake.changeDirection(RIGHT);
            }
        }
    }

    void update() {
        snake.move();

        // Проверка столкновения с едой
        if (snake.getSegments().front().x * SIZE == food.getPosition().x &&
            snake.getSegments().front().y * SIZE == food.getPosition().y) {
            snake.grow();
            food.setPosition(rand() % NUM_X * SIZE, rand() % NUM_Y * SIZE);
        }
    }

    void render() {
        window.clear();

        // Рисуем змейку
        for (const auto& segment : snake.getSegments()) {
            sf::RectangleShape rectangle(sf::Vector2f(SIZE, SIZE));
            rectangle.setPosition(segment.x * SIZE, segment.y * SIZE);
            window.draw(rectangle);
        }

        // Рисуем еду
        food.setSize(sf::Vector2f(SIZE, SIZE));
        food.setFillColor(sf::Color::Red);
        window.draw(food);

        window.display();
    }
};

int main() {
    srand(static_cast<unsigned>(time(0))); // Инициализация генератора случайных чисел
    Game game;
    game.run();
    return 0;
}

Представление в виде диаграммы#

Диаграмма классов предназначена для представления внутренней структуры программы в виде классов и связей между ними.

Все сущности реального мира, с которыми собирается работать программист, должны быть представлены объектами классов в программе.
При этом у каждого класса должно быть только одно назначение и уникально осмысленное имя, которое будет связано с этой целью.

https://ratcatcher.ru/media/inf/pr/pr8/UML.png