U
tab 05Xuất lên điện thoại7 bài — chạm để mở
04 / 07 · 10 phút
Xuất lên điện thoại

Lưu tiến trình

dự án của bạn tới lúc này

Assets/

Scenes/

Arena.unity

Knight

Scripts/

CharacterStats.csEquipment.csSaveData.csSaveSystem.csGameSession.cs

đã có sẵnbài này thêm

Game mobile bị ngắt liên tục: cuộc gọi, thông báo, hệ điều hành tắt app để giải phóng bộ nhớ. Nếu đóng app là mất tiến trình thì không ai quay lại chơi nữa.

sơ đồ động

Biến một class C# thường thành JSON, ghi vào persistentDataPath, mở game lên thì đọc lại.

Dùng PlayerPrefs hay file?

✓ PlayerPrefs

Cho cài đặt

Chỉ ba kiểu (int, float, string), không có cấu trúc, lưu trong registry / file plist. Hợp với âm lượng và đảo trục Y. Rất tệ để lưu trạng thái game.

✓ JSON file

Cho tiến trình chơi

Cả một cây dữ liệu trong một file, mở ra xem được, sao lưu được, đánh version được. Đây mới đúng nghĩa là file save.

Bước 1 — một class dữ liệu thuần

Class lưu trữ chỉ chứa giá trị, tuyệt đối không chứa MonoBehaviour. Bạn không serialise được GameObject — hãy ghi lại đủ thông tin để dựng lại nó.

SaveData.cs
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class SaveData
{
    public int version = 1;              // tăng khi đổi cấu trúc

    public int level = 1;
    public float health = 100f;
    public int gold;
    public float playTimeSeconds;

    public string sceneName = "Arena";
    public Vector3 playerPosition;       // Vector3 thì Unity serialize được bình thường

    public List<string> ownedArtifacts = new List<string>();
    public List<string> equippedArtifacts = new List<string>();

    public string savedAtUtc = "";
}

Bước 2 — ghi và đọc

SaveSystem.cs
using System;
using System.IO;
using UnityEngine;

public static class SaveSystem
{
    // thư mục DUY NHẤT được phép ghi trên iOS và Android
    private static string Path => System.IO.Path.Combine(Application.persistentDataPath, "save.json");

    public static bool Exists => File.Exists(Path);

    public static void Save(SaveData data)
    {
        data.savedAtUtc = DateTime.UtcNow.ToString("o");

        string json = JsonUtility.ToJson(data, prettyPrint: true);
        string temp = Path + ".tmp";

        //ghi ra file tạm rồi mới tráo vào
        // nếu máy tắt giữa chừng thì file save cũ vẫn còn nguyên
        File.WriteAllText(temp, json);
        File.Copy(temp, Path, overwrite: true);
        File.Delete(temp);

        Debug.Log("Saved to " + Path);
    }

    public static SaveData Load()
    {
        if (!Exists) return new SaveData();          // lần chạy đầu tiên

        try
        {
            string json = File.ReadAllText(Path);
            SaveData data = JsonUtility.FromJson<SaveData>(json);
            return Migrate(data);
        }
        catch (Exception e)
        {
            //file hỏng không được làm chết game
            Debug.LogError("Save file unreadable, starting fresh: " + e.Message);
            File.Move(Path, Path + ".broken");
            return new SaveData();
        }
    }

    public static void Delete()
    {
        if (Exists) File.Delete(Path);
    }

    /// bản save cũ vẫn phải chạy được sau khi bạn cập nhật game
    private static SaveData Migrate(SaveData data)
    {
        if (data.version < 1)
        {
            data.gold = 0;
            data.version = 1;
        }
        return data;
    }
}
  • Application.persistentDataPath là thư mục duy nhất ghi được trên điện thoại. Application.dataPath nằm trong gói app và chỉ đọc.

  • Mẹo ghi ra file tạm rồi tráo bảo vệ bạn khỏi file save ghi dở khi hệ điều hành tắt app giữa chừng.

  • Luôn bọc Load trong try/catch. Người dùng có file hỏng mà không mở nổi game sẽ để lại đánh giá một sao.

  • Trường version hôm nay không tốn gì, nhưng cứu bạn vào ngày bạn thêm trường mới ở bản 1.3.

Bước 3 — lưu vào đúng lúc

Đừng lưu mỗi khung hình, cũng đừng chỉ lưu khi người chơi bấm nút — trên điện thoại họ sẽ không kịp bấm đâu.

GameSession.cs
using UnityEngine;

public class GameSession : MonoBehaviour
{
    public static GameSession Instance { get; private set; }
    public SaveData Data { get; private set; }

    [SerializeField] private CharacterStats playerStats;   //chính là Knight
    [SerializeField] private Transform player;

    void Awake()
    {
        if (Instance != null) { Destroy(gameObject); return; }
        Instance = this;
        DontDestroyOnLoad(gameObject);

        Data = SaveSystem.Load();
        Apply(Data);
    }

    void Update() => Data.playTimeSeconds += Time.deltaTime;

    void Collect()
    {
        Data.health = playerStats.currentHealth;
        Data.playerPosition = player.position;
        Data.sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
    }

    void Apply(SaveData d)
    {
        playerStats.currentHealth = d.health;
        if (d.playerPosition != Vector3.zero) player.position = d.playerPosition;
    }

    /// chạy khi app bị đưa xuống nền — TRÊN MOBILE ĐÂY MỚI LÀ CÁI QUAN TRỌNG
    void OnApplicationPause(bool paused)
    {
        if (paused) { Collect(); SaveSystem.Save(Data); }
    }

    //trên máy tính và khi thoát bình thường
    void OnApplicationQuit()
    {
        Collect();
        SaveSystem.Save(Data);
    }
}

Điểm lưu, đừng lưu liên tục

  • Lưu khi người chơi sang khu vực mới, kết thúc trận đánh, mua đồ, hoặc app bị đưa xuống nền.

  • Ghi một file JSON nhỏ mất chừng một mili giây. Nhưng ghi 60 lần mỗi giây vẫn làm mòn bộ nhớ flash.

  • Hiện một dấu 'đã lưu' nhỏ trong nửa giây. Người chơi không tin những game lưu âm thầm.

Tìm file để gỡ lỗi

terminal
#in đường dẫn từ trong game
Debug.Log(Application.persistentDataPath);

#lấy file save từ máy về
adb shell run-as com.yourstudio.yourgame cat files/save.json

#đường dẫn khi chạy trong editor