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

Практика SFML (функции)#

Игра «Жизнь» — это не типичная компьютерная игра. Это клеточный автомат, придуманный английским математиком Джоном Конвеем еще в 1970 году. Эта игра не требует игроков, но тем интереснее наблюдать за ее развитием, создавая начальное случайное состояние.
«Жизнь» состоит из сетки клеток, которые, основываясь на нескольких математических правилах, могут жить, умирать или размножаться. В зависимости от начальных условий клетки на протяжении всей игры образуют различные узоры.

Представьте себе двумерную вселенную, состоящую из клеток (как в школьной тетради). Некоторые клетки закрашены — тогда мы говорим, что в них есть жизнь. У каждой клетки есть 8 клеток-соседей

Введём некоторые правила для нашей жизни:

1) Каждая клетка, имеющая всего одного соседа или не имеющая ни единого, погибает «в одиночестве».

https://ratcatcher.ru/media/inf/pr/pr7/firstBlock-0.svg

2) Каждая клетка, y которой четыре и более соседей, погибает «от перенаселения».

https://ratcatcher.ru/media/inf/pr/pr7/firstBlock-1.svg

3) Каждая клетка, y которой два или три соседа, «выживает»

https://ratcatcher.ru/media/inf/pr/pr7/firstBlock-2.svg

4) Каждая ячейка c тремя соседями становится «заселенной».

https://ratcatcher.ru/media/inf/pr/pr7/secondBlock-0.svg

Cистема поражает своей непредсказуемостью. Клетки постоянно рождаются и умирают, количество живых клеток то возрастает, то убывает. Иногда через какое-то количество шагов поле остаётся пустым, иногда на нём остаются стабильные структуры, а порой нечто «живое» остаётся в игре очень долго или навсегда

https://ratcatcher.ru/media/inf/pr/pr7/1546083072127410608.gif

Также определим следующие правила окончания игры:
- на поле не останется ни одной «живой» клетки;
- конфигурация на очередном шаге в точности (без сдвигов и поворотов) повторит себя же на одном из более ранних шагов (складывается периодическая конфигурация)
- при очередном шаге ни одна из клеток не меняет своего состояния (частный случай предыдущего правила, складывается стабильная конфигурация)

Константы для создания мира#

Поскольку в прошоой практике было создано поле N x N подробно останавливаться на данном этапе не будет. Однако договоримся создать константы

// Константы для настройки мира
const unsigned int WORLD_WIDTH = 20;  // Ширина сетки
const unsigned int WORLD_HEIGHT = 20; // Высота сетки
const unsigned int CELL_SIZE = 30;    // Размер клетки в пикселях
const sf::Color LIVE_COLOR = sf::Color::White;  // Цвет "живых" клеток
const sf::Color DEAD_COLOR = sf::Color::Black;  // Цвет "мертвых" клеток
const sf::Color GRID_COLOR = sf::Color::Red;    // Цвет линии сетки

// Структура Point представляет клетку сетки, хранящую её статус
struct Point {
    bool is_live = false;  // Флаг, живет ли клетка
};

Инциализация main#

Для инциализации мира нам необходимо создать два массива (прошлый шаг и текущий шаг, чтобы проверить не попали ли мы в цикл)

Также необходимо проинциализировать текущий мир для этого создадим функцию initWorld

