Вы можете просматривать вашу систему так, как если бы она была составлена из ряда состояний и функций, где функция f[j]
с вводом x[j]
изменяет состояние системы s[j]
в состояние s[j+1]
, например так:
s[j+1] = f[j](s[j], x[j])
Государство - это объяснение всего вашего мира. Расположение игрока, местоположение противника, счет, оставшиеся боеприпасы и т. Д. Все, что вам нужно, чтобы нарисовать рамку вашей игры.
Функция - это все, что может повлиять на мир. Смена кадра, нажатие клавиши, сетевой пакет.
Ввод - это данные, которые получает функция. Смена кадра может занять количество времени, прошедшее с момента последнего пройденного кадра, нажатие клавиши может включать в себя фактическую нажатую клавишу, а также то, была ли нажата клавиша Shift.
Ради этого объяснения я сделаю следующие предположения:
Предположение 1:
Количество состояний для данного запуска игры намного больше, чем количество функций. Вероятно, у вас есть сотни тысяч состояний, но только несколько десятков функций (смена кадров, нажатие клавиш, сетевой пакет и т. Д.). Конечно, количество входов должно быть равно количеству состояний минус один.
Предположение 2:
Пространственная стоимость (память, диск) хранения одного состояния намного больше, чем стоимость хранения функции и ее ввода.
Предположение 3:
Временные затраты (время) представления состояния аналогичны или на один-два порядка больше, чем затраты на вычисление функции по состоянию.
В зависимости от требований вашей системы воспроизведения, существует несколько способов реализации системы воспроизведения, поэтому мы можем начать с самого простого. Я также приведу небольшой пример, используя игру в шахматы, записанную на листах бумаги.
Способ 1:
Магазин s[0]...s[n]
. Это очень просто, очень просто. Из-за предположения 2 пространственная стоимость этого довольно высока.
Для шахмат это можно сделать, рисуя всю доску за каждый ход.
Способ 2:
Если вам нужно только прямое воспроизведение, вы можете просто сохранить s[0]
, а затем сохранить f[0]...f[n-1]
(помните, это только имя идентификатора функции) и x[0]...x[n-1]
(что было введено для каждой из этих функций). Чтобы воспроизвести, вы просто начинаете с s[0]
и рассчитываете
s[1] = f[0](s[0], x[0])
s[2] = f[1](s[1], x[1])
и так далее...
Я хочу сделать небольшую аннотацию здесь. Несколько других комментаторов заявили, что игра «должна быть детерминированной». Любой, кто говорит, что должен снова принять Computer Science 101, потому что, если ваша игра не предназначена для работы на квантовых компьютерах, ВСЕ КОМПЬЮТЕРНЫЕ ПРОГРАММЫ ДЕТЕРМИНИСТИЧЕСКИ ». Вот что делает компьютеры такими классными.
Однако, поскольку ваша программа, скорее всего, зависит от внешних программ, от библиотек до фактической реализации ЦП, убедиться, что ваши функции ведут себя одинаково на разных платформах, может быть довольно сложно.
Если вы используете псевдослучайные числа, вы можете либо сохранить сгенерированные числа как часть вашего ввода x
, либо сохранить состояние функции prng как часть вашего состояния s
, а ее реализацию как часть функции f
.
Для шахмат это можно сделать, нарисовав начальную доску (которая известна), а затем опишите каждый ход, указав, какая фигура куда ушла. Кстати, именно так они и делают.
Способ 3:
Теперь вы, скорее всего, хотите иметь возможность искать свой повтор. То есть рассчитать s[n]
на произвольную n
. Используя метод 2, вам нужно рассчитать, s[0]...s[n-1]
прежде чем вы сможете рассчитать s[n]
, который, согласно предположению 2, может быть довольно медленным.
Для реализации этого метод 3 является обобщением методов 1 и 2: хранить f[0]...f[n-1]
и x[0]...x[n-1]
точно так же, как метод 2, но также хранить s[j]
для всех j % Q == 0
для данной константы Q
. Проще говоря, это означает, что вы храните закладку в каждом из Q
штатов. Например, для Q == 100
, вы хранитеs[0], s[100], s[200]...
Для того, чтобы вычислить s[n]
произвольно n
, вы сначала загружаете ранее сохраненные s[floor(n/Q)]
, а затем вычисляете все функции от floor(n/Q)
до n
. Самое большее, вы будете вычислять Q
функции. Меньшие значения Q
вычисляются быстрее Q
, но занимают гораздо больше места, в то время как большие значения занимают меньше места, но вычисляются дольше.
Метод 3 с Q==1
такой же, как метод 1, а метод 3 с Q==inf
такой же, как метод 2.
В шахматах это можно сделать, разыгрывая каждый ход, а также каждую десятую доску (для Q==10
).
Способ 4:
Если вы хотите отменить воспроизведение, вы можете сделать небольшое изменение метода 3. Предположим Q==100
, и вы хотите , чтобы вычислить s[150]
через s[90]
в обратном направлении. При неизмененном методе 3 вам нужно будет сделать 50 вычислений, чтобы получить, s[150]
а затем еще 49 вычислений, чтобы получить s[149]
и так далее. Но так как вы уже рассчитали, s[149]
чтобы получить s[150]
, вы можете создать кеш, s[100]...s[150]
когда вы рассчитываете s[150]
в первый раз, а затем вы уже s[149]
в кеше, когда вам нужно отобразить его.
Вам нужно только восстанавливать кэш каждый раз, когда вам нужно рассчитать s[j]
, j==(k*Q)-1
для любого данного k
. На этот раз увеличение Q
приведет к уменьшению размера (только для кеша), но к более длительному времени (только для воссоздания кеша). Оптимальное значение для Q
может быть рассчитано, если вы знаете размеры и время, необходимые для вычисления состояний и функций.
Для шахмат это можно сделать, рисуя каждый ход, а также одну на каждые 10 досок (для Q==10
), но также потребуется нарисовать на отдельном листе бумаги последние 10 досок, которые вы вычислили.
Способ 5:
Если состояния просто занимают слишком много места, или функции занимают слишком много времени, вы можете создать решение, которое фактически реализует (не подделывает) обратное воспроизведение. Для этого вы должны создать обратные функции для каждой из ваших функций. Однако для этого необходимо, чтобы каждая из ваших функций была инъекцией. Если это выполнимо, то для f'
обозначения обратной функции f
вычисление s[j-1]
так же просто, как
s[j-1] = f'[j-1](s[j], x[j-1])
Обратите внимание, что здесь и функция, и вход - j-1
нет j
. Эта же функция и входные данные были бы теми, которые вы использовали бы, если бы вычисляли
s[j] = f[j-1](s[j-1], x[j-1])
Создание обратной функции - сложная часть. Тем не менее, вы обычно не можете, так как некоторые данные состояния обычно теряются после каждой функции в игре.
Этот метод, как есть, может обратный расчет s[j-1]
, но только если у вас есть s[j]
. Это означает, что вы можете смотреть реплей только в обратном направлении, начиная с того момента, когда вы решили переиграть в обратном направлении. Если вы хотите воспроизвести в обратном направлении с произвольной точки, вы должны смешать это с методом 4.
Для шахмат это не может быть реализовано, так как с данной доской и предыдущим ходом вы можете знать, какая фигура была перемещена, но не куда она пошла.
Способ 6:
Наконец, если вы не можете гарантировать, что все ваши функции являются инъекциями, вы можете сделать небольшой трюк для этого. Вместо того, чтобы каждая функция возвращала только новое состояние, вы также можете сделать так, чтобы она возвращала отброшенные данные, например, так:
s[j+1], r[j] = f[j](s[j], x[j])
Где r[j]
находятся сброшенные данные. А затем создайте свои обратные функции, чтобы они принимали отброшенные данные, например так:
s[j] = f'[j](s[j+1], x[j], r[j])
В дополнение к f[j]
и x[j]
, вы также должны хранить r[j]
для каждой функции. Еще раз, если вы хотите иметь возможность искать, вы должны хранить закладки, например, с помощью метода 4.
Для шахмат это будет то же самое, что и для метода 2, но в отличие от метода 2, который говорит только о том, куда и куда идет фигура, вам также нужно хранить, откуда взялась каждая фигура.
Реализация:
Поскольку это работает для всех видов состояний, со всеми видами функций, для конкретной игры, вы можете сделать несколько предположений, которые облегчат реализацию. На самом деле, если вы реализуете метод 6 со всем состоянием игры, вы сможете не только воспроизвести данные, но и вернуться назад во времени и возобновить игру с любого момента. Это было бы здорово.
Вместо того, чтобы хранить все игровое состояние, вы можете просто сохранить необходимый минимум, необходимый для рисования данного состояния, и сериализовать эти данные каждый фиксированный промежуток времени. Ваши состояния будут этими сериализациями, а ваш вклад теперь будет разницей между двумя сериализациями. Ключ к тому, чтобы это работало, заключается в том, что сериализация должна мало меняться, если состояние мира тоже мало меняется. Это различие полностью обратимо, поэтому реализация метода 5 с закладками очень возможна.
Я видел, как это реализовано в некоторых крупных играх, в основном для мгновенного воспроизведения последних данных, когда происходит событие (фрагмент в кадрах в секунду или счет в спортивных играх).
Надеюсь, это объяснение не было слишком скучным.
¹ Это не означает, что некоторые программы действуют как недетерминированные (например, MS Windows ^^). Если серьезно, если вы можете создать недетерминированную программу на детерминированном компьютере, вы можете быть уверены, что одновременно выиграете медаль Филдса, премию Тьюринга и, возможно, даже Оскара и Грэмми за все, что стоит.