Складніша робота з об'єктами у Cocos2d-x

10 хв. читання

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

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

  1. Зелене НЛО має весь час злегка коливатись (обертатись) навколо власного центру.
  2. Посеред сцени на рівні НЛО мають бути дві кнопки: права та ліва стрілка. Якщо натиснути на одну з них, НЛО починає рухатись у відповідному напрямку, поки не дійде до крайньої позиції.
  3. Якщо під час руху натиснути на іншу кнопку, рух у поточному напрямку припиняється, починається у протилежному.
  4. Синій корабель з'являється у лівій частині екрана носом догори. У правій розташована кнопка-стрілка.
  5. При натисненні на стрілку корабель повертається і починає рухатись.
  6. Коли корабель доходить до місця призначення, він повертається носом догори; після цього кнопка-стрілка змінюється на протилежну.
  7. Якщо знову натиснути на кнопку, корабель почне рухатись в іншому напрямку.

Тож зелене НЛО буде поводитись так:

Складніша робота з об'єктами у Cocos2d-x

А синій корабель так:

Складніша робота з об'єктами у Cocos2d-x

Нагадаю, що зображення для прикладів взяті з безкоштовних наборів: кнопки та космічні кораблі.

Теги для Action

У класу Action є параметр tag — це просте число, за яким можна відрізнити одну акцію від іншої. Тег встановлюється методом setTag або іноді передається у конструкторі акції.

Відповідно, у Node є ряд методів для роботи з акціями:

  • void stopActionByTag(int tag) — зупинка акції з відповідним тегом;
  • Action* getActionByTag(int tag) — отримання вказівника на акцію;
  • ssize_t getNumberOfRunningActionsByTag(int tag) const — кількість акцій, які зараз діють.

Окрім того, існує синглтон ActionManager зі схожими методами, але у документації не рекомендують ним користуватись.

У нашому прикладі теги знадобляться, щоб відділити обертання НЛО від його горизонтального руху. Щоб виконати перший пункт нашого завдання, застосуємо послідовності та акції RepeatForever ось так:

Sequence* rseq = Sequence::create(RotateBy::create(3, 30),
                                  RotateBy::create(3, -30), nullptr);

RepeatForever* reps = RepeatForever::create(rseq);
reps->setTag(AT_UFO_ROTATION);
greenUfo->runAction(reps);

Тут AT_UFO_ROTATION — це числова константа і тег, що позначає акцію обертання.

Запуск руху НЛО в сторону відбувається приблизно так:

MoveBy* moveTo = MoveTo::create(time, Vec2(newX,240));
moveTo->setTag(AT_UFO_MOVING);
greenUfo->runAction(moveTo);

а зупинка ось так:

greenUfo->stopAllActionsByTag(AT_UFO_MOVING);

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

У випадку синього корабля доведеться зупиняти послідовність. Це не відрізняється від одної акції, просто тег треба ставити саме на послідовність, а не на окремі акції, приблизно так:

// створюємо ряд акцій для послідовності
///....

Sequence* seq = Sequence::create(rotateAct, moveAct, rotateBackAct, nullptr);
seq->setTag(AT_BS_MOVE);

blueShip->runAction(seq);

Визначення акції

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

Є два методи, які можуть показати, що певна акція триває у цей час: getNumberOfRunningActionsByTag() та getActionByTag().

Застосовуються вони однаково:

if (blueShip->getNumberOfRunningActionsByTag(AT_BS_ROTATION_BACK)>0 ) {
  log("%s: rotating back, do not interfere with moving", __func__);
  return;
}

або

if (blueShip->getActionByTag(AT_BS_ROTATION_BACK) != nullptr) {
  log("%s: rotating back, do not interfere", __func__);
  return;
}

Обробка завершення акції

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

Можна зробити лямбда-вираз, який виконає необхідні дії при завершенні послідовності:

CallFunc* finalCf = CallFunc::create([this]() {
  // запуск акції по зворотному повороту корабля
  RotateTo* ra2 = RotateTo::create(3, 0);
  ra2->setTag(AT_BS_ROTATION_BACK);
  this->blueShip->runAction(ra2);

  // заміна зображень на правій кнопці
  // ......
});

Sequence* seq = Sequence::create(rotateAct, moveAct, finalCf, nullptr);
seq->setTag(AT_BS_MOVE);

blueShip->runAction(seq);

Нагадаю, що конструктор Sequence отримує акції, які треба об'єднати у послідовність, а також nullptr у кінці. В цьому випадку ми виконуємо три дії: поворот, рух та обробку завершення.

Параметри для колбеків

Переміщення НЛО праворуч відрізняється від переміщення ліворуч лише значенням координати, всі інші дії однакові. Очевидно, було б непогано, якби натиснення на праву та ліву кнопку обробляв один метод, викликаний з різними параметрами. Як цього можна досягти?

У попередній статті було сказано, що цифра у назві макросу CC_CALLBACK_ задає кількість параметрів, які отримає відповідна функція. Це не зовсім правильно: насправді цей макрос може отримувати будь-яку кількість параметрів і всі вони будуть передані std::bind до функції, яка буде викликана.

Краще пояснити на прикладі. Універсальний колбек обробки натиснення на кнопку оголошується так:

class ActionsDemoScene : public cocos2d::Scene {
public:
  // .....
protected:
  void ufoMoveCallback(cocos2d::Ref *pSender, const int direction);
};

Тут direction — це той самий параметр, який ми хочемо додати до виклику функції.

А ось таким буде встановлення колбеку:

MenuItemImage* leftButton = MenuItemImage::create(
    "ui/leftArrow1.png", "ui/leftArrow2.png",
    CC_CALLBACK_1(ActionsDemoScene::ufoMoveCallback, this, GU_MOVE_LEFT));

MenuItemImage* rightButton = MenuItemImage::create(
  "ui/rightArrow1.png", "ui/rightArrow2.png",
  CC_CALLBACK_1(ActionsDemoScene::ufoMoveCallback, this, GU_MOVE_RIGHT));

Тут GU_MOVE_LEFT та GU_MOVE_RIGHT — це константи, які відрізняють два випадки виклику методу. Метод для обробки події вказується той самий, але завдяки магії std::bind він буде викликаний з різними значеннями параметрів.

Відповідно, у реалізації універсального методу може бути щось таке:

void ActionsDemoScene::ufoMoveCallback(Ref *pSender, const int direction) {
  float newX = 0; //undefided at this point
  if (direction == GU_MOVE_RIGHT) {
    newX = rightX;
  }
  else {
    newX = leftX;
  }

  // використання відповідного значення newX
}

У цьому прикладі можна було б передавати координату одразу під час створення кнопки, тоді б if у методі став би зовсім непотрібним. Проте мені така реалізація здалась не дуже наочною, тому я тут використав додатковий параметр direction просто у якості індикатора.

Видалення об'єктів

Після того як ми ознайомились з Action та іншими базовими термінами, можна розбиратись з проблемою коректного видалення об'єктів, створених Cocos2d-x.

Щоб це проілюструвати, напишемо просту сцену, у якій буде один спрайт і одна кнопка. Після натиснення на кнопку спрайт буде переміщуватись за межі екрана і там якось видалятиметься. Точніше, замість кнопки нам доведеться зробити меню з кількома варіантами видалення. Загалом програма матиме такий вигляд:

Складніша робота з об'єктами у Cocos2d-x

В меню будуть такі варіанти:

  • нічого не робити — просто залишити виведений за межі екрана елемент;
  • використати RemoveSelf — найбільш правильний спосіб;
  • використати метод Node removeChild() — трохи складніше і може спричинити певні проблеми;
  • не видаляти об'єкт, а зберегти для повторного використання.

Налагоджувальний клас

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

Реалізуватиметься він ось так:

class EnhancedSprite : public cocos2d::Sprite {
public:
  static EnhancedSprite* create(const std::string& filename);
protected:
  EnhancedSprite();
  virtual ~EnhancedSprite();

  int objectId;
};

static int objectIdGenerator = 0;

EnhancedSprite::EnhancedSprite() {
  objectId = objectIdGenerator++;
  log("EnchancedSprite constructor here, id %02d", objectId);
}

EnhancedSprite::~EnhancedSprite() {
  log("EnchancedSprite destructor here, id %02d", objectId);
}

EnhancedSprite* EnhancedSprite::create(const std::string& filename) {
  EnhancedSprite *result = new (std::nothrow) EnhancedSprite();
  if (result && result->initWithFile(filename)) {
    result->autorelease();
    return result;
  }
  CC_SAFE_DELETE(result);
  return nullptr;
}

Тут у кожного об'єкта є поле objectId з унікальним значенням, яке дозволить відрізняти об'єкти один від одного. Все інше буде таке ж, як у звичайного спрайта.

Варіанти видалення

Якщо нічого не робити, спрайти залишатимуться у пам'яті. Цікаво, що коли ви спробуєте закрити програму, то побачите, як викликаються деструктори. Тобто формально це не є втратою пам'яті: невидимі об'єкти все ще залишаються на сцені й вона зберігає посилання на них. Але все одно ця ситуація погана і так робити не можна.

Найбільш зручний і правильний спосіб видалення — використати в кінці послідовність RemoveSelf (спеціальної акції, призначеної для видалення об'єктів). Працює ось так:

MoveTo* moveAct = MoveTo::create(5, Vec2(800, 120));
Sequence* seq = Sequence::create(moveAct, RemoveSelf::create(), nullptr);

blueShip->runAction(seq);

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

Такого ж ефекту можна досягти, якщо використати CallFunc та removeChild():

MoveTo* moveAct = MoveTo::create(5, Vec2(800, 120));

Sprite* oldShip = blueShip; //save blueShip pointer because it may get changed by the time
                            // the ship reaches "out of range: destination
CallFunc* finalCf = CallFunc::create([this, oldShip]() {
  log("%s: callback implemented in processMenuRemoveChild: move finished", __func__);
  this->removeChild(oldShip);
});

Sequence* seq = Sequence::create(moveAct, finalCf, nullptr);

blueShip->runAction(seq);

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

Збереження для повторного використання

Припустимо, що замість видалення ви хочете зберегти об'єкт десь у пулі. Це можливо, але доведеться зробити трохи інакше, ніж в описаних вище методах.

Усі класи Cocos2d-x є нащадками Ref — це така спроба авторів фреймворку зробити щось схоже на Objective-C. Тому в кожного об'єкта є такі методи:

  • retain() збільшує кількість посилань на 1, «захищаючи» об'єкт від видалення;
  • release() зменшує кількість посилань. Якщо їх менше за 0, об'єкт видаляється;
  • autorelease() зменшує кількість посилань, але перевірка лічильника та видалення об'єкта відбудуться коли-небудь потім.

Тож кожному виклику retain() має відповідати один виклик release() або autorelease().

Для об'єктів, що створені за допомогою статичних методів create(), завжди викликається autorelease() (так само, як у нашому класі EnhancedSprite). Тому, якщо хочеться, можна взагалі нічого зі створеним об'єктом не робити, він видалиться автоматично.

Як це працює? Десь глибоко в основі фреймворку розташований ігровий цикл, у ході якого виконуються приблизно такі дії:

  • програма спить дуже маленький (десятки секунд) проміжок часу, потім прокидається;
  • оброблює способи вводу;
  • сцена починає перемальовуватись (в процесі перераховуючи параметри акцій та викликаючи відповідні колбеки);
  • після перемалювання сцени ті об'єкти, у яких лічильник посилань рівний 0, видаляються.

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

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

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

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

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

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