Ви можете переглядати свою систему так, ніби вона складається з ряду станів і функцій, де функція f[j]
з введенням x[j]
перетворює стан системи s[j]
в стан s[j+1]
, наприклад:
s[j+1] = f[j](s[j], x[j])
Держава - це пояснення всього вашого світу. Розташування гравця, місце розташування противника, рахунок, боєприпаси, що залишилися тощо. Все, що вам потрібно, щоб намалювати рамку вашої гри.
Функція - це все, що може впливати на світ. Зміна кадру, натискання клавіші, мережевий пакет.
Вхід - це дані, які приймає функція. Зміна кадру може зайняти кількість часу з моменту проходження останнього кадру; натискання клавіш може включати фактично натиснуту клавішу, а також, натиснули чи ні клавішу зсуву.
Заради цього пояснення я буду робити такі припущення:
Припущення 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.
Для шахів це можна було б виконати, намалювавши кожен хід, а також по кожній з 10 дощок (для 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 ^^). Тепер серйозно, якщо ви можете зробити недетерміновану програму на детермінованому комп’ютері, ви можете бути впевнені, що одночасно виграєте медаль «Філдс», премію Тьюрінга і, мабуть, навіть «Оскар» та «Греммі» за все, що варто.