Ігровий цикл, як один раз перевірити умови, зробити щось, а потім не робити це знову


13

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

if ( mLives < 1 ) {
    // Do some work.
}

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

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

Яке правильне рішення для цього (не для мого прикладу, а для проблемної області)? Як би ти це зробив у грі?


Дякую за відповіді, тепер моє рішення - це комбінація відповідей ktodisco та crancran.
EddieV223

Відповіді:


13

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

void subtractPlayerLife() {
    // Generic life losing stuff, such as mLives -= 1
    if (mLives < 1) {
        // Do specific stuff.
    }
}

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

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

Якщо є щось, що здається неможливим для рефактора, і вам дійсно потрібно перевірити лише один раз, тоді стан (як enum) або статичні булеви - це шлях. Щоб уникнути декларування статики самостійно, ви можете скористатися наступним трюком:

#define ONE_TIME_IF(condition, statement) \
    static bool once##__LINE__##__FILE__; \
    if(!once##__LINE__##__FILE__ && condition) \
    { \
        once##__LINE__##__FILE__ = true; \
        statement \
    }

який зробив би такий код:

while (true) {
    ONE_TIME_IF(1 > 0,
        printf("something\n");
        printf("something else\n");
    )
}

друк somethingі something elseлише один раз. Він не дає найкращого вигляду коду, але він працює. Я також впевнений, що існують шляхи її вдосконалення, можливо, завдяки кращому забезпеченню унікальних імен змінних. Це #defineпрацюватиме лише один раз під час виконання. Немає можливості її скинути, і немає способу її зберегти.

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


+1 приємно один раз, якщо рішення. Брудний, але крутий.
кендер

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

1
@Gajoo Це правда, це проблема, якщо стан потрібно скинути або зберегти. Я редагував, щоб уточнити це. Ось чому я рекомендував це в крайньому випадку, або просто для налагодження.
kevintodisco

Чи можу я запропонувати ідіому do {} while (0)? Я знаю, що я особисто щось накрутив і поставив крапку з комою, куди не повинен. Хороший макрос, хоч, браво.
michael.bartnett

5

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

У найбільш тривіальному підході:

switch(mCurrentState) {
  case LIVES_GREATER_THAN_0:
    if(lives < 1) mCurrentState = LIVES_LESS_THAN_1;
    break;
  case LIVES_LESS_THAN_1:
    timer.expires(5000); // expire in 5 seconds
    mCurrentState = TIMER_WAIT;
    break;
  case TIMER_WAIT:
    if(timer.expired()) mCurrentState = EXIT;
    break;
  case EXIT:
    mQuit = true;
    break;
}

Ви можете розглянути можливість використання класу State разом з StateManager / StateMachine для обробки зміни / натискання / переходу між станами, а не оператором переключення.

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


1

Якщо ви турбуєтесь про ефективність, ефективність перевірки декількох разів зазвичай не є значною; дето за умови булінгу.

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

У цьому випадку давайте припустимо, що ви хочете викликати метод, fooякщо у гравця не залишилося життя. Ви реалізували б це як два класи, той, який дзвонить foo, і той, який нічого не робить. Дозвольте їм зателефонувати DoNothingі CallFooтак:

class SomeLevel {
  int numLives = 3;
  FooClass behaviour = new DoNothing();
  ...
  void tick() { 
    if (numLives < 1) {
      behaviour = new CallFoo();
    }

    behaviour.execute();
  }
}

Це трохи "сирий" приклад; Ключові моменти:

  • Слідкуйте за тим, який примірник FooClassможе бути DoNothingабо CallFoo. Це може бути базовий клас, абстрактний клас або інтерфейс.
  • Завжди викликайте executeметод цього класу.
  • За замовчуванням клас нічого не робить ( DoNothingекземпляр).
  • Коли життя закінчується, екземпляр поміняється на екземпляр, який фактично щось робить ( CallFooекземпляр).

Це по суті як поєднання стратегії та нульових моделей.


Але він буде продовжувати створювати новий CallFoo () кожен кадр, який ви повинні видалити перед новим, а також триває, що не вирішує проблему. Наприклад, якби у мене був метод виклику CallFoo (), який встановлює таймер для виконання за кілька секунд, цей таймер буде встановлений на один і той же час кожного кадру. Наскільки я можу сказати, ваше рішення таке саме, як якщо б (numLives <1) {// робити речі} else {// порожній}
EddieV223

Гм, я бачу, що ти кажеш. Рішенням може бути переміщення ifчека в самий FooClassекземпляр, тому він виконується лише один раз та ditto для інстанції CallFoo.
ashes999
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.