Amikor Scene-eket és a hozzájuk szükséges minden beállítást állítjuk elő Unity-ben, akkor valójában egy kezdeti állapotát hozzuk létre a játékunknak. Futás közben ez a játékállapot változik, a játékos eredményeket érhet el, előre-haladhat. Ezen eredmények a futó szoftver aktuális állapotában jelennek meg, amely a számítógép memóriájában tárolódik. Ez a memória a szoftver bezárása után törlődik. Ennek az az eredménye, hogy a játék később csakis a kezdeti állapotból tud újra elindulni.
Ha ezt el akarjuk kerülni és azt szeretnénk, hogy a játékállapot részben megmaradjon a program különböző futtatásai közt, akkor manuálisan ki kell építenünk rendszereket, amik bizonyos információkat eltárolnak a számítógép hosszútávú memóriájában (SSD, HDD). Ez más játékmotor alatt is pontosan így működik.
Ezen információk eltárolását mentésnek (saving), visszaállítását pedig betöltésnek (loading) nevezzük.
Player Prefs
A Player Prefs rendszere a legegyszerűbb eszköz Unity-ben a mentésre és betöltésre.
3 típusú adatot lehet menteni és betölteni a PlayerPrefs segítségével: int, float és string.
Ezen adatok közül bármelyiket lehetőség van elmenteni egy szöveges (string típusú) kulcshoz, majd az értéket a kulcs segítségével visszatölteni.
Mentés | Betöltés | |
int | void SetInt(string key, int value) | int GetInt(string key) |
float | void SetFloat(string key, float value) | float GetFloat(string key) |
string | void SetString(string key, string value) | string GetString(string key) |
A PlayerPrefs az adott kulcsokat és a hozzá szükséges adatokat mindig egy adott játékhoz kapcsolva mentii, tehát ugyanazon kulcs létezhet külön játékokhoz, anélkül, hogy a két rendszer összeütközésbe kerülne.
A kulcsok mentésének módja és helye platformonként változik. Mindennek utánanézhettek a hivatalos dokumentációban.
A következő scrip például menti a pontszámot (score), akkor amikor az objektum megszűnik létezni és visszatölti azt amikor létrejön.
using UnityEngine;
public class ScoreHolder : MonoBehaviour
{
const string scoreKey = "Score"; // Egy konstans kulcs string az mentéshez
int score = 0; // Ezt az adatot mentjük
public void AddToScore(int value) // Játék közben módosulhat a pontszám
{
score += value;
}
void Awake() // A játék kezdetekor (ScoreHolder komponens létrejöttekor)...
{
score = PlayerPrefs.GetInt(scoreKey); // ...betöltjük a mentett adatot.
}
void OnDestroy() // A játék végén (ScoreHolder komponens törlésekor)...
{
PlayerPrefs.SetInt(scoreKey, score); // ...mentjük az adatot.
}
}
A lekérdező metódusok az adott típus default értékével térnek vissza, ha a megadott kulcs (key) még nem létezik, ahelyett, hogy a rendszer hibát jelezne, kivételt dobna (Kivételkezelés (Hamarosan)).
A mentés mindig a játék végén történik csak meg automatikusan. Ha korábban is szeretnénk manuálisan menteni, akkor hívjuk meg a PlayerPrefs.Save()
metódust!
Egy kulcs létezését (vagyis azt, hogy el lett-e már adat mentve az adott kulcshoz) le lehet kérdezni a PlayerPrefs.HasKey(string key)
függvénnyel, ami egy bool értékkel tér vissza.
Kulcsot törölni lehet egyenként is de egyszerre mindet is a következő parancsokkal:
PlayerPrefs.DeleteKey(string key)
: Törli az adott kulcshoz tartozó mentést.
PlayerPrefs.DeleteAll(string key)
: Törli az adott játékhoz tartozó minden mentést.
Szerializáció és fájlba mentés
A Player Prefs használata különösen egyszerű viszont pont ezért néha nem elégséges komplexebb játékokhoz, amik fejlettebb mentési rendszert igényelnek.
Sokkal kényelmesebb nem egyenkén menteni és betölteni int, float, string adatokat külön kulccsal, hanem ehelyett a mentést és betöltést egyben összetett objektumokon (osztályok és struktúrák példányain) végezni.
Ehhez szükségünk van egy műveletre, amivel bármilyen objektumot átalakíthatunk menthető adattá, majd később vissza C# objektummá.
Ezt a műveletet szerializációnak és deszerializációnak nevezik.
A szerializáció az a folyamat, amikor egy programozási objektumból nyelv- és platformfüggetlen adatot készítünk.
Ezt a szerializált adatott aztán többek között elmenthetjük fájlba vagy továbbíthatjuk hálózaton keresztül.
A szerializált adatokat később deszerializálás során lehet visszaállítani az eredeti formájukra, azaz programozási nyelvek objektumaivá.
A Unity is a szerializálást használja arra, hogy információkat mentsen jelenet és prefab file-okba. Ehhez a Unity-be épített szerializátor osztályát használja: UnityEngine.JsonUtility.
Ehhez az osztályhoz mi is hozzáférünk és használhatjuk saját célra, például mentésre is.
A következő osztály egy példa az objektumok szerializálására és külön fájlba történő mentésére JsonUtility segítségével. (A példakód egyéb segéd metódusokat is tartalmaz a mentésen és betöltésen kívül)
Figyeljük meg, hogy hogyan használom az Unity szerializátort és olyan fájlműveleteket: (System.IO névtér) mint mentés fájlba vagy beolvasás fájlból!
(A lenti egy statikus osztály és tartalmaz generikus metódusokat. Bővebben: Statikus tagok és osztályok, Generikusok (Hamarosan) )
using System.IO;
using UnityEngine;
public static class ObjectSaver
{
// Tetszőleges objektum mentése fájlba
public static void SaveToFile(string filePath, object data)
{
// Elérési útvonal létrehozása
string path = FullPath(filePath);
// Adat szerializálás JSON stringbe
string json = JsonUtility.ToJson(data);
// JSON string írása fájlba
File.WriteAllText(path, json);
}
// Lekérdezés: Létezik az adott mentési fájl?
public static bool HaveFileToLoad(string filePath)
{
string path = FullPath(filePath);
return File.Exists(path);
}
// Mentett fájl betöltésse: Visszaalakítás C# objektummá: Deszerializáció
public static T LoadFromFile<T>(string filePath)
{
// Elérési útvonal létrehozása
string path = FullPath(filePath);
// ha a file nem létezik,
if (!File.Exists(path))
{
// akkor a generikus típus alapértelmezett értékével térünk vissza:
return default;
}
// A file beolvasásam JSON string-be
string json = File.ReadAllText(path);
// Deszerializálás és visszatérés az adattal
return JsonUtility.FromJson<T>(json);
}
// Adott mentési fájl törlése
public static void DeleteFile(string filePath)
{
string path = FullPath(filePath);
File.Delete(path);
}
// Az összes mentett fájl tölése
public static void DeleteSaveDirectory()
{
DirectoryInfo dir = new DirectoryInfo(Application.persistentDataPath);
// Minden File törlése
foreach (FileInfo file in dir.GetFiles())
{
file.Delete();
}
// Minden mappa törlése
foreach (DirectoryInfo innerDir in dir.GetDirectories())
{
innerDir.Delete(true);
}
}
// Elérési útvonal
static string FullPath(string filePath)
{
return Path.Combine(Application.persistentDataPath, filePath + ".json");
}
}
Ha szeretnéd használni a fentieket, először add hozzá a szkript-et a projektedhez egy új ObjectSaver.cs fájlban.
Ezután a következő módon tudod kódból összetett objektumokat menteni külön file-ba és beolvasni őket.
// létrehozunk valamiféle adatot.
List<string> data = new List<string> { "hello", "world" };
// Elentjük az adatot a "savedList" nevű file-ba
ObjectSaver.SaveToFile("savedList", data);
// Visszatöltjök a "savedList" file tartalmát a newData változóba.
List<string> newData = ObjectSaver.LoadFromFile<List<string>>("savedList");
// Visszatöltésnél generikus paraméterként meg kell adni az adatt típusát.
//Pl.:
int intData = ObjectSaver.LoadFromFile<int>("intDataFileName");
string stringData= ObjectSaver.LoadFromFile<string>("stringDataFileName");
Vector3 vector3Data = ObjectSaver.LoadFromFile<Vector3>("vector3DataFileName");
A fenti rendszer szerializálásra a Unity beépített rendszerét használja.
Ebben a rendszerben a szerializálás szabályai (mi menthető és mi nem) a következőek:
- Képes a rendszer bármilyen primitív típust szerializálni.
- Képes a rendszer bármilyen enum típust szerializálni.
- Képes a rendszer bármilyen saját definiált típust szerializálni, ami meg van jelöleve a [System.Serializable] attribútummal.
- A rendszer az összetett típusok azon mezőit szerializálja, amik publikusak vagy meg vannak jelölve a [SerializaField] attribútummal.
- Képes a rendszer bármilyen szerializálható típus tömbjét vagy listáját szerializálni.
- Ez alól kivétel a tömbök és a listák maguk. Tehát pl. listák listáját nem lehet szerializálni. Ez megkerülhető, ha a belső listát először egy saját magunk által definiált összetett típusba (class, struct) tesszük. és abból a típusból teszünk példányokat a külső tömbbe vagy listába.
Ha a fenti szabályok túlzottan korlátozóak a te adott játékodhoz, használhatsz egyéb szerializáló könyvtárakat is.
A UnityEngine.JsonUtility kifejezetten egy JSON nevű szabványnak megfelelő szöveges adatot készít. Ez a szabvány ember számára könnyedén olvashatóm és szerkeszthető, tehát a felhasználó viszonylag könnyen módosíthatja manuálisan a mentési fájlt.
Ha ezt szeretnénk elkerülni, akkor javasolt nem szöveges, hanem bináris szerializátort használni.