int main() {
    sf::RenderWindow window(sf::VideoMode(WORLD_WIDTH * CELL_SIZE, WORLD_HEIGHT * CELL_SIZE), "Game of Life");

    // Создаем двумерные массивы для текущего и предыдущего состояний мира с именем world и prevWorld (массивы хранят клетки! (Point!) 

    ДОБАВЬТЕ КОД МАССИВОВ СЮДА
    initWorld(world);  // Инициализируем мир 

     // Загрузка шрифта для вывода текста
    sf::Font font;
    if (!font.loadFromFile("arial.ttf")) {  // Путь к файлу шрифта
        return EXIT_FAILURE;
    }

    // Настройка текста для вывода статуса
    sf::Text statusText;
    statusText.setFont(font);
    statusText.setCharacterSize(24);
    statusText.setFillColor(sf::Color::Red);

Общая структура функции в C++#

Функции в C++ состоят из заголовка и тела:

<возвращаемый_тип> <имя_функции>(<параметры>) {
    // Тело функции
    return <значение>; // если возвращаемый тип не void
}

Компоненты функции
- Имя функции — идентификатор, по которому вызывается функция.
- Параметры — переменные, передаваемые в функцию.
- Тело функции — блок кода, выполняющий необходимые действия.
- Возвращаемое значение — результат, который возвращается из функции (если тип не void).

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

InitWorld#

Функция initWorld инициализирует сетку клеток случайным образом. Она принимает на вход вектор world по ссылке, что позволяет изменять оригинальный объект, не создавая копий.

void initWorld(std::vector<std::vector<Point>>& world) {
    std::random_device rd;
    std::mt19937 gen(rd());  // Генератор случайных чисел
    std::uniform_int_distribution<> dis(0, 1);  // Диапазон: 0 или 1

    for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
        for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
            world[y][x].is_live = dis(gen) == 1;  // Случайное присвоение статуса
        }
    }
}

класс std::mt19937 (начиная с C++11) является очень эффективным генератором псевдослучайных чисел и определяется в случайном заголовочном файле.
Класс uniform_int_distribution Формирует равномерное (каждое значение одинаково вероятно) распределение целых чисел в выходном диапазоне.

Продолжение main#

В отличие от многих других библиотек, в которых время представлено uint32 числом секунд, или дробным числом секунд, SFML не навязывает какую либо единицу или тип для значений времени. Вместо этого она оставляет этот выбор пользователю, предоставляя класс sf::Time. Все классы и функции, манипулирующие значениями времени, используют этот класс.

sf::Time представляет период времени (другими словами, время, прошедшее между двумя событиями). Это не класс даты и времени, который представляет текущий год/месяц/день/минуту/секунду как отметку времени, это просто значение, обозначающее количество времени и предоставляющее способ интерпретировать это значение в зависимости от контекта использования.

В SFML есть очень простой класс, предназначенный для измерения времени: sf::Clock. В этом классе есть только два метода: getElapsedTime, предназначенный для получения времени с момента последнего перезапуска, и restart, предназначенный для перезапуска часов.

int main() {
    sf::RenderWindow window(sf::VideoMode(WORLD_WIDTH * CELL_SIZE, WORLD_HEIGHT * CELL_SIZE), "Game of Life");

    // Создаем двумерные массивы для текущего и предыдущего состояний мира
    std::vector<std::vector<Point>> world(WORLD_HEIGHT, std::vector<Point>(WORLD_WIDTH));
    std::vector<std::vector<Point>> prevWorld(WORLD_HEIGHT, std::vector<Point>(WORLD_WIDTH));

    initWorld(world);  // Инициализируем мир

    // Загрузка шрифта для вывода текста
    sf::Font font;
    if (!font.loadFromFile("arial.ttf")) {  // Путь к файлу шрифта
        return EXIT_FAILURE;
    }

    // Настройка текста для вывода статуса
    sf::Text statusText;
    statusText.setFont(font);
    statusText.setCharacterSize(24);
    statusText.setFillColor(sf::Color::Red);

    sf::Clock clock;
    const sf::Time updateInterval = sf::seconds(0.5f);  // Интервал обновления в секундах

    bool optimalDetected = false;
    bool showGrid = false;  // Отображение сетки
    bool isPaused = false;  // Пауза

Переменные optimalDetected, showGrid, isPaused в данном коде являются флагами.

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

Цикл While и события#

Хотя полгон определение соыбтий будет дано в практиках, посвященных ООП. Дадим промежуточное опредление событиям.
Событие — это то, что может случиться с некоторым объектом при определённых условиях (например, с кнопкой при клике на неё мышкой).

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
            // Обработка нажатий клавиш
            else if (event.type == sf::Event::KeyPressed) {
                if (event.key.code == sf::Keyboard::G) {
                    showGrid = !showGrid;  // Переключение сетки
                }
                else if (event.key.code == sf::Keyboard::P) {
                    isPaused = !isPaused;  // Переключение паузы
                    if (isPaused) {
                        statusText.setString("Paused");
                    }
                }
                else if (event.key.code == sf::Keyboard::R) {
                    initWorld(world);  // Перезапуск игры
                    isPaused = false;
                    optimalDetected = false;
                    statusText.setString("");
                }
            }
        }

