Space Invaders «с нуля» — Часть 3: создаём клон игры с минимумом зависимостей

В третьей части серии «Space Invaders с нуля» мы переходим от основ к геймплею: добавляем игрока и рои пришельцев, вводим анимацию спрайтов и делаем игровой цикл на фиксированном шаге времени с V-sync. Пошагово разбираем, как структурировать данные и оживить игровую сцену на C++.

88 открытий1К показов
Space Invaders «с нуля» — Часть 3: создаём клон игры с минимумом зависимостей

Это перевод статьи автора Nick Tasios. Мы уже публиковали перевод первой части с настройками окна и контекста и перевод второй части с настройкой шедеров и отрисовкой спрайта. В этой части создадим клон классической аркадной игры Space Invaders на C++, используя минимум зависимостей.

В этой части мы сделаем несколько ключевых шагов:

  • реализуем игровой цикл с фиксированным временным шагом,
  • добавим игрока и пришельцев,
  • и, наконец, добавим анимацию спрайтов.

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

Добавление игрока и роя пришельцев

Прежде чем добавить игрока и рой пришельцев, создадим два агрегата данных — то есть структуры (struct):

			struct Alien
{
    size_t x, y;
    uint8_t type;
};

struct Player
{
    size_t x, y;
    size_t life;
};
		

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

В классических аркадных играх Space Invaders существует три типа пришельцев, которые отличаются только своими спрайтами. Этот параметр мы кодируем в поле type.

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

			struct Game
{
    size_t width, height;
    size_t num_aliens;
    Alien* aliens;
    Player player;
};
		

Это включает ширину и высоту игры в пикселях, объект игрока и массив пришельцев, выделяемый динамически.

Как и ранее, мы добавляем спрайт для игрока, закодированный в виде растрового изображения (bitmap).

			Sprite player_sprite;
player_sprite.width = 11;
player_sprite.height = 7;
player_sprite.data = new uint8_t[77]
{
    0,0,0,0,0,1,0,0,0,0,0, // .....@.....
    0,0,0,0,1,1,1,0,0,0,0, // ....@@@....
    0,0,0,0,1,1,1,0,0,0,0, // ....@@@....
    0,1,1,1,1,1,1,1,1,1,0, // .@@@@@@@@@.
    1,1,1,1,1,1,1,1,1,1,1, // @@@@@@@@@@@
    1,1,1,1,1,1,1,1,1,1,1, // @@@@@@@@@@@
    1,1,1,1,1,1,1,1,1,1,1, // @@@@@@@@@@@
};
		

Затем мы создаём и инициализируем структуру Game.

			Game game;
game.width = buffer_width;
game.height = buffer_height;
game.num_aliens = 55;
game.aliens = new Alien[game.num_aliens];

game.player.x = 112 - 5;
game.player.y = 32;

game.player.life = 3;
		

Мы задаём количество пришельцев равное 55 — как в оригинальной аркадной игре, — даём игроку 3 жизни и размещаем его рядом с нижней центральной частью экрана. Затем инициализируем позиции пришельцев, выбрав для них разумные координаты.

			for(size_t yi = 0; yi < 5; ++yi)
{
    for(size_t xi = 0; xi < 11; ++xi)
    {
        game.aliens[yi * 11 + xi].x = 16 * xi + 20;
        game.aliens[yi * 11 + xi].y = 17 * yi + 128;
    }
}
		

Наконец, в основном игровом цикле мы отрисовываем игрока и всех пришельцев.

			for(size_t ai = 0; ai < game.num_aliens; ++ai)
{
    const Alien& alien = game.aliens[ai];
    buffer_draw_sprite(&buffer, alien_sprite,
        alien.x, alien.y, rgb_to_uint32(128, 0, 0));
}

buffer_draw_sprite(&buffer, player_sprite, game.player.x, game.player.y, rgb_to_uint32(128, 0, 0));
		

Получаем вот такой результат:

Space Invaders «с нуля» — Часть 3: создаём клон игры с минимумом зависимостей 1

Это уже начинает напоминать Space Invaders, но пока всё довольно статично!

Анимация спрайтов

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

			struct SpriteAnimation
{
    bool loop;
    size_t num_frames;
    size_t frame_duration;
    size_t time;
    Sprite** frames;
};
		

Структура SpriteAnimation по сути представляет собой массив Sprite. Здесь мы используем тип «указатель на указатель» для хранения спрайтов, чтобы их можно было шарить (совместно использовать). Если хочется повысить эффективность, спрайты можно упаковать в атласы (spritesheets). Кроме того, мы добавляем флаг, указывающий, нужно ли зацикливать анимацию или воспроизводить её лишь один раз, интервал между последовательными кадрами и время, проведённое в текущем экземпляре анимации. Зная число кадров и желаемую длительность анимации, интервал между кадрами легко вычислить. Ниже мы вводим дополнительный спрайт для нашего инопланетянина…

			Sprite alien_sprite1;
alien_sprite1.width = 11;
alien_sprite1.height = 8;
alien_sprite1.data = new uint8_t[88]
{
    0,0,1,0,0,0,0,0,1,0,0, // ..@.....@..
    1,0,0,1,0,0,0,1,0,0,1, // @..@...@..@
    1,0,1,1,1,1,1,1,1,0,1, // @.@@@@@@@.@
    1,1,1,0,1,1,1,0,1,1,1, // @@@.@@@.@@@
    1,1,1,1,1,1,1,1,1,1,1, // @@@@@@@@@@@
    0,1,1,1,1,1,1,1,1,1,0, // .@@@@@@@@@.
    0,0,1,0,0,0,0,0,1,0,0, // ..@.....@..
    0,1,0,0,0,0,0,0,0,1,0  // .@.......@.
};
		

…и создаём двухкадровую анимацию, используя два спрайта пришельца.

			SpriteAnimation* alien_animation = new SpriteAnimation;

alien_animation->loop = true;
alien_animation->num_frames = 2;
alien_animation->frame_duration = 10;
alien_animation->time = 0;

alien_animation->frames = new Sprite*[2];
alien_animation->frames[0] = &alien_sprite0;
alien_animation->frames[1] = &alien_sprite1;
		
Обратите внимание: чтобы упростить задачу, мы измеряем длительность кадра и время в игровых циклах — то есть в количестве итераций игрового цикла. Чтобы это работало корректно, нужно зафиксировать частоту кадров. Для простой игры, подобной нашей, можно использовать V-sync — опцию, при которой обновления видеокарты синхронизируются с частотой обновления монитора.

Большинство современных мониторов имеют частоту обновления 60 Гц, что означает обновление экрана 60 раз в секунду. Включив V-sync, мы получим частоту кадров 60 FPS или кратное значение, синхронизированное с частотой дисплея.

Однако есть и недостаток: на мониторах с более высокой частотой обновления, например, 120 Гц или 240 Гц, игра будет работать быстрее. Чтобы включить V-sync, вызывается функция GLFW:

			glfwSwapInterval(1)
		

В конце каждого кадра мы обновляем все анимации, увеличивая значение времени.

Если анимация дошла до конца, мы либо удаляем её, либо сбрасываем время обратно в 0, если это зацикленная анимация.

			++alien_animation->time;
if(alien_animation->time == alien_animation->num_frames * alien_animation->frame_duration)
{
    if(alien_animation->loop) alien_animation->time = 0;
    else
    {
        delete alien_animation;
        alien_animation = nullptr;
    }
}
		

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

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

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

			for(size_t ai = 0; ai < game.num_aliens; ++ai)
{
    const Alien& alien = game.aliens[ai];
    size_t current_frame = alien_animation->time / alien_animation->frame_duration;
    const Sprite& sprite = *alien_animation->frames[current_frame];
    buffer_draw_sprite(&buffer, sprite, alien.x, alien.y, rgb_to_uint32(128, 0, 0));
}
		

Чтобы сделать процесс более интересным, мы также добавляем простое движение игрока, введя переменную, которая управляет направлением его движения…

			int player_move_dir = 1;
		

…и обновляем положение игрока в конце каждого кадра в зависимости от значения этой переменной.

			if(game.player.x + player_sprite.width + player_move_dir >= game.width - 1)
{
    game.player.x = game.width - player_sprite.width - player_move_dir - 1;
    player_move_dir *= -1;
}
else if((int)game.player.x + player_move_dir <= 0)
{
    game.player.x = 0;
    player_move_dir *= -1;
}
else game.player.x += player_move_dir;
		

Условия if выполняют простую проверку столкновений спрайта игрока с границами игрового поля, гарантируя, что игрок остаётся внутри этих границ.

Выше вы можете видеть анимированный GIF с результатом.

Заключение

В этом посте мы создали несколько структур (struct), чтобы логически сгруппировать данные для игры, игрока и пришельцев. Ещё важнее то, что мы заложили основу для анимации спрайтов. Для этой простой версии Space Invaders мы используем фиксированные игровые циклы, задав постоянную частоту кадров с помощью включения вертикальной синхронизации (V-sync). Это позволяет нам выполнять анимацию спрайтов в игровых циклах, а не в реальном времени.

Единственное, что остаётся сделать, прежде чем игра станет играбельной, — реализовать обработку пользовательского ввода. Источников ввода может быть много, но в нашем случае мы ограничимся клавиатурой.

Следите за новыми постами
Следите за новыми постами по любимым темам
88 открытий1К показов