Игровой цикл, как проверить условия один раз, сделать что-то, а потом не делать это снова

13

Например, у меня есть класс Game, и он хранит данные, intкоторые отслеживают жизнь игрока. У меня условно

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

Однако это условие продолжает работать, и работа выполняется повторно. Например, я хочу установить таймер для выхода из игры через 5 секунд. В настоящее время он будет устанавливать 5 секунд на каждый кадр, и игра никогда не заканчивается.

Это всего лишь один пример, и у меня одна и та же проблема в нескольких областях моей игры. Я хочу проверить какое-то условие и затем сделать что-то один раз и только один раз, а затем не проверять или делать код снова в операторе if. Некоторые возможные решения, которые приходят на ум, имеют boolдля каждого условия и устанавливают, boolкогда условие срабатывает. Однако на практике это приводит к большим ошибкам bools, поскольку они должны храниться как поля класса или статически в самом методе.

Каково правильное решение этого (не для моего примера, но для проблемной области)? Как бы вы сделали это в игре?

EddieV223
источник
Спасибо за ответы, мое решение теперь представляет собой комбинацию ответов ktodisco и crancran.
EddieV223

Ответы:

13

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

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

Это, в свою очередь, предполагает, что он subtractPlayerLifeбудет вызываться только в определенных случаях, что, возможно, будет результатом условий, которые вы должны проверять каждый кадр (возможно, столкновения).

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

Если есть что-то, что кажется просто невозможным для рефакторинга, и вам действительно нужно проверить только один раз, тогда можно использовать enumстатические или логические значения (например, a ). Чтобы избежать объявления статики самостоятельно, вы можете использовать следующий прием:

#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в качестве крайней меры или только для целей отладки.

kevintodisco
источник
+1 приятно один раз, если решение. Грязно, но круто.
Кендер
1
Есть одна большая проблема с вашим ONE_TIME_IF, вы не можете сделать это снова. Например, игрок возвращается в главное меню и позже возвращается на игровую сцену. Это заявление больше не сработает. и существуют условия, которые вам, возможно, придется соблюдать между запусками приложений, например, список уже полученных трофеев. Ваш метод будет вознаграждать трофей дважды, если игрок получит его в двух разных запусках приложения.
Ali1S232
1
@Gajoo Это правда, это проблема, если условие необходимо сбросить или сохранить. Я редактировал, чтобы уточнить это. Вот почему я рекомендовал это как последнее средство или просто для отладки.
Кевинтодиско
Могу ли я предложить идиому 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 для обработки изменения / нажатия / перехода между состояниями, а не оператора switch.

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

Naros
источник
1

Если вы беспокоитесь о производительности, производительность проверки несколько раз не является значительной; То же самое для того, чтобы иметь условия bool.

Если вас интересует что-то еще, вы можете использовать что-то похожее на шаблон нулевого дизайна для решения этой проблемы.

В этом случае предположим, что вы хотите вызвать метод, 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экземпляр).

Это по сути как сочетание стратегии и нулевых паттернов.

ashes999
источник
Но он будет продолжать делать новый CallFoo () каждый кадр, который вы должны удалить, прежде чем новый, и также будет происходить, что не решит проблему. Например, если бы у меня был CallFoo (), вызывающий метод, который устанавливает таймер для выполнения через несколько секунд, этот таймер был бы установлен в то же время в каждом кадре. Насколько я могу судить, ваше решение такое же, как если бы (numLives <1) {// do stuff} else {// empty}
EddieV223
Хм, я понимаю, что вы говорите. Решением может быть перемещение ifчека в сам FooClassэкземпляр, чтобы он выполнялся только один раз, и то же самое для создания экземпляра CallFoo.
ashes999