Cocos2d-x тестирование производительности
У меня есть заготовки для top-down rts. Проведем несколько тестов, чтобы узнать на что способен движок и сложно ли с ним работать.
Установка тривиальна, рассматривать ее я здесь не буду.
Бойлерплейт проект создается с помощью консольной утилиты cocos, что довольно удобно:
cocos new MyGame -p com.your_company.mygame -l cpp -d NEW_PROJECTS_DIR
cd NEW_PROJECTS_DIR/MyGame
// стандартная команда создает нерабочую 64х версию,
// следует воспользоваться cmake:
cmake . -A win32
Далее можно открыть созданный файл MyGame.sln в VisualStudio и выполнить сборку тестового проекта, выбрав MyGame вместо ALL_BUILD.
Что внутри
До этого пробовал javascript движки phaser, pixijs. Тут все очень похоже: Сцены, спрайты. Действия спрайтов: движение, поворот выполняются через отдельный класс Actions. Есть библиотека для текстов, кнопок. Есть автоматическая чистка объектов динамической памяти, поэтому создание объектов рекомендуется делать через методы движка.
Добавим звуки, спрайты и тайловые карты
Подключим звуковые файлы.
Ассеты нужно сначала положить в папку ./Resources. Все файлы из папки ./Resources подгружаются автоматический при сборке, можно сразу проиграть файл:
AudioEngine::play2d("audio/gruntnogold1.mp3");
Теперь добавим тайлмапу:
auto map = TMXTiledMap::create("strategy_map.tmx");
addChild(map, 0, 99);
Добавляется без проблем: слои, тайлсеты и картинки цепляются автоматом.
Не поддерживается json файлы только xml. И только один тайлсет на слой.
Рендер тайлсета
Тайлсет
Добавим перемещение карты с помощью клавиатуры:
void HelloWorld::onKeyPressed(EventKeyboard::KeyCode keyCode, Event* event)
{
log("Key with keycode %d pressed", keyCode);
bool isMoveScreenPressed = false;
switch (keyCode)
{
case cocos2d::EventKeyboard::KeyCode::KEY_LEFT_ARROW:
this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_LEFT_ARROW] = true;
isMoveScreenPressed = true;
break;
case cocos2d::EventKeyboard::KeyCode::KEY_RIGHT_ARROW:
this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_RIGHT_ARROW] = true;
isMoveScreenPressed = true;
break;
case cocos2d::EventKeyboard::KeyCode::KEY_UP_ARROW:
this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_UP_ARROW] = true;
isMoveScreenPressed = true;
break;
case cocos2d::EventKeyboard::KeyCode::KEY_DOWN_ARROW:
this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_DOWN_ARROW] = true;
isMoveScreenPressed = true;
break;
}
if (isMoveScreenPressed == true && this->isScroll == false) {
std::thread thr(&HelloWorld::startScroll, this);
thr.detach();
}
}
void HelloWorld::onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event)
{
log("Key with keycode %d released", keyCode);
this->keyPressed[keyCode] = false;
if (
this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_DOWN_ARROW] == false
&& this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_UP_ARROW] == false
&& this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_LEFT_ARROW] == false
&& this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_RIGHT_ARROW] == false) {
this->isScroll = false;
}
}
void HelloWorld::startScroll() {
if (this->isScroll == false) {
this->isScroll = true;
while (this->isScroll == true) {
this->moveScreen();
std::this_thread::sleep_for(std::chrono::milliseconds(SCROLL_INTERVAL));
}
}
}
void HelloWorld::moveScreen() {
float x, y;
float shift = 100.00;
if (this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_LEFT_ARROW] == true) {
x = this->getPositionX();
log("move map left %f", x);
if (x < 0) {
this->setPositionX(x + shift);
}
}
if (this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_RIGHT_ARROW] == true) {
x = this->getPositionX();
log("move map right %f", x);
if (x > -(this->mapWidth)) {
log("set pos %f", x - shift);
this->setPositionX(x - shift);
}
}
if (this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_UP_ARROW] == true) {
y = this->getPositionY();
log("move map up %f", y);
if (y > -(600 + this->mapHeight)) {
this->setPositionY(y - shift);
}
}
if (this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_DOWN_ARROW] == true) {
y = this->getPositionY();
log("move map down %f", y);
if (y < 0) {
this->setPositionY(y + shift);
}
}
}
Перемещение по карте
Перемещение по карте
Добавим спрайт на карту. Спрайту можно присвоить имя, чтобы в последующем можно было выбрать его из общего массива:
// Создаем текстуру
auto texture = Director::getInstance()->getTextureCache()->addImage("./images/MiniWorldSprites/Characters/Monsters/Orcs/FarmerGoblin.png");
// Вырезаем фрейм
auto frame = SpriteFrame::createWithTexture(texture, Rect(0, 0, 32, 32));
// создаем спрайт
auto sprite = Sprite::createWithSpriteFrame(frame);
sprite->setPosition(Vec2(START_X, START_Y));
sprite->setName("playerUnit");
// добавляем на сцену
this->addChild(sprite, 1);
Далее нужно выбрать спрайт по щелчку и передвинуть по-следующему. Для этого есть встроенная функция moveTo (). Все объекты которые добавляются на сцену, попадают в общий массив Vector
void HelloWorld::onMouseDown(Event* event)
{
EventMouse* e = (EventMouse*)event;
if (e->getMouseButton() == EventMouse::MouseButton::BUTTON_LEFT) {
float x = e->getCursorX() - this->getPositionX();
float y = e->getCursorY() - this->getPositionY();
auto children = this->getChildren();
// перебираем все объекты на сцене
for (auto child : children) {
if (child->getName() == "playerUnit") {
if (child->getBoundingBox().containsPoint(Vec2(x, y))) {
// Если спрайт в движении, ничего не делаем
if (child->getActionByTag(1) != nullptr) {
return;
}
// удаляем рамку, если она уже есть
if (!this->selectedUnits.empty()) {
this->removeSelections();
}
AudioEngine::play2d("audio/gruntnogold1.mp3");
const Vec2& pos = child->getPosition();
const Vec2& anchor = child->getAnchorPoint();
// рисуем новую рамку
DrawNode* node = DrawNode::create();
// устанавливаем размер 32х32
node->setContentSize(cocos2d::Size(32.00, 32.00));
node->setAnchorPoint(anchor);
cocos2d::Color4F white(1, 1, 1, 1);
node->drawRect(Vec2(0, 0), Vec2(32, 0), Vec2(32, 32), Vec2(0, 32), white);
node->setPosition(pos);
node->setName("selection");
this->addChild(node, 2);
this->selectedUnits.push_back(child);
this->selectedUnits.push_back(node);
break;
}
else {
// если есть рамка - это второй сценарий, перемещения
if (this->getChildByName("selection") != nullptr) {
for (auto unit : this->selectedUnits) {
this->moveUnitTo(unit, x, y);
}
}
}
}
}
}
}
void HelloWorld::removeSelections() {
this->removeChildByName("selection");
this->selectedUnits.clear();
}
void HelloWorld::moveUnitTo(Node* node, float x, float y) {
// если спрайт уже в движении, отменим задание
if (node->getActionByTag(1) != nullptr) {
node->stopActionByTag(1);
}
Vec2 currPos = node->getPosition();
float distance = currPos.distance(Vec2(x, y));
auto calcAngle = [](Vec2 pos1, Vec2 pos2) { return atan2((pos2.y - pos1.y), (pos2.x - pos1.x)); };
float angle = calcAngle(currPos, Vec2(x, y));
float moveTime = distance / 100.00;
auto moveTo = MoveTo::create(moveTime, Vec2(x, y));
moveTo->setTag(1);
node->runAction(moveTo);
}
Выбранный спрайт
Выбранный спрайт с рамкой
Официальные рекомендации для создания анимаций — использовать стороннюю программу для создания анимации и файлы plist.
Но если неохота заморачиваться, можно сделать проще. Загружаем картинку, затем создаем массивы Vector
std::string ANIMATION_MOVE_DOWN = "moveDown",
ANIMATION_MOVE_UP = "moveUp",
ANIMATION_MOVE_RIGHT = "moveRight",
ANIMATION_SPEARGOBLIN_MOVE_RIGHT = "spearGoblinMoveRight";
void HelloWorld::addStartUnitsAndBuildings() {
auto texture = Director::getInstance()->getTextureCache()->addImage("./images/MiniWorldSprites/Characters/Monsters/Orcs/FarmerGoblin.png");
auto frame1 = SpriteFrame::createWithTexture(texture, Rect(0, 0, 32, 32));
Vector framesMoveDown = {
SpriteFrame::createWithTexture(texture, Rect(0, 0, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(32, 0, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(64, 0, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(96, 0, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(128, 0, 32, 32))
};
Vector framesMoveUp = {
SpriteFrame::createWithTexture(texture, Rect(0, 32, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(32, 32, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(64, 32, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(96, 32, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(128, 32, 32, 32))
};
Vector framesMoveRight = {
SpriteFrame::createWithTexture(texture, Rect(0, 64, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(32, 64, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(64, 64, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(96, 64, 32, 32)),
SpriteFrame::createWithTexture(texture, Rect(128, 64, 32, 32))
};
auto textureSpearGoblin = Director::getInstance()->getTextureCache()->addImage("./images/MiniWorldSprites/Characters/Monsters/Orcs/SpearGoblin.png");
Vector framesSpearGoblinMoveRight = {
SpriteFrame::createWithTexture(textureSpearGoblin, Rect(0, 64, 32, 32)),
SpriteFrame::createWithTexture(textureSpearGoblin, Rect(32, 64, 32, 32)),
SpriteFrame::createWithTexture(textureSpearGoblin, Rect(64, 64, 32, 32)),
SpriteFrame::createWithTexture(textureSpearGoblin, Rect(96, 64, 32, 32)),
SpriteFrame::createWithTexture(textureSpearGoblin, Rect(128, 64, 32, 32))
};
animationFrames[ANIMATION_MOVE_DOWN] = framesMoveDown;
animationFrames[ANIMATION_MOVE_UP] = framesMoveUp;
animationFrames[ANIMATION_MOVE_RIGHT] = framesMoveRight;
animationFrames[ANIMATION_SPEARGOBLIN_MOVE_RIGHT] = framesSpearGoblinMoveRight;
auto sprite = Sprite::createWithSpriteFrame(frame1);
if (sprite == nullptr)
{
problemLoading("'./images/MiniWorldSprites/Characters/Monsters/Orcs/FarmerGoblin.png'");
}
else
{
sprite->setPosition(Vec2(START_X, START_Y));
sprite->setName("playerUnit");
this->addChild(sprite, 1);
}
}
void HelloWorld::moveUnitTo(Node* node, float x, float y) {
...
if (node->getName() == "playerUnit") {
// если анимация уже проигрывается, отменим задание
if (node->getActionByTag(2) != nullptr) {
node->stopActionByTag(2);
}
std::string animationType = "none";
auto sprite = dynamic_cast(node);
if ((M_PI / 4 >= angle) && (angle >= -1 * M_PI / 4)) {
animationType = ANIMATION_MOVE_RIGHT;
sprite->setFlippedX(false);
}
else if ((3 * M_PI / 4 >= angle) && (angle >= M_PI / 4)) {
animationType = ANIMATION_MOVE_UP;
}
else if ((angle <= (- 1 * M_PI / 4)) && (angle >= (-3 * M_PI / 4))) {
animationType = ANIMATION_MOVE_DOWN;
}
else if ((angle >= 3 * M_PI / 4) || (angle <= -3 * M_PI / 4)) {
animationType = ANIMATION_MOVE_RIGHT;
sprite->setFlippedX(true);
}
// set animation
float animationTickTime = 0.3;
float animationFrameTime = animationFrames[animationType].size() * animationTickTime;
int animationRepeats = static_cast(moveTime / animationFrameTime);
auto animation = Animation::createWithSpriteFrames(animationFrames[animationType], animationTickTime);
auto animate = Animate::create(animation);
auto repeatAction = Sequence::create(animate, DelayTime::create(.0f), nullptr);
auto repeatAnim = Repeat::create(repeatAction, animationRepeats);
repeatAnim->setTag(2);
sprite->runAction(repeatAnim);
}
}
Анимация движения
Движение и анимация
Попробуем большие tilemap карты. У меня запустилась карта 100×100, карты 200×200 и больше падают с ошибкой. Как оказалось, максимальный размер карты, ограничен 128×128 ячеек, на форумах предлагают укрупнять размер ячеек если нужна большая карта. К сравнению в своем движке jsge, реализовал поддержку сколь угодно больших карт за счет обработки только видимых ячеек на экране.
Тест по количеству спрайтов. Попробовал добавлять спрайты пробелом и двигать с анимацией в другой угол карты — 300 шт. тянет без проблем, нужен тест посложнее.
Будем добавлять спрайты пока держим клавишу пробел. Если использовать нативный вызов std: thread, как делали перемещение карты и в нем добавление спрайтов, игра падает различными с ошибками.
Основная проблема в том, что cocos2d: Vector
void HelloWorld::onKeyPressed(EventKeyboard::KeyCode keyCode, Event* event)
{
...
case cocos2d::EventKeyboard::KeyCode::KEY_SPACE:
this->keyPressed[cocos2d::EventKeyboard::KeyCode::KEY_SPACE] = true;
log("Start spawn thread");
this->schedule(CC_SCHEDULE_SELECTOR(HelloWorld::addUnitAndMove), 0.006f);
break;
}
...
}
void HelloWorld::addUnitAndMove(float dt) {
auto sprite = Sprite::createWithSpriteFrame(*animationFrames[ANIMATION_MOVE_RIGHT].begin());
sprite->setPosition(Vec2(this->addUnitPosX, this->addUnitPosY));
this->addUnitPosY -= 16.00;
sprite->setName("playerUnit");
this->addChild(sprite, 1);
this->moveUnitTo(sprite, 2500, this->addUnitPosY);
}
Чтобы точно знать сколько спрайтов уже добавили, выведем эту информацию на экран:
bool HelloWorld::init()
{
...
std::string labelStr = std::to_string((int)this->unitsCount);
auto label = Label::createWithTTF("Sprites #", "fonts/arial.ttf", 62);
label->setPosition(Vec2(origin.x + visibleSize.width / 2,
origin.y + visibleSize.height - label->getContentSize().height));
this->unitsCountLabel = Label::createWithTTF(labelStr, "fonts/arial.ttf", 62);
this->unitsCountLabel->setPosition(Vec2(origin.x + visibleSize.width / 2 + label->getContentSize().width,
origin.y + visibleSize.height - label->getContentSize().height));
this->addChild(label, 3);
this->addChild(this->unitsCountLabel, 3);
}
void HelloWorld::addUnitAndMove(float dt) {
...
++this->unitsCount;
this->unitsCountLabel->setString(std::to_string((int)this->unitsCount));
}
Что получилось: движок легко тянет больше 15 000 спрайтов, какого-то замедления я не заметил:
15 000 спрайтов / одна текстура
15 000 спрайтов/ одна текстура
1*Скорость gif-ки: 1 кадр/сек.
FPS: 60
GL CALLS: 8
Судя по отладочной информации у нас постоянно 8 вызовов GL CALLS — это вызовы отрисовки, шесть для слоев тайлмапы, два — для текста (количества спрайтов на экране) и один для спрайта. Получается, все добавленные спрайты рисуются в один проход и получается очень быстро, попробуем сделать еще один тест и загрузим еще один спрайт с другой текстурой.
10000 спрайтов / две текстуры
4000 спрайтов/ две текстуры
*Скорость gif-ки: 1 кадр/сек.
FPS: 50–60
GL CALLS: 6–8376
После каждого добавления спрайтов, растет и число GL CALLS после 6000 спрайтов идет небольшое уменьшение FPS. Судя по всему движок не склеивает разные текстуры, а рисует каждый раз отдельно, производительность от этого немного страдает.
Сделаем еще один тест, добавим в симуляцию детектор коллизий, в cocos2d есть встроенная физика на основе box2d. Включим ее в приложение.
bool HelloWorld::init()
{
//////////////////////////////
// 1. super init first
if ( !Scene::initWithPhysics() )
{
return false;
}
...
}
Далее добавляем физические параметры нашим спрайтам:
auto sprite = Sprite::createWithSpriteFrame(*animationFrames[ANIMATION_MOVE_RIGHT].begin());
sprite->setPosition(Vec2(this->addUnitPosX, this->addUnitPosY));
this->addUnitPosY -= 32.00;
sprite->setName("playerUnit");
auto spriteBody = PhysicsBody::createBox(sprite->getContentSize(), PhysicsMaterial(0, 0, 0));
sprite->setPhysicsBody(spriteBody);
this->addChild(sprite, 1);
Что такое? Спрайты начинают падать:
Спрайты падают
*Скорость gif-ки: 4 кадр/сек.
Мы забыли выключить «гравитацию»:
bool HelloWorld::init()
{
...
// по-умолчанию включена гравитация и все спайты полетят вниз карты
// выключим ее
this->_physicsWorld->setGravity(Vec2(0, 0));
}
Теперь спрайты начинают толкаться, посмотрим сколько выдержит движок.
Физика 3000 спрайтов
физика 3000 спрайтов
*Скорость gif-ки: 1 кадр/сек.
FPS: 38–60
Около 1800 тянет легко, потом немного снижается fps при добавлении новых объектов, около 3000 спрайтов FPS примерно 50–55. К сравнению, демка matter.js, тянет 700 объектов без просадки fps, 1290 — на скорости 40 fps, но это javascript в браузере.
Заключение
Очень много различных программ и их версий под одним брендом Cocos2d, очень легко запутаться и понять, что есть что. Есть Cocos Creator, его не пробовал, по способу установки и скриншотам очень напоминает Unreal Engine 5. Есть движок cocos2d-x, который мы рассматривали в статье, последняя версия v4.
По документации. Есть отдельный ресурс для Cocos Creator версий 1–3 и для cocos2d-x: https://docs.cocos2d-x.org/api-ref/
По движку cocos2d-x
Технический, все очень похоже на то, что есть в других движках в т.ч. и на javascript.
Дружит со сторонними программами и файлами. Максимальный размер тайлмапы 128×128 ячеек.
В отладке, помимо FPS, есть количество вызовов отрисовки GL_CALLS.
Легко подключается физика на основе box2d.
Встроенный физ. движок, тянет до 3000 спрайтов на карте.
Доступна только 32 битная сборка.
Последний релиз 2019. К сожалению проект мертв и поддерживается только Cocos Creator. Как идейного продолжателя предлагают Axmol Engine — это форк который поддерживают и стабильно релизят, есть и 64 битная сборка.
Ссылки
Исходники и демка: https://github.com/ALapinskas/cocos2d-x-performance-test
Спрайты отсюда: https://merchant-shade.itch.io/16×16-mini-world-sprites
Конфигурация оборудования на котором проводились тесты: