WebAssembly SDL canvas Arkanoid на C/C++
Table of contents
Introduction
В статье WebAssembly SDL Float Cube на C/C++ мы познакомились с возможностью графической отрисовки, анимацией и взаимодействием с нарисованными объектами. С помощью полученных знаний мы напишем простенькую игру под названием Arkanoid.
Репозиторий с полным кодом проекта: https://github.com/superrosko/wasm-game-sdl-canvas-arkanoid.
Npm package: https://www.npmjs.com/package/wasm-game-sdl-canvas-arkanoid.
Demo: https://pages.rdevelab.ru/wasm-game-sdl-canvas-arkanoid/index.html.
Структура проекта
.
├── build директория для сборки, которая игнорируется в .gitignore
├── distr директория с собранным проектом для npm-пакета
├── include директория для заголовочных файлов .h/.hpp
│ ├── ball.h заголовочный файл с определением функций, типов и переменных летающего мяча
│ ├── basic.h заголовочный файл с определением базовых типов и переменных
│ ├── border.h заголовочный файл с определением функций, типов и переменных границ игры
│ ├── bricks.h заголовочный файл с определением функций, типов и переменных кирпичиков
│ ├── core.h файл с определением функций, типов и переменных для отрисовки
│ └── field.h заголовочный файл с определением функций, типов и переменных поля
│ └── platform.h заголовочный файл с определением функций, типов и переменных платформы
├── public
│ └── index.html файл с демонстрацией работы нашего проекта в браузере
├── src исходные файлы проекта .c/.cpp
│ ├── ball.cpp код, отвечающий за отрисовку летающего мяча
│ ├── border.cpp код, отвечающий за отрисовку границ игры
│ ├── bricks.cpp код, отвечающий за отрисовку кирпичиков
│ ├── core.cpp точка входа с функцией main и циклами отрисовки
│ └── field.cpp код, отвечающий за отрисовку поля
│ └── platform.cpp код, отвечающий за отрисовку платформы
├── .gitattributes
├── .gitignore
├── CMakeLists.txt файл с командами cmake для сборки
├── LICENSE
├── README.md
├── build.sh файл, управляющий сборкой проекта
└── package.json описание npm-пакета
Вид игрового поля:
Для реализации игры создаем 5 объектов:
- поле, на котором происходят все действия игры
- границы поля, за которые мяч не должен вылетать, и должен отбиваться, а при соприкосновении с нижней границей будет происходить еще и уменьшение жизней
- платформа, с помощью которой будем отбивать летающий мяч
- сам летающий мяч, который будет отбиваться ото всех объектов, а при соприкосновении со статичными кирпичиками будет их уничтожать
- статичные кирпичики, которые нужно уничтожить с помощью летающего мяча, отбивая его платформой
Также реализуем вывод информации о заработанных очках и количестве жизней. А для управления создадим 3 кнопки: start
, pause
, restart
.
Исходный код проекта
Файлы basic.h
, field.h
, field.cpp
уже описывались в предыдущей статье и никак не изменились для реализации игры.
Начнем с файла, отвечающего за описание и отрисовку летающего мяча ball.h
/ball.cpp
:
struct Ball
{
int velocityX;
int velocityY;
SDL_Rect rect;
Color color;
};
const int BALL_SPAWN_LINE_WPADDING = 20;
const int BALL_SPAWN_LINE = 40;
const int BALL_WIDTH = 10;
const int BALL_HEIGHT = 10;
const int BALL_VELOCITY_X = 5;
const int BALL_VELOCITY_Y = 5;
const Color BALL_COLOR = {253, 21, 27, 255};
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setBallSize(int width, int height);
EMSCRIPTEN_KEEPALIVE
void setBallPosition(int x, int y);
EMSCRIPTEN_KEEPALIVE
void setBallColor(Color color);
EMSCRIPTEN_KEEPALIVE
void setRndBallPosition();
EMSCRIPTEN_KEEPALIVE
void setBallVelocity(int velocityX, int velocityY);
}
extern int getFieldWidth();
extern int getFieldHeight();
int getBallVerifiedPoint(int sizeField, int sizeBall, int point);
int getBallVelocityX();
int revBallVelocityX();
int getBallVelocityY();
int revBallVelocityY();
void setBallRect(SDL_Rect rect);
SDL_Rect getBallRect();
void renderBall(SDL_Renderer *renderer);
Ball *initBall();
Вначале мы определяем структуру Ball
с данными о летающем мяче - это структура SDL_Rect
с размерами и координатами мяча, структура цвета и две переменные, отвечающие за вектор скорости полета мяча.
Затем задаем значения по умолчанию для области, в которой будет рандомно спавниться мяч в начале каждой игры, размерам, вектору скорости и цвету мяча.
Далее мы описываем функции, которые потом реализуем или будем использовать в файле ball.cpp
:
setBallSize
- устанавливает размер мячаsetBallPosition
- устанавливает позицию мячаsetBallColor
- устанавливает цвет мячаsetRndBallPosition
- устанавливает рандомную позицию мяча в пределах указанной зоны спавнаsetBallVelocity
- устанавливает скорость полета мячаgetBallVerifiedPoint
- проверяет и корректирует координаты, в которые мы хотим переместить мяч, на предмет выхода за рамки игрового поляgetBallVelocityX
- возвращает текущий вектор скорости мяча по координатеX
revBallVelocityX
- реверсирует вектор скорости мяча по координатеX
getBallVelocityY
- возвращает текущий вектор скорости мяча по координатеY
revBallVelocityY
- реверсирует вектор скорости мяча по координатеY
setBallRect
- устанавливает структуру с размерами и координатами мячаgetBallRect
- возвращает структуру с размерами и координатами мячаrenderBall
- отрисовывает мячinitBall
- функция инициализации объекта мяча
#include "basic.h"
#include "ball.h"
Ball gBall;
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setBallSize(int width, int height)
{
gBall.rect.w = width;
gBall.rect.h = height;
}
EMSCRIPTEN_KEEPALIVE
void setBallPosition(int x, int y)
{
gBall.rect.x = getBallVerifiedPoint(getFieldWidth(), gBall.rect.w, x);
gBall.rect.y = getBallVerifiedPoint(getFieldHeight(), gBall.rect.h, y);
}
EMSCRIPTEN_KEEPALIVE
void setBallColor(Color color)
{
gBall.color = color;
}
EMSCRIPTEN_KEEPALIVE
void setRndBallPosition()
{
srand(time(NULL));
int x = rand() % (getFieldWidth() - BALL_SPAWN_LINE_WPADDING) + BALL_SPAWN_LINE_WPADDING / 2;
int y = rand() % BALL_SPAWN_LINE + getFieldHeight() / 2 - gBall.rect.h;
setBallPosition(x, y);
}
EMSCRIPTEN_KEEPALIVE
void setBallVelocity(int velocityX, int velocityY)
{
gBall.velocityX = velocityX;
gBall.velocityY = velocityY;
}
}
int getBallVerifiedPoint(int sizeField, int sizeBall, int point)
{
if (sizeField < (point + sizeBall))
{
return sizeField - sizeBall;
}
if (point < 0)
{
return 0;
}
return point;
}
int getBallVelocityX()
{
return gBall.velocityX;
}
int revBallVelocityX()
{
return gBall.velocityX *= -1;
}
int getBallVelocityY()
{
return gBall.velocityY;
}
int revBallVelocityY()
{
return gBall.velocityY *= -1;
}
void setBallRect(SDL_Rect rect)
{
gBall.rect = rect;
}
SDL_Rect getBallRect()
{
return gBall.rect;
}
void renderBall(SDL_Renderer *renderer)
{
SDL_SetRenderDrawColor(renderer, gBall.color.r, gBall.color.g, gBall.color.b, gBall.color.a);
SDL_RenderFillRect(renderer, &gBall.rect);
SDL_RenderPresent(renderer);
}
Ball *initBall()
{
setBallVelocity(BALL_VELOCITY_X, BALL_VELOCITY_Y);
setBallSize(BALL_WIDTH, BALL_HEIGHT);
setBallColor(BALL_COLOR);
setRndBallPosition();
return &gBall;
}
Глобальная переменная Ball gBall
хранит состояние мяча, а далее идет реализация описанных ранее функций.
Перейдем к коду границ игрового поля border.h
/border.cpp
, от которых будет отбиваться мяч, а касание нижней границы будет еще и уменьшать жизни:
struct Border
{
bool status;
SDL_Rect rect;
};
struct Borders
{
Border *border;
Color color;
};
const int BORDER_SIZE = 10;
const Color BORDER_COLOR = {181, 112, 61, 255};
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setBorderColor(Color color);
}
extern int getFieldWidth();
extern int getFieldHeight();
bool getBorderStatus(int number);
SDL_Rect getBorderRect(int number);
void renderBorders(SDL_Renderer *renderer);
Borders *initBorders();
Вначале мы определяем структуру Border
со статусом границы, который будет обозначать необходимость вычитать жизни при соприкосновении мяча с ней, и структурой SDL_Rect
с размерами и координатами границы.
Потом определяем структуру Borders
, в которой будем хранить цвет и указатель на массив со структурами границ.
Затем задаем значения по умолчанию для размера границ и их цвета.
Далее мы описываем функции, которые потом реализуем или будем использовать в файле border.cpp
:
setBorderColor
- устанавливает цвет границgetBorderStatus
- возвращает статус указанной границыgetBorderRect
- возвращает структуру с размерами и координатами указанной границыrenderBorders
- отрисовывает границыinitBorders
- функция инициализации объектов границ
#include "basic.h"
#include "border.h"
Borders gBorders;
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setBorderColor(Color color)
{
gBorders.color = color;
}
}
bool getBorderStatus(int number)
{
return gBorders.border[number].status;
}
SDL_Rect getBorderRect(int number)
{
return gBorders.border[number].rect;
}
void renderBorders(SDL_Renderer *renderer)
{
for (int i = 0; i < 4; i++)
{
SDL_SetRenderDrawColor(renderer, gBorders.color.r, gBorders.color.g, gBorders.color.b, gBorders.color.a);
SDL_RenderFillRect(renderer, &(gBorders.border[i].rect));
SDL_RenderPresent(renderer);
}
}
Borders *initBorders()
{
setBorderColor(BORDER_COLOR);
int fieldSizeW = getFieldWidth();
int fieldSizeH = getFieldHeight();
gBorders.border = new Border[4];
gBorders.border[0].rect = {0, 0, BORDER_SIZE, fieldSizeH};
gBorders.border[1].rect = {fieldSizeW - BORDER_SIZE, 0, BORDER_SIZE, fieldSizeH};
gBorders.border[2].rect = {0, 0, fieldSizeW, BORDER_SIZE};
gBorders.border[3].rect = {0, fieldSizeH - BORDER_SIZE, fieldSizeW, BORDER_SIZE};
gBorders.border[0].status = false;
gBorders.border[1].status = false;
gBorders.border[2].status = false;
gBorders.border[3].status = true;
return &gBorders;
}
Перейдем к коду кирпичиков bricks.h
/bricks.cpp
, от которых будет отбиваться мяч, а касание каждого из них мячом будет их уничтожать и прибавлять по одному очку за каждый:
struct Brick
{
bool status;
SDL_Rect rect;
};
struct Bricks
{
int rowCount;
int lineCount;
int width;
int height;
int countAllBricks;
int countActiveBricks;
Brick *brick;
Color color;
};
const int BRICKS_LINE_COUNT = 6;
const int BRICKS_ROWS = 10;
const int BRICKS_PADDING = 30;
const int BRICKS_SPACE = 12;
const int BRICKS_HEIGHT = 10;
const Color BRICKS_COLOR = {255, 179, 15, 255};
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setBricksCount(int rowCount, int lineCount);
EMSCRIPTEN_KEEPALIVE
void setBricksColor(Color color);
}
extern int getFieldWidth();
extern int getFieldHeight();
void setBricksSize();
void setBrickStatus(int number, bool status);
bool getBrickStatus(int number);
int getActiveBricks();
int incActiveBricks();
int decActiveBricks();
SDL_Rect getBrickRect(int number);
void renderBricks(SDL_Renderer *renderer);
Bricks *initBricks();
Вначале мы определяем структуру Brick
со статусом, который будет указывать на активность кирпичика (попал ли в него мяч или нет), и структурой SDL_Rect
с размерами и координатами кирпичика.
Потом определяем структуру Bricks
, в которой будем хранить значения количества кирпичиков в ряду, цвета, количества рядов, общее количество кирпичиков, количества активных кирпичиков, размеры и указатель на массив с их структурами.
Затем задаем значения по умолчанию для количества рядов, количества кирпичиков в каждом ряду, расстояния между кирпичиками и высоты кирпичиков.
Далее мы описываем функции, которые потом реализуем или будем использовать в файле bricks.cpp
:
setBricksCount
- устанавливает количество кирпичиковsetBricksColor
- устанавливает цвет кирпичиковsetBricksSize
- устанавливает размер кирпичиковsetBrickStatus
- устанавливает статус указанного кирпичикаgetBrickStatus
- возвращает статус указанного кирпичикаgetActiveBricks
- возвращает количество активных кирпичиковincActiveBricks
- увеличивает и возвращает количество активных кирпичиковdecActiveBricks
- уменьшает и возвращает количество активных кирпичиковgetBrickRect
- возвращает структуру с размерами и координатами указанного кирпичикаrenderBricks
- отрисовывает кирпичикиinitBricks
- функция инициализации объектов кирпичиков
#include "basic.h"
#include "bricks.h"
Bricks gBricks;
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setBricksCount(int rowCount, int lineCount)
{
gBricks.rowCount = rowCount;
gBricks.lineCount = lineCount;
}
EMSCRIPTEN_KEEPALIVE
void setBricksColor(Color color)
{
gBricks.color = color;
}
}
void setBricksSize()
{
gBricks.width = (int)((getFieldWidth() - 2 * BRICKS_PADDING + BRICKS_SPACE) / gBricks.lineCount) - BRICKS_SPACE;
gBricks.height = BRICKS_HEIGHT;
}
void setBrickStatus(int number, bool status)
{
gBricks.brick[number].status = status;
}
bool getBrickStatus(int number)
{
return gBricks.brick[number].status;
}
int getActiveBricks()
{
return gBricks.countActiveBricks;
}
int incActiveBricks()
{
gBricks.countActiveBricks++;
return gBricks.countActiveBricks;
}
int decActiveBricks()
{
gBricks.countActiveBricks--;
return gBricks.countActiveBricks;
}
SDL_Rect getBrickRect(int number)
{
return gBricks.brick[number].rect;
}
void renderBricks(SDL_Renderer *renderer)
{
for (int i = 0; i < gBricks.countAllBricks; i++)
{
if (getBrickStatus(i))
{
SDL_SetRenderDrawColor(renderer, gBricks.color.r, gBricks.color.g, gBricks.color.b, gBricks.color.a);
SDL_RenderFillRect(renderer, &(gBricks.brick[i].rect));
SDL_RenderPresent(renderer);
}
}
}
Bricks *initBricks()
{
gBricks.countAllBricks = BRICKS_LINE_COUNT * BRICKS_ROWS;
gBricks.countActiveBricks = gBricks.countAllBricks;
setBricksCount(BRICKS_ROWS, BRICKS_LINE_COUNT);
setBricksSize();
setBricksColor(BRICKS_COLOR);
gBricks.brick = new Brick[gBricks.countAllBricks];
for (int i = 0; i < BRICKS_ROWS; i++)
{
for (int j = 0; j < BRICKS_LINE_COUNT; j++)
{
SDL_Rect r;
r.x = BRICKS_PADDING + (BRICKS_SPACE + gBricks.width) * j;
r.y = BRICKS_PADDING + (BRICKS_SPACE + gBricks.height) * i;
r.w = gBricks.width;
r.h = gBricks.height;
gBricks.brick[i * BRICKS_LINE_COUNT + j].status = true;
gBricks.brick[i * BRICKS_LINE_COUNT + j].rect = r;
}
}
return &gBricks;
}
Перейдем к коду платформы platform.h
/platform.cpp
, которой мы будем управлять для того, чтобы отбивать мяч и не давать ему коснуться нижней границы:
struct Platform
{
SDL_Rect rect;
Color color;
};
const int PLATFORM_TOP_LINE = 50;
const int PLATFORM_WIDTH = 100;
const int PLATFORM_HEIGHT = 10;
const Color PLATFORM_COLOR = {150, 150, 150, 255};
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setPlatformSize(int width, int height);
EMSCRIPTEN_KEEPALIVE
void setPlatformPosition(int x, int y, bool fixX, bool fixY);
EMSCRIPTEN_KEEPALIVE
void setPlatformColor(Color color);
}
extern int getFieldWidth();
extern int getFieldHeight();
int getPlatformVerifiedPoint(int sizeField, int sizePlatform, int point);
SDL_Rect getPlatformRect();
void renderPlatform(SDL_Renderer *renderer);
Platform *initPlatform();
Вначале мы определяем структуру Platform
со структурой SDL_Rect
и цветом платформы.
Затем задаем значения по умолчанию для координат линии расположения, ширины, толщины и цвета платформы.
Далее мы описываем функции, которые потом реализуем или будем использовать в файле platform.cpp
:
setPlatformSize
- устанавливает размер платформыsetPlatformPosition
- устанавливает позицию платформыsetPlatformColor
- устанавливает цвет платформыgetPlatformVerifiedPoint
- проверяет и корректирует координаты, в которые мы хотим переместить платформу, на предмет выхода за рамки игрового поляgetPlatformRect
- возвращает структуру с размерами и координатами платформыrenderPlatform
- отрисовывает платформуinitPlatform
- функция инициализации объекта платформы
#include "basic.h"
#include "platform.h"
Platform gPlatform;
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void setPlatformSize(int width, int height)
{
gPlatform.rect.w = width;
gPlatform.rect.h = height;
}
EMSCRIPTEN_KEEPALIVE
void setPlatformPosition(int x, int y, bool fixX = true, bool fixY = true)
{
if (!fixX)
{
gPlatform.rect.x = getPlatformVerifiedPoint(getFieldWidth(), gPlatform.rect.w, x);
}
if (!fixY)
{
gPlatform.rect.y = getPlatformVerifiedPoint(getFieldHeight(), gPlatform.rect.h, y);
}
}
EMSCRIPTEN_KEEPALIVE
void setPlatformColor(Color color)
{
gPlatform.color = color;
}
}
int getPlatformVerifiedPoint(int sizeField, int sizePlatform, int point)
{
point -= sizePlatform / 2;
if (sizeField < (point + sizePlatform))
{
return sizeField - sizePlatform;
}
if (point < 0)
{
return 0;
}
return point;
}
SDL_Rect getPlatformRect()
{
return gPlatform.rect;
}
void renderPlatform(SDL_Renderer *renderer)
{
SDL_SetRenderDrawColor(renderer, gPlatform.color.r, gPlatform.color.g, gPlatform.color.b, gPlatform.color.a);
SDL_RenderFillRect(renderer, &gPlatform.rect);
SDL_RenderPresent(renderer);
}
Platform *initPlatform()
{
setPlatformSize(PLATFORM_WIDTH, PLATFORM_HEIGHT);
setPlatformColor(PLATFORM_COLOR);
setPlatformPosition(getFieldWidth() / 2, getFieldHeight() - PLATFORM_TOP_LINE, false, false);
return &gPlatform;
}
Итак, мы рассмотрели реализацию каждого объекта игрового поля и теперь нам необходимо описать саму игру, т.е. взаимодействие между объектами, за это отвечают файлы core.h
и core.cpp
:
struct Context
{
SDL_Window *window;
SDL_Renderer *renderer;
int countLives;
int countScore;
int gameActive;
};
const int GAME_LIVES_COUNT = 5;
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void gameRestart();
EMSCRIPTEN_KEEPALIVE
void gameStart();
EMSCRIPTEN_KEEPALIVE
void gameStop();
EMSCRIPTEN_KEEPALIVE
int getLivesCount();
EMSCRIPTEN_KEEPALIVE
int getScoreCount();
}
int valueInRange(int value, int min, int max);
int objectsOverlap(SDL_Rect *A, SDL_Rect *B);
bool checkObjectMoving(SDL_Rect *A, SDL_Rect *B);
void moveBall();
void mainloop(void *arg);
SDL_Renderer *getGContextRender();
void runLoop();
void renderWindow();
int decLivesCount();
void setLivesCount(int lives);
int incScoreCount();
void setScoreCount(int score);
bool getGameActive();
void setGameActive(bool status);
Вначале мы определяем структуру Context
, в которой будем хранить указатели на объекты для рендеринга и параметры игры, такие как жизни, набранные очки и статус игры (поставлена на паузу или нет).
Затем задаем значения по умолчанию для максимального количества начальных жизней. Далее мы описываем функции, которые потом реализуем или будем использовать в файле core.cpp
:
gameRestart
- функция перезапуска игры, которая будет вызываться по кнопке со страницыgameStart
- функция запуска игры с паузы, которая будет вызываться по кнопке со страницыgameStop
- функция приостановки игры, которая будет вызываться по кнопке со страницыgetLivesCount
- функция, возвращающая текущее количество жизней для вывода на страницуgetScoreCount
- функция, возвращающая текущее количество очков для вывода на страницуvalueInRange
- функция для определения вхождения значения в определенный промежутокobjectsOverlap
- функция, которая определяет было ли пересечение мяча с каким-то объектом или нетcheckObjectMoving
- функция, которая определяет новый вектор движения мяча, проверяя его пересечение с указанным объектомmoveBall
- основная логика игры, полета, отбития мяча посредством проверки пересечения объекта мяча с остальными объектамиmainloop
- основной цикл рендеринга объектов игрыgetGContextRender
- возвращает объект рендерингаrunLoop
- запуск цикла рендерингаrenderWindow
- отрисовка окна рендерингаdecLivesCount
- уменьшает количество жизнейsetLivesCount
- устанавливает количество жизней и вызывает внешнюю функциюwasmGameArkanoidJsSetLives
incScoreCount
- увеличивает количество очковsetScoreCount
- устанавливает количество очков и вызывает внешнюю функциюwasmGameArkanoidJsSetScore
getGameActive
- возвращает статус активности игрыsetGameActive
- устанавливает статус активности игры
#include "basic.h"
#include "ball.h"
#include "border.h"
#include "bricks.h"
#include "field.h"
#include "platform.h"
#include "core.h"
Context gContext;
int valueInRange(int value, int min, int max)
{
return (value >= min) && (value <= max);
}
int objectsOverlap(SDL_Rect *A, SDL_Rect *B)
{
int xOverlap =
valueInRange(A->x, B->x, B->x + B->w) || valueInRange(B->x, A->x, A->x + A->w);
int yOverlap =
valueInRange(A->y, B->y, B->y + B->h) || valueInRange(B->y, A->y, A->y + A->h);
return xOverlap && yOverlap;
}
bool checkObjectMoving(SDL_Rect *A, SDL_Rect *B)
{
SDL_Rect rBall = {};
if (objectsOverlap(A, B))
{
rBall = getBallRect();
rBall.x += getBallVelocityX();
if (objectsOverlap(&rBall, B))
{
revBallVelocityX();
}
rBall = getBallRect();
rBall.y += getBallVelocityY();
if (objectsOverlap(&rBall, B))
{
revBallVelocityY();
}
A->x = rBall.x + getBallVelocityX();
A->y = rBall.y + getBallVelocityY();
return true;
}
return false;
}
void moveBall()
{
SDL_Rect rBall = getBallRect();
rBall.x += getBallVelocityX();
rBall.y += getBallVelocityY();
SDL_Rect wo = getPlatformRect();
checkObjectMoving(&rBall, &wo);
for (int i = 0; i < 4; i++)
{
SDL_Rect wo = getBorderRect(i);
if (checkObjectMoving(&rBall, &wo) && getBorderStatus(i))
{
decLivesCount();
}
}
int count = BRICKS_LINE_COUNT * BRICKS_ROWS;
for (int i = 0; i < count; i++)
{
SDL_Rect wo = getBrickRect(i);
if (getBrickStatus(i))
{
if (checkObjectMoving(&rBall, &wo))
{
setBrickStatus(i, false);
incScoreCount();
decActiveBricks();
;
}
}
}
setBallRect(rBall);
}
void mainloop(void *arg)
{
SDL_Renderer *render = getGContextRender();
renderField(render);
renderBricks(render);
renderBall(render);
renderPlatform(render);
renderBorders(render);
if (getGameActive() && getActiveBricks() && getLivesCount())
{
moveBall();
}
}
SDL_Renderer *getGContextRender()
{
return gContext.renderer;
}
void runLoop()
{
const int simulate_infinite_loop = 1;
const int fps = -1;
emscripten_set_main_loop_arg(mainloop, &gContext, fps, simulate_infinite_loop);
}
void renderWindow()
{
SDL_Window *window;
SDL_Renderer *renderer;
SDL_CreateWindowAndRenderer(getFieldWidth(), getFieldHeight(), 0, &window, &renderer);
gContext.renderer = renderer;
gContext.window = window;
}
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void gameRestart()
{
setLivesCount(GAME_LIVES_COUNT);
initBall();
initBricks();
setGameActive(false);
setScoreCount(0);
}
EMSCRIPTEN_KEEPALIVE
void gameStart()
{
setGameActive(true);
}
EMSCRIPTEN_KEEPALIVE
void gameStop()
{
setGameActive(false);
}
EMSCRIPTEN_KEEPALIVE
int getLivesCount()
{
return gContext.countLives;
}
EMSCRIPTEN_KEEPALIVE
int getScoreCount()
{
return gContext.countScore;
}
}
int decLivesCount()
{
gContext.countLives--;
setLivesCount(gContext.countLives);
return gContext.countLives;
}
void setLivesCount(int lives)
{
gContext.countLives = lives;
char buffer[50];
sprintf(buffer, "wasmGameArkanoidJsSetLives(%d);", lives);
emscripten_run_script(buffer);
}
int incScoreCount()
{
gContext.countScore++;
setScoreCount(gContext.countScore);
return gContext.countScore;
}
void setScoreCount(int score)
{
gContext.countScore = score;
char buffer[50];
sprintf(buffer, "wasmGameArkanoidJsSetScore(%d);", score);
emscripten_run_script(buffer);
}
bool getGameActive()
{
return gContext.gameActive;
}
void setGameActive(bool status)
{
gContext.gameActive = status;
}
int main()
{
SDL_Init(SDL_INIT_VIDEO);
initField();
initPlatform();
gameRestart();
initBorders();
renderWindow();
runLoop();
SDL_DestroyRenderer(gContext.renderer);
SDL_DestroyWindow(gContext.window);
SDL_Quit();
return EXIT_SUCCESS;
}
Сборка проекта
Файлы (build.sh
и CMakeLists.txt
) для сборки практически не изменились: в CMakeLists.txt
поменялось название проекта, сменилась директория для собранных файлов на distr
и добавилась пара флагов:
MODULARIZE=1
- флаг, указывающий на необходимость собрать файл game_arkanoid.wasm.js
как модуль.
EXPORT_NAME=wasmGameArkanoid
- флаг, указывающий имя модуля.
Для сборки проекта нам необходимо выполнить build.sh
, после чего в директории distr
появятся файлы game_arkanoid.wasm.js
и game_arkanoid.wasm.wasm
.
Также собранные файлы будут продублированы в директорию public
с помощью добавленной в build.sh
команды:
find distr/ -type f -name "*.wasm.*" -exec cp {} public/ \;
После того, как мы собрали проект, нам необходимо посмотреть на его выполнение в web (браузере).
Запуск проекта в web
Для запуска через web добавим следующий код в файл ./public/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>WebAssembly Game Arkanoid!</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.row {
display: flex;
height: 100vh;
justify-content: center;
align-items: center;
}
canvas {
cursor: move !important;
width: 100%;
max-width: 400px;
}
</style>
</head>
<body>
<div class="row">
<div id="game_arkanoid">
<div class="info">
score : <span class="score">0</span>
lives: <span class="lives">0</span>
</div>
<canvas id="canvas"></canvas>
<div class="controls">
<button class="stop">stop</button>
<button class="start">start</button>
<button class="restart">restart</button>
</div>
</div>
</div>
<script src="game_arkanoid.wasm.js"></script>
<script>
wasmGameArkanoid['canvas'] = (function () {
return document.getElementById('canvas');
})();
wasmGameArkanoid(wasmGameArkanoid).then(function (Module) {
var game = document.getElementById('game_arkanoid');
var canvas = document.getElementById('canvas');
var score = game.querySelector('.score');
var lives = game.querySelector('.lives');
var stop = game.querySelector('.stop');
var start = game.querySelector('.start');
var restart = game.querySelector('.restart');
stop.addEventListener("click", function (evt) {
Module._gameStop();
});
start.addEventListener("click", function (evt) {
Module._gameStart();
});
restart.addEventListener("click", function (evt) {
Module._gameRestart();
});
canvas.addEventListener("mousemove", function (evt) {
var rect = this.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
Module._setPlatformPosition(x, y, false, true);
}, false);
canvas.addEventListener("touchmove", function (evt) {
var rect = this.getBoundingClientRect();
var x = evt.touches[0].clientX - rect.left;
var y = evt.touches[0].clientY - rect.top;
Module._setPlatformPosition(x, y, false, true);
}, false);
})
function wasmGameArkanoidJsSetLives(lives) {
document.querySelector('#game_arkanoid .lives').textContent = lives;
}
function wasmGameArkanoidJsSetScore(score) {
document.querySelector('#game_arkanoid .score').textContent = score;
}
</script>
</body>
</html>
У нас есть 3 кнопки, по клику на которые вызываются функции _gameStop
, _gameStart
, _gameRestart
, и объявлены 2 функции: wasmGameArkanoidJsSetLives
, wasmGameArkanoidJsSetScore
, которые вызываются при изменении количества жизней и очков.
Также события на mousemove
и touchmove
позволяют управлять платформой как движением мыши, так и с помощью свайпа на мобильных устройствах.
Перейдем в директорию ./public
и запустим в ней сервер:
cd ./public
http-server
Откроем в браузере адрес http://127.0.0.1:8080 и можем наслаждаться игрой Arkanoid!
Дата редактирования : 2020-11-12 00:39:31
Автор : Rosko