sf::Event event — это объект класса sf::Event из библиотеки SFML, который хранит информацию о происходящих в приложении событиях, таких как нажатия клавиш, перемещение мыши, закрытие окна и т.д. Этот объект используется для отслеживания и обработки событий в игровом цикле.

Класс sf::Event включает множество типов событий, таких как:

sf::Event::Closed — событие закрытия окна,
sf::Event::KeyPressed — событие нажатия клавиши на клавиатуре,
sf::Event::KeyReleased — событие отпускания клавиши,
sf::Event::MouseButtonPressed и sf::Event::MouseButtonReleased — события нажатия и отпускания кнопок мыши,

Функция updateWorld#

Функция updateWorld состоит из двух этапов :
1) подсчет количества живых соседей
2) принятие решения о дальнейшей судьбе клетки

void updateWorld(std::vector<std::vector<Point>>& world, const std::vector<std::vector<Point>>& prevWorld) {
    // Лямбда-функция для подсчета живых соседей вокруг клетки
    auto countLiveNeighbors = [&](unsigned int x, unsigned int y) -> unsigned int {
        unsigned int count = 0;
        for (int dy = -1; dy <= 1; ++dy) {
            for (int dx = -1; dx <= 1; ++dx) {
                if (dx == 0 && dy == 0) continue;  // Пропуск самой клетки
                int nx = x + dx;
                int ny = y + dy;
                if (nx >= 0 && ny >= 0 && nx < WORLD_WIDTH && ny < WORLD_HEIGHT && prevWorld[ny][nx].is_live) {
                    ++count;
                }
            }
        }
        return count;
        };

    // Применение правил "Жизни" к каждой клетке
    for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
        for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
            unsigned int liveNeighbors = countLiveNeighbors(x, y);
            if (prevWorld[y][x].is_live) {
                world[y][x].is_live = liveNeighbors == 2 || liveNeighbors == 3;  // Клетка выживает
            }
            else {
                world[y][x].is_live = liveNeighbors == 3;  // Новая клетка рождается
            }
        }
    }
}

Использование лямбда-функции для подсчета живых соседей в функции updateWorld имеет несколько преимуществ:
- Лямбда-функция countLiveNeighbors доступна только внутри updateWorld. Это удобно, поскольку она используется только в этом контексте и не нужна в других частях программы. Лямбда-функция позволяет держать код компактным и ограничить область видимости вспомогательной функции.
- Внедрив countLiveNeighbors как лямбда-функцию внутри updateWorld, мы можем легче понять, что ее задача — вычислить количество соседей для нужной клетки в процессе обновления мира. Это делает код более самодокументируемым и легче воспринимаемым для тех, кто его читает.

Функция worldsAreEqual#

Функция worldsAreEqual предназначена для сравнения двух двумерных массивов (миров) в игре "Жизнь". Она проверяет, являются ли состояния всех клеток в двух мирах одинаковыми.

// Проверка, идентичны ли два мира (нет изменений)
bool worldsAreEqual(const std::vector<std::vector<Point>>& w1, const std::vector<std::vector<Point>>& w2) {
    for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
        for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
            if (w1[y][x].is_live != w2[y][x].is_live) {
                return false;
            }
        }
    }
    return true;
}

Рендер окна#

Основы для данного фрагмента кода приведены в прошлых практиках

       // Отображение сетки, если включена
       if (showGrid) {
           sf::VertexArray gridLines(sf::Lines);

           for (unsigned int i = 0; i <= WORLD_WIDTH; ++i) {
               gridLines.append(sf::Vertex(sf::Vector2f(i * CELL_SIZE, 0), GRID_COLOR));
               gridLines.append(sf::Vertex(sf::Vector2f(i * CELL_SIZE, WORLD_HEIGHT * CELL_SIZE), GRID_COLOR));
           }

           for (unsigned int i = 0; i <= WORLD_HEIGHT; ++i) {
               gridLines.append(sf::Vertex(sf::Vector2f(0, i * CELL_SIZE), GRID_COLOR));
               gridLines.append(sf::Vertex(sf::Vector2f(WORLD_WIDTH * CELL_SIZE, i * CELL_SIZE), GRID_COLOR));
           }

           window.draw(gridLines);
       }

       // Отображение статуса (оптимальной конфигурации или паузы)
       if (optimalDetected || isPaused) {
           window.draw(statusText);
       }

       window.display();

Полннвй код программы (для самоконтроля)#

#include <SFML/Graphics.hpp>
#include <vector>
#include <random>
#include <string>

// Константы для настройки мира
const unsigned int WORLD_WIDTH = 20;  // Ширина сетки
const unsigned int WORLD_HEIGHT = 20; // Высота сетки
const unsigned int CELL_SIZE = 30;    // Размер клетки в пикселях
const sf::Color LIVE_COLOR = sf::Color::White;  // Цвет "живых" клеток
const sf::Color DEAD_COLOR = sf::Color::Black;  // Цвет "мертвых" клеток
const sf::Color GRID_COLOR = sf::Color::Red;    // Цвет линии сетки

// Структура Point представляет клетку сетки, хранящую её статус
struct Point {
    bool is_live = false;  // Флаг, живет ли клетка
};

// Функция инициализирует мир, случайно определяя статус каждой клетки
void initWorld(std::vector<std::vector<Point>>& world) {
    std::random_device rd;
    std::mt19937 gen(rd());  // Генератор случайных чисел
    std::uniform_int_distribution<> dis(0, 1);  // Диапазон: 0 или 1

    for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
        for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
            world[y][x].is_live = dis(gen) == 1;  // Случайное присвоение статуса
        }
    }
}

// Функция обновляет мир согласно правилам "Жизни"
void updateWorld(std::vector<std::vector<Point>>& world, const std::vector<std::vector<Point>>& prevWorld) {
    // Лямбда-функция для подсчета живых соседей вокруг клетки
    auto countLiveNeighbors = [&](unsigned int x, unsigned int y) -> unsigned int {
        unsigned int count = 0;
        for (int dy = -1; dy <= 1; ++dy) {
            for (int dx = -1; dx <= 1; ++dx) {
                if (dx == 0 && dy == 0) continue;  // Пропуск самой клетки
                int nx = x + dx;
                int ny = y + dy;
                if (nx >= 0 && ny >= 0 && nx < WORLD_WIDTH && ny < WORLD_HEIGHT && prevWorld[ny][nx].is_live) {
                    ++count;
                }
            }
        }
        return count;
        };

    // Применение правил "Жизни" к каждой клетке
    for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
        for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
            unsigned int liveNeighbors = countLiveNeighbors(x, y);
            if (prevWorld[y][x].is_live) {
                world[y][x].is_live = liveNeighbors == 2 || liveNeighbors == 3;  // Клетка выживает
            }
            else {
                world[y][x].is_live = liveNeighbors == 3;  // Новая клетка рождается
            }
        }
    }
}

// Проверка, идентичны ли два мира (нет изменений)
bool worldsAreEqual(const std::vector<std::vector<Point>>& w1, const std::vector<std::vector<Point>>& w2) {
    for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
        for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
            if (w1[y][x].is_live != w2[y][x].is_live) {
                return false;
            }
        }
    }
    return true;
}

// Основная функция, отвечающая за создание окна и игровой цикл
int main() {
    sf::RenderWindow window(sf::VideoMode(WORLD_WIDTH * CELL_SIZE, WORLD_HEIGHT * CELL_SIZE), "Game of Life");

    // Создаем двумерные массивы для текущего и предыдущего состояний мира
    std::vector<std::vector<Point>> world(WORLD_HEIGHT, std::vector<Point>(WORLD_WIDTH));
    std::vector<std::vector<Point>> prevWorld(WORLD_HEIGHT, std::vector<Point>(WORLD_WIDTH));

    initWorld(world);  // Инициализируем мир

    // Загрузка шрифта для вывода текста
    sf::Font font;
    if (!font.loadFromFile("arial.ttf")) {  // Путь к файлу шрифта
        return EXIT_FAILURE;
    }

    // Настройка текста для вывода статуса
    sf::Text statusText;
    statusText.setFont(font);
    statusText.setCharacterSize(24);
    statusText.setFillColor(sf::Color::Red);

    sf::Clock clock;
    const sf::Time updateInterval = sf::seconds(0.5f);  // Интервал обновления в секундах

    bool optimalDetected = false;
    bool showGrid = false;  // Отображение сетки
    bool isPaused = false;  // Пауза

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
            // Обработка нажатий клавиш
            else if (event.type == sf::Event::KeyPressed) {
                if (event.key.code == sf::Keyboard::G) {
                    showGrid = !showGrid;  // Переключение сетки
                }
                else if (event.key.code == sf::Keyboard::P) {
                    isPaused = !isPaused;  // Переключение паузы
                    if (isPaused) {
                        statusText.setString("Paused");
                    }
                }
                else if (event.key.code == sf::Keyboard::R) {
                    initWorld(world);  // Перезапуск игры
                    isPaused = false;
                    optimalDetected = false;
                    statusText.setString("");
                }
            }
        }

        // Если пауза не активна и интервал прошел, обновляем мир
        if (!isPaused && clock.getElapsedTime() >= updateInterval) {
            clock.restart();

            prevWorld = world;
            updateWorld(world, prevWorld);

            if (worldsAreEqual(world, prevWorld)) {
                statusText.setString("Optimal configuration detected");
                optimalDetected = true;
            }

            // Проверка на отсутствие живых клеток
            bool allDead = true;
            for (const auto& row : world) {
                for (const auto& cell : row) {
                    if (cell.is_live) {
                        allDead = false;
                        break;
                    }
                }
                if (!allDead) break;
            }

            if (allDead) {
                statusText.setString("All points died");
                optimalDetected = true;
            }
        }

        window.clear(sf::Color::Black);

        // Рисование клеток
        for (unsigned int y = 0; y < WORLD_HEIGHT; ++y) {
            for (unsigned int x = 0; x < WORLD_WIDTH; ++x) {
                sf::RectangleShape cell(sf::Vector2f(CELL_SIZE, CELL_SIZE));
                cell.setPosition(x * CELL_SIZE, y * CELL_SIZE);
                cell.setFillColor(world[y][x].is_live ? LIVE_COLOR : DEAD_COLOR);
                window.draw(cell);
            }
        }

        // Отображение сетки, если включена
        if (showGrid) {
            sf::VertexArray gridLines(sf::Lines);

            for (unsigned int i = 0; i <= WORLD_WIDTH; ++i) {
                gridLines.append(sf::Vertex(sf::Vector2f(i * CELL_SIZE, 0), GRID_COLOR));
                gridLines.append(sf::Vertex(sf::Vector2f(i * CELL_SIZE, WORLD_HEIGHT * CELL_SIZE), GRID_COLOR));
            }

            for (unsigned int i = 0; i <= WORLD_HEIGHT; ++i) {
                gridLines.append(sf::Vertex(sf::Vector2f(0, i * CELL_SIZE), GRID_COLOR));
                gridLines.append(sf::Vertex(sf::Vector2f(WORLD_WIDTH * CELL_SIZE, i * CELL_SIZE), GRID_COLOR));
            }

            window.draw(gridLines);
        }

        // Отображение статуса (оптимальной конфигурации или паузы)
        if (optimalDetected || isPaused) {
            window.draw(statusText);
        }

        window.display();
    }

    return 0;
}