U
11 / 22 · 9 phút
Hỏi đáp

Singleton: một GameManager mà cả game gọi được

câu trả lời ngắn

Singleton là gì, và nên viết GameManager thế nào?

Singleton là một object duy nhất đang sống, kèm một trường static trỏ tới nó, nên script nào cũng gọi được GameManager.Instance mà không cần giữ tham chiếu. Gán trường đó trong Awake, đọc trong Start, huỷ bản thứ hai sinh ra khi nạp lại scene, và chỉ gọi DontDestroyOnLoad khi manager thật sự phải sống lâu hơn scene.

Điểm số cần được cộng từ con quái vừa chết, từ menu tạm dừng và từ file save. Chẳng thứ nào trong số đó sở hữu cái manager đang giữ điểm. Singleton là câu trả lời quen thuộc: một object duy nhất, một trường static trỏ tới nó, và script nào cũng gọi được bằng tên.

Toàn bộ mẫu code

GameManager.cs — a singleton that survives a scene change
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Trường duy nhất mà cả game dùng để với tới nó.
    public static GameManager Instance { get; private set; }

    public int Score { get; private set; }

    void Awake()
    {
        // Quay lại một scene vốn đã có sẵn một cái? Thì đây là bản thừa.
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;

        // Chỉ dành cho manager phải sống lâu hơn scene. Không cần thì bỏ dòng này.
        DontDestroyOnLoad(gameObject);
    }

    void OnDestroy()
    {
        // Đừng để trường static trỏ vào một object đã bị huỷ.
        if (Instance == this) Instance = null;
    }

    public void AddScore(int points) => Score += points;
}

Ở bất cứ chỗ nào khác trong game, giờ chỉ còn một dòng: GameManager.Instance.AddScore(10). Không cần trường public để kéo thả trong Inspector, cũng không phải lùng object đó trong cảnh.

Bốn nguyên tắc giữ cho nó chạy đúng

  • Gán Instance trong Awake, đọc nó trong Start. Unity chạy hết mọi Awake trong cảnh rồi mới tới Start đầu tiên, nên script nào đọc manager trong Start cũng luôn tìm thấy. Hai script đọc lẫn nhau ngay trong Awake là cuộc đua không ai thắng.

  • Huỷ bản trùng, đừng huỷ bản gốc. Nạp menu rồi quay lại là sinh ra manager thứ hai; bản tới sau mới là bản phải đi, không thì mọi tham chiếu đã phát ra đều trỏ vào một object đã chết.

  • DontDestroyOnLoad chỉ có tác dụng với object gốc. Đặt trên object con thì Unity bỏ qua và ghi cảnh báo — còn manager thì vẫn chết theo scene như thường.

  • Đừng gắn DontDestroyOnLoad theo thói quen. Nếu manager thuộc về đúng một scene, cứ để nó chết theo scene đó: manager sống lâu hơn màn chơi mà nó đang đếm chính là nguồn gốc của điểm số cộng dồn lung tung và kẻ địch bị đếm hai lần.

Một cái bẫy trong editor

Nếu bạn tắt Domain Reload để vào Play Mode cho nhanh, các trường static vẫn giữ nguyên giá trị từ lần dừng trước — nên Instance vẫn trỏ vào một object của phiên chạy cũ, và khung hình đầu tiên là ném lỗi. Hãy reset nó một cách tường minh, kiểu vào nhanh đó sẽ an toàn.

GameManager.cs — two lines that make Play Mode without a domain reload safe
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void ResetStatics() => Instance = null;

Khi nào đừng dùng tới nó

  • Thứ gì có thể tồn tại hai bản — nhân vật trong chế độ chia đôi màn hình, kẻ địch, vũ khí — thì không phải singleton, và ép nó thành singleton là tự hẹn trước một lần viết lại.

  • Khi chỉ một script cần tới manager, một trường public kéo thả trong Inspector là đủ và đơn giản hơn, mà nhìn Inspector cũng thấy ngay cái gì nối với cái gì.

  • Các manager với qua nhau bằng Instance rồi sẽ tới lúc không test nổi, cũng chẳng tái sử dụng được. Hai ba cái trong một dự án là bình thường; cả chục cái nghĩa là game đã không còn cấu trúc nào.

bài học làm ra thứ này

Screen, panel và GameManager

Bài này là một công thức đứng riêng. Trong khoá học, cùng thứ đó được dựng như một phần của dự án xuyên suốt năm tab — Căn bản, bài 10.