Разделение состояния мира и анимации в пошаговой игре

9

Как вы справляетесь с отделением анимации от состояния мира в пошаговой игре? Сейчас я работаю над 2D-сеткой. Приведенный ниже код упрощен, чтобы лучше объяснить.

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

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

class Actor {
    void move(PositionInfo oldPosition, PositionInfo newPosition) {
        if(isValidMove(oldPosition, newPosition) {
             getGame().pushBlockingAnimation(new MoveAnimation(this, oldPosition, newPosition, ....));
             player.setPosition(newPosition);
        }
    }
}

Затем основной цикл обновления выполняет:

class Game {
    void update(float dt) {
        updateNonBlockingAnimations(dt); //Effects like particle explosions, damage numbers, etc. Things that don't block the flow of turns.
        if(!mBlockingAnimations.empty()) {
            mBlockingAnimations.get(0).update(dt);
        } else {
             //.. handle the next turn. This is where new animations will be enqueued..//
        }
        cleanUpAnimations(); // remove finished animations
    }
}

... где анимация обновляет положение экрана актера.

Еще одна идея, которую я реализую, заключается в том, чтобы иметь параллельную анимацию блокировки, при которой несколько анимаций блокировки будут обновляться одновременно, но следующий поворот не произойдет, пока все они не будут выполнены.

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

mdkess
источник

Ответы:

2

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

Ну, одна из причин: это может запутаться, если вам нелегко судить о последствиях "player.setPosition (newPosition);" больше.

Пример (просто выдумка): представьте, что вы перемещаете игрока с помощью setPosition поверх какой-то ловушки. Вызов «player.setPosition» сам по себе вызовет какой-то другой вызов… скажем, кода прослушивателя ловушек, который, в свою очередь, сам запустит блокирующую анимацию, которая отображает «разбрызгиватель всплеска» поверх самого себя.

Вы видите проблему: всплеск отображается одновременно с движением игрока к ловушке. (Давайте предположим, что вы этого не хотите, но хотите, чтобы анимация ловушек воспроизводилась сразу после окончания движения;)).

Так что до тех пор, пока вы можете помнить все последствия своих действий, которые вы делаете после этих вызовов «pushBlockingAnimation», все в порядке.

Возможно, вам придется начать оборачивать игровую логику в «блокирующие что-то» классы, чтобы их можно было поставить в очередь для выполнения после завершения анимации. Или вы передаете функцию обратного вызова «callThisAfterAnimationFinishes» в pushBlockingAnimation и перемещаете setPosition в функцию обратного вызова.

Другим подходом будет использование скриптовых языков. Например, Lua имеет «coroutine.yield», который возвращает функцию с особым состоянием, чтобы ее можно было вызвать позже для продолжения при следующей инструкции. Таким образом, вы можете легко дождаться окончания анимации, чтобы выполнить игровую логику, не загромождая «нормальный» вид программного потока вашего кода. (означает, что ваш код по-прежнему будет выглядеть как «playAnimation (); setPosition ();» вместо передачи обратных вызовов и загромождения игровой логики несколькими функциями).

Unity3D (и, по крайней мере, еще одна известная мне игровая система) имеет оператор C # «yield return» для имитации этого непосредственно в коде C #.

// instead of simple call to move(), you have to "iterate" it in a foreach-like loop
IEnumerable<Updatable> move(...) {
    // playAnimation returns an object that contain an update() and finished() method.
    // the caller will not iterate over move() again until the object is "finished"
    yield return getGame().playAnimation(...)
    // the next statement will be executed *after* the animation finished
    player.setPosition(newPosition);
}

Конечно, в этой схеме вызов move () теперь получает всю грязную работу. Вы, вероятно, примените эту технику только для определенных функций, где вы точно знаете, кто их вызывает и откуда. Так что принимайте это только как основную идею, как организованы другие движки - это, вероятно, слишком сложно для вашей текущей конкретной проблемы.

Я рекомендую придерживаться идеи pushBlockingAnimation, пока у вас не возникнут реальные проблемы. Тогда измените это. ;)

Imi
источник
Большое спасибо, что нашли время написать этот ответ, я действительно ценю это.
mdkess