Играчите прощават много: тромаво меню, труден етап, дори срив. Не прощават загубени двайсет часа игра. В Godot първата система за записи, която повечето от нас пишат, е три реда: отваряме файл, пишем JSON, затваряме. Месеци наред работи на компютъра на разработчика. После батерията на нечий лаптоп свършва по време на автоматичен запис или играта се обновява и старият запис вече не пасва, а следващото ревю е за загубен напредък. Тук показваме как се чупят записите и как да ги пишете така, че да оцеляват при сривове, ръчни промени и обновления, с GDScript, който пуснахме в Godot 4.7.1.
#Как се губят записи
- Срив по средата на записа.
FileAccess.open(path, FileAccess.WRITE)изпразва файла в момента, в който го отвори. Ако играта се срине, токът спре или телефонът затвори приложението, преди новото съдържание да е записано, остава празен или отрязан файл, а старият запис вече го няма. - Стойности, които сменят типа си.
JSON.stringify()не познава типовете на Godot.Vector2(3, 4)става низът"(3.0, 4.0)", а всяко число се връща като дробно, така че брой 3 в инвентара се връща като 3.0. - Обновления на играта. Версия 1.1 преименува поле или разделя „coins“ на „gold“ и „silver“. Старите записи се зареждат без грешка, а играчът започва без нищо.
- Ръчно променени файлове. Някои играчи отварят файла в текстов редактор. Някои редактори го записват с друго кодиране или играчът изтрива скоба.
Нищо от това не излиза при тестване, освен ако не го тествате нарочно.
#Пазете типовете на Godot с JSON.from_native
От Godot 4.4 насам JSON.from_native() превръща всяка стойност във JSON, който помни типа ѝ, а JSON.to_native() я връща обратно. Vector2 се пази като {"type": "Vector2", "args": [3.0, 4.0]}, целите числа остават цели, а речници с ключове Vector2i или цели числа оцеляват. Обектите се пропускат на връщане, освен ако не ги разрешите, и точно това искате: файл със запис никога не бива да може да създаде обект с код в него.
JSON не е единственият вариант. FileAccess.store_var() пише двоичния формат на Godot, който е по-малък, но не се чете, когато нещо се обърка. За записите четимият JSON си струва размера: когато играч ви изпрати счупен файл, можете да го отворите. Настройките са в отделен ConfigFile, така че счупен запис никога да не нулира звука или клавишите на играча.
#Пишете във временен файл, проверете го и после разменете
Безопасният подход е стар и прост: никога не пишете върху единственото добро копие. Запишете новия запис до него, уверете се, че е цял, пазете предишния като резервно копие и едва тогава сложете новия на мястото му. Контролна сума във файла ви позволява по-късно да различите цял запис от отрязан или променен:
const BACKUPS := 2
func save_game(path: String, data: Dictionary) -> Error:
var body := JSON.stringify(JSON.from_native(data))
var text := JSON.stringify({"version": 1, "sha256": body.sha256_text(), "body": body})
var tmp := path + ".tmp"
var f := FileAccess.open(tmp, FileAccess.WRITE)
if f == null:
return FileAccess.get_open_error()
f.store_string(text)
f.flush()
f.close()
# Read it back before trusting it.
if FileAccess.get_file_as_string(tmp) != text:
DirAccess.remove_absolute(tmp)
return ERR_FILE_CORRUPT
# Keep older copies: save.json.1 is the previous save, save.json.2 the one before.
for i in range(BACKUPS - 1, 0, -1):
if FileAccess.file_exists(path + "." + str(i)):
DirAccess.rename_absolute(path + "." + str(i), path + "." + str(i + 1))
if FileAccess.file_exists(path):
DirAccess.rename_absolute(path, path + ".1")
return DirAccess.rename_absolute(tmp, path)
Ако нещо се обърка преди последния ред, предишният запис още е на диска, като основен файл или като първо резервно копие. Ползвайте пътища под user://: Godot ги насочва към правилната папка на всяка система, например %APPDATA%\Godot\app_userdata\ под Windows и ~/.local/share/godot/app_userdata/ под Linux, освен ако проектът не задава собствено име на папката.
#Зареждайте най-новия цял запис
При зареждането контролната сума се отплаща. Опитайте основния файл, а ако липсва, отрязан е или е променен, минете към резервните копия по ред:
func read_save(path: String) -> Variant:
if not FileAccess.file_exists(path):
return null
var env = JSON.parse_string(FileAccess.get_file_as_string(path))
if typeof(env) != TYPE_DICTIONARY or not env.has("body") or not env.has("sha256"):
return null
if String(env["body"]).sha256_text() != env["sha256"]:
return null
return JSON.to_native(JSON.parse_string(env["body"]))
func load_game(path: String) -> Dictionary:
for candidate in [path, path + ".1", path + ".2"]:
var data = read_save(candidate)
if data is Dictionary:
return data
return {}
Пуснахме двете функции без прозорец в Godot 4.7.1. След два записа отрязахме основния файл по средата, както би го оставил срив; load_game() върна предишния запис от първото резервно копие, с непокътнати Vector2 и Color. Файл, чието съдържание вече не отговаряше на контролната сума, беше отхвърлен. Кажете на играча, когато това се случи („Последният ви запис беше повреден; заредихме предишния“), и запишете възстановения запис обратно като основен файл, за да е чисто следващото пускане.
Контролната сума не пази от измама: всеки, който прочете кода, може да я изчисли. Тя спира случайностите, а случайностите губят напредък. Ако искате да затрудните ръчните промени, Godot може да пише и криптирани файлове с парола, но това е неравност по пътя, а не ключалка.
#Номерирайте версиите на данните още от първата версия на играта
Слагайте номер на версията във всеки запис, като започнете от 1, и пазете по една малка функция за всяка промяна:
- от версия 1 към 2: „coins“ става „gold“;
- от версия 2 към 3: добавя се „silver“ със стойност 0 по подразбиране.
При зареждане изпълнете стъпките една след друга, докато данните станат текущи, и ги запишете в новия формат. Запис от по-нова версия на играта, което се случва, когато играч сменя бета и стабилния клон, трябва да се откаже с ясно съобщение, вместо да се прочете грешно и да се презапише.
#Кога да записвате
Записвайте в моменти, които значат нещо: контролна точка, край на ниво, затваряне на меню, покупка. Добавете автоматичен запис на интервал за дългите сесии, в собствен слот, за да не презаписва запис, който играчът е направил нарочно. И записвайте, когато играта излиза: NOTIFICATION_WM_CLOSE_REQUEST на компютър и NOTIFICATION_APPLICATION_PAUSED на Android и iOS, където системата може да затвори спряно приложение без предупреждение.
После тествайте провалите нарочно: спрете процеса по време на запис, отрежете файл наполовина, сменете едно число в него и заредете запис от предишната си версия. Всеки случай трябва да завърши с работеща игра и с най-новия напредък, който е можело да бъде запазен.
#Добавка, която прави всичко това
Събрахме тези правила в Skrinka, добавка за Godot 4 със слотове за записи, настройки на играча и готови менюта. Записите минават през временен файл, проверка след запис и контролна сума SHA-256, с до девет резервни копия, а повреден слот се възстановява от най-новото добро копие със сигнал, който можете да покажете на играча. Стойностите пазят типовете си в Godot, миграции обновяват старите записи стъпка по стъпка, а слотовете имат миниатюри, време на игра и автоматичен запис по желание. Настройките се прилагат сами, играчите могат да сменят клавишите и бутоните на геймпада, а менютата работят на английски и български с мишка, клавиатура, геймпад и докосване. Тествана е с 26 теста без прозорец в Godot 4.7.1 на Linux и е обявена като „Очаквайте скоро“.
Нашите безплатни игри за компютър и телефон, например Tumblekeep и Nudgeholt, са направени с Godot за Windows, Linux и Android и те също пазят напредъка и настройките в два отделни файла, така че проблем с единия никога не струва другия.
#Често задавани въпроси
Безопасен ли е JSON за записи или да ползвам двоичен формат?
JSON с JSON.from_native() пази типовете на Godot и остава четим, което помага, когато играч ви изпрати счупен файл. Двоичният формат е по-малък. И в двата случая не разрешавайте обекти при зареждане.
Спира ли контролната сума измамите?
Не. Всеки, който прочете кода ви, може да я изчисли наново. Тя хваща повредени и случайно променени файлове, а това е нейната работа.
Колко резервни копия да пазя?
Две са добро начало: предишният запис и този преди него. Записите са малки, така че повече не струва много, но най-новото цяло копие почти винаги е това, което играчите искат.
Къде Godot пази записите на компютъра на играча?
Под user://, което Godot насочва към папка за всяка игра, например под %APPDATA% под Windows и ~/.local/share под Linux. Кажете на играчите къде е, за да могат да си я пазят.
Коментари
Коментари
Напишете първия коментар.
Оставете коментар