Към съдържанието
PN Scripts

Игри

Система за записи в Godot 4, която оцелява при сривове и обновления

  • PN Scripts
  • 8 мин четене

Играчите прощават много: тромаво меню, труден етап, дори срив. Не прощават загубени двайсет часа игра. В 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. Кажете на играчите къде е, за да могат да си я пазят.

#Източници

Коментари

Коментари

Напишете първия коментар.

Оставете коментар

Следваща стъпка

pnscripts.com/bg/contact

Пишете на екипа

Попитайте за статия или ни разкажете за проект, който искате да изградим. Отговаряме до един работен ден.

Пишете ни

Семейството PN Scripts

Другите сайтове на PN Scripts

Хостингът, игрите и блогът имат свои сайтове, поддържани от същата компания.

  • pnscripts.com

    PN Scripts

    Софтуерно инженерство

    Уеб и мобилни приложения, API и игри по поръчка, плюс нашите продукти и плъгини с отворен код.

  • games.pnscripts.com

    Игри

    Игри и игрови сървъри

    Домът на игрите на PN Scripts: безплатни браузърни игри, които се играят веднага, а игровите сървъри предстоят.

  • hosting.pnscripts.com

    Хостинг

    Хостинг и инфраструктура

    Споделен хостинг, KVM VPS, самостоятелни сървъри и домейни от същата компания, която изработва проекта ви.

  • blog.pnscripts.com

    Блог

    Статии и бележки

    Практични статии за разработка на софтуер, хостинг, отворен код и игри, писани от хората, които ги създават.

    Вие сте тук