Робота з картами рівнів у Cocos2d-x

11 хв. читання

Для більш-менш великих ігрових проєктів, як стратегії або платформери, вам знадобиться окремий засіб для проєктування карт рівнів. Таким засобом є Tiled — універсальний редактор рівнів з відкритим кодом.

Основи Tiled

Інтерфейс Tiled загалом схожий на інші редактори, тому я не буду тут його детально описувати (але, якщо треба, варто глянути сюди). Суть в тому, що ви берете створений художником набір елементів (tileset) і використовуєте його для побудови карти.

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

Робота з картами рівнів у Cocos2d-x

Є деякі особливості, специфічні для Cocos2d-x:

  • один шар (layer) у редакторі має використовувати елементи одного і того ж тайлсету. Якщо ви хочете використати інший тайлсет, обов'язково треба створити новий шар;
  • бажано, щоб файл тайлсету(*.tsx) був в одній папці з файлом карти (\*.tmx), інакше можуть виникнути проблеми під час завантаження;
  • старі версії Cocos2d-x (3.17.2 і менше) могли працювати лише з дуже-дуже старою версією редактора. У Cocos2d-x 4.0 цієї проблеми вже немає.

Карта завантажується дуже просто, фактично одним рядком:

const char mapFilename[] = "beach/beach_map.tmx";

TMXTiledMap* mapNode = TMXTiledMap::create(mapFilename);
if (mapNode == nullptr) {
// обробка помилки  
}
addChild(mapNode, ZO_BACKGROUND);

TMXTiledMap::create створює звичайний об'єкт-нащадок Node і далі з ним можна робити все те саме, що з іншими, зокрема використовувати у якості фону.

Додаткова інформація на карті

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

Для того щоб це зробити, використовується такий хід:

  • на карту додають спеціальний «службовий» шар, на якому використовуються елементи зі спеціального тайлсету;
  • під час завантаження інформація зі службового шару обробляється окремо, а потім його приховують від гравця.

Службовий тайлсет може мати такий вигляд:

Робота з картами рівнів у Cocos2d-x

Немає значення, що саме зображено на квадратах; в нашому випадку це просто цифри й позначки.

До кожного елементу тайлсету треба додати спеціальну властивість — числовий код, за допомогою якого програма відрізнятиме тайли один від одного. На малюнку вище така властивість названа «MetaCode», додається вона після натиснення на синій «+» внизу панелі.

Завантаження службового шару відбувається так:

bool SimpleNoScrollScene::loadMetaInfo(TMXTiledMap* const mapNode) {

  const string metaLayerName = "meta";
  TMXLayer* const metaLayer = mapNode->getLayer(metaLayerName);
  if (metaLayer == nullptr) {
    printf("Failed to find %s layer\
", metaLayerName.c_str());
    return false;
  }

  const Size mapSize = mapNode->getMapSize();
  obstaclesMapWidth = mapSize.width;
  obstaclesMapHeight = mapSize.height;
  obstaclesMap = new bool[obstaclesMapWidth*obstaclesMapHeight];
  memset(obstaclesMap, 0, (obstaclesMapWidth*obstaclesMapHeight));

  for (int tileX = 0; tileX < mapSize.width; tileX++) {
    for (int tileY = 0; tileY < mapSize.height; tileY++) {
      const int tileGid = metaLayer->getTileGIDAt(Vec2(tileX, tileY));
      const Value prop    = mapNode->getPropertiesForGID(tileGid);

      if (prop.isNull()) {
        continue;
      }

      const ValueMap vm   = prop.asValueMap();
      const auto frez = vm.find("MetaCode");

      if (frez == vm.end()) {
        continue;
      }

      const int metaCode = frez->second.asInt();

      switch (metaCode) {
      case MMC_OBSTACLE:
        obstaclesMap[obstaclesMapWidth*(obstaclesMapHeight - tileY - 1) + tileX] = true;
        break;

      case MMC_MAGE_START:
        mageStartX   = tileX;
        mageStartY   = mapSize.height - tileY - 1;
        break;

      case MMC_KNIGHT_START:
        knightStartX   = tileX;
        knightStartY   = mapSize.height - tileY - 1;

        currentKnightX = knightStartX;
        currentKnightY = knightStartY;
        break;

        // Note there is no suitable default action here
        // default:
      }
    }
  }

  metaLayer->setVisible(false);

  return true;
}

Тут ми отримуємо шар методом getLayer і обходимо всі його квадрати. Для кожного квадрату намагаємось отримати значення параметру «MetaCode», якщо таке є. Залежно від значення, це може бути або стартове місце, або позначка квадрату, на який не можна заходити.

У Tiled квадрат з координатами 0:0 розташований у верхньому лівому куті карти, а вісь Y зростає згори донизу. Це суперечить підходу Cocos2d-x, у якому точка 0:0 розташована внизу ліворуч, а Y збільшується догори. Тому при завантаженні координати Y доводиться перераховувати, використовуючи вираз типу knightStartY = mapSize.height - tileY - 1;.

Тож можна взяти таку карту:

Робота з картами рівнів у Cocos2d-x

В програмі вона матиме такий вигляд:

Робота з картами рівнів у Cocos2d-x

Цифри тайлсету позначають стартові позиції мага та лицаря, хрестики — місця, де не можна ходити. Лицар запрограмований так, щоб пересуватись на один квадрат вправо, доки у наступному квадраті не буде позначки-заборони. Повний прилад можна переглянути тут.

Переміщення по карті

Під час розробки ігор буває потрібно пересувати карту відповідно до дій персонажу. Особливо важливо правильно реалізувати скролінг у платформерах — я б дуже рекомендував прочитати ось цю статтю, просто щоб зрозуміти всю глибину проблеми.

Для прикладу візьмемо анімацію чарівника та проведемо його по довгому-довгому рівню, схожому на замок.

Робота з картами рівнів у Cocos2d-x

У Cocos2d переміщення карти (точніше, переміщення по карті) легко реалізувати за допомогою класу Camera.

Camera є нащадком Node, тому для того щоб змінити її позицію, можна використовувати звичайні акції (в нашому випадку MoveTo). Але, на відміну від об'єктів, які ми використовували раніше, камера живе у тривимірному просторі, тому координати вказуються трохи інакше.

Ось таким буде перенесення камери до точки, на якій стоїть маг:

cocos2d::Vec2 expectedMagePos; // поточна позиція мага
<....>
Camera* camera = getDefaultCamera();
const Vec3 currentCameraPos = camera->getPosition3D();
Vec3 newCameraPos = Vec3(expectedMagePos.x, expectedMagePos.y, currentCameraPos.z);
MoveTo* cameraMoveAction = MoveTo::create(1, newCameraPos);
camera->runAction(cameraMoveAction);

Просто задаєте нову позицію камери (висота така сама, як у старої, а координати на площині змінюються), створюєте акцію і все, скролінг готовий. У демонстраційній програмі камера пересувається кожного разу, як маг завершує перехід у точку, розташовану занадто близько до краю вікна.

Результат такий:

Робота з картами рівнів у Cocos2d-x

Зі скелетами у цій програмі пов'язано кілька цікавих речей. По-перше, деякі з них ходять за межами екрану, тому ресурси на обчислення їх положення витрачаються даремно. Може виникнути бажання якось вимкнути виконання акцій для об'єктів, розташованих далеко від гравця. Проте така дія є передчасною оптимізацією, її краще уникати.

По-друге, об'єктів-скелетів багато, тому для управління ними краще створити окремий клас. В принципі, для мага теж треба було б зробити свій клас, але це ускладнило б код, тому в прикладі його координати зберігаються у класі сцени.

Можливі два варіанти:

  • зробити клас скелету нащадком cocos2d::Sprite;
  • створити клас, що буде управляти об'єктом cocos2d::Sprite.

Це дискусійне питання «наслідування проти композиції», у кожного вибору є свої переваги та недоліки. У своєму прикладі я використав наслідування, а поради щодо створення композиції можна знайти у цій статті.

Різні формати екранів

В одній з перших статей я рекомендував видалити зі створеного cocos дефолтного коду майже все, що стосується роботи з різними роздільними здатностями. Цій проблемі присвячений окремий баг в офіційному репозиторії, але у нього низький пріоритет.

Розробка Cocos2d починалась у далекі-далекі часи, коли різновидів форматів екрану було небагато. Потім розмаїття сильно збільшилось, а сьогодні здається, що всі користувачі знову ходять з більш-менш однаковими екранами. Крім того, у певний момент була актуальною проблема роздільної здатності: програма мала використовувати різні набори зображень для пристроїв різних поколінь. На мій погляд, зараз такої необхідності уже немає, проте у вас може бути інша думка з цього приводу.

Теоретично, процес розробки має відбуватись приблизно так:

  1. Розробники обирають базовий формат екрану, під який розробляється програма. Це так званий design resolution.
  2. Якщо при запуску на пристрої виявляється, що його екран відрізняється від design resolution, згенероване програмою зображення буде у певний спосіб приводитись до цього самого design resolution.
  3. Всередині програми не можна застосовувати фіксовані числа для позначення позиції. У навчальних статтях я використовував числа, але тільки тому, що так код сприймається простіше. Насправді позиції всіх елементів потрібно обчислювати, враховуючи реальний стан справ з екраном пристрою.

У першій програмі ви могли бачити такий код:

auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin      = Director::getInstance()->getVisibleOrigin();

visibleSize — це розмір тієї частини екрану, яку бачить користувач. origin — положення нижнього лівого кута частини екрану, яку бачить користувач відносно того зображення, яке згенерувала б програма в ідеальному випадку.

Уявіть собі ось таку ситуацію (цифри сьогодні виглядають нереальними, але най буде)

Робота з картами рівнів у Cocos2d-x

У цьому випадку:

  • розробка велась під 800x480, це той самий design resolution;
  • під час запуску виявляється, що екран пристрою 1024x768. Після адаптації поле збільшується, але пропорції лишаються тими самими, тому з обох боків зображення обрізається;
  • після адаптації visibleSize буде 1024x768, а origin стане (128:0) — позиція нижнього лівого кута червоного прямокутника на малюнку.

Cocos2d пропонує кілька варіантів приведення (ResolutionPolicy):

  • EXACT_FIT — зображення повністю заповнює екран, пропорції ігноруються. Результат може бути розтягнено по горизонталі або вертикалі.
  • FIXED_HEIGHT та FIXED_WIDTH — співвідношення сторін зберігається, зображення може обрізатись так, щоб зберегти відповідно висоту або ширину.
  • NO_BORDER — схоже на автоматичне обирання FIXED_HEIGHT або FIXED_WIDTH.
  • SHOW_ALL — пропорції зберігаються, зображення повністю показується, але можуть бути чорні смужки з обох боків або згори екрану.

Також можна реалізувати свою власну політику, як зроблено у цій статті.

Крім того, можна не вказувати design resolution, а запуститись з реальним розміром екрану. Для цього в AppDelegate.cpp треба зробити так:

Size realScreenSize = glview->getFrameSize();
glview->setDesignResolutionSize(realScreenSize.width, realScreenSize.height,
                                ResolutionPolicy::NO_BORDER);

Тут байдуже, яку ResolutionPolicy вказувати, все одно запуск виконується з розміром екрану поточного пристрою.

Якщо ви захочете використовувати різні набори зображень для різних пристроїв, доведеться змінювати дефолтні шляхи, за якими завантажуються файли. Детальніше про це написано тут, тут або у цій статті.

На цьому все, дякую за увагу.

Серія статей про Cocos2d-x також завершується. Всі базові засоби фреймворку вже описані, їх має вистачити для створення простих програм.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 3.9K
Приєднався: 11 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація