Trong Unity, làm cách nào để triển khai chính xác mẫu singleton?


36

Tôi đã thấy một số video và hướng dẫn để tạo các đối tượng singleton trong Unity, chủ yếu là cho a GameManager, dường như sử dụng các cách tiếp cận khác nhau để khởi tạo và xác thực một singleton.

Có một cách tiếp cận chính xác, hay đúng hơn, ưa thích cho điều này?

Hai ví dụ chính tôi đã gặp là:

Đầu tiên

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = GameObject.FindObjectOfType<GameManager>();
            }

            return _instance;
        }
    }

    void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }
}

Thứ hai

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                instance = new GameObject("Game Manager");
                instance.AddComponent<GameManager>();
            }

            return _instance;
        }
    }

    void Awake()
    {
        _instance = this;
    }
}

Sự khác biệt chính tôi có thể thấy giữa hai là:

Cách tiếp cận đầu tiên sẽ cố gắng điều hướng ngăn xếp đối tượng trò chơi để tìm một thể hiện của GameManagerđiều đó mặc dù điều này chỉ xảy ra (hoặc chỉ nên xảy ra) một lần có vẻ như nó có thể không được đánh giá cao khi các cảnh tăng kích thước trong quá trình phát triển.

Ngoài ra, cách tiếp cận đầu tiên đánh dấu đối tượng không bị xóa khi ứng dụng thay đổi cảnh, điều này đảm bảo rằng đối tượng được duy trì giữa các cảnh. Cách tiếp cận thứ hai dường như không tuân thủ điều này.

Cách tiếp cận thứ hai có vẻ kỳ lạ như trong trường hợp cá thể là null trong getter, nó sẽ tạo ra một GameObject mới và gán một thành phần GameManger cho nó. Tuy nhiên, điều này không thể chạy mà không có thành phần GameManager đầu tiên được gắn vào một đối tượng trong cảnh, vì vậy điều này gây cho tôi một số nhầm lẫn.

Có cách tiếp cận nào khác sẽ được đề xuất, hoặc kết hợp cả hai cách trên không? Có rất nhiều video và hướng dẫn về singletons nhưng tất cả chúng đều khác nhau đến mức khó có thể đưa ra bất kỳ so sánh nào giữa hai và do đó, một kết luận về cách nào là cách tiếp cận tốt nhất / ưa thích.


GameManager nói phải làm gì? Nó có phải là một GameObject không?
bummzack

1
Đây thực sự không phải là một câu hỏi về những gì GameManagernên làm, hơn là làm thế nào để đảm bảo chỉ có một trường hợp của đối tượng và cách tốt nhất để thực thi điều đó.
CaptainRedmuff

hướng dẫn này được giải thích rất độc đáo, cách triển khai singleton unitygeek.com/unity_c_singleton , tôi hy vọng nó hữu ích
Rahul Lalit

Câu trả lời:


30

Nó phụ thuộc, nhưng thường tôi sử dụng một phương pháp thứ ba. Vấn đề với các phương thức mà bạn đã sử dụng là trong trường hợp bắt đầu có đối tượng, nó sẽ không xóa chúng khỏi cây và chúng vẫn có thể được tạo bằng cách khởi tạo quá nhiều cuộc gọi, điều này có thể khiến mọi thứ thực sự khó hiểu.

public class SomeClass : MonoBehaviour {
    private static SomeClass _instance;

    public static SomeClass Instance { get { return _instance; } }


    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(this.gameObject);
        } else {
            _instance = this;
        }
    }
}

Vấn đề với cả hai triển khai của bạn là chúng không phá hủy một đối tượng được tạo sau này. Nó có thể hoạt động, nhưng người ta có thể ném một chiếc mỏ lết khỉ vào các tác phẩm có thể dẫn đến một lỗi rất khó để gỡ lỗi xuống dòng. Đảm bảo kiểm tra Awake nếu đã có một phiên bản và nếu vậy, phá hủy phiên bản mới.


2
Bạn cũng có thể muốn OnDestroy() { if (this == _instance) { _instance = null; } }, nếu bạn muốn có một trường hợp khác nhau trong mỗi cảnh.
Dietrich Epp

Thay vì Tiêu diệt () trong GameObject, bạn nên đưa ra một lỗi.
Doodlemeat

2
Có thể. Bạn có thể muốn đăng nhập nó, nhưng tôi không nghĩ rằng bạn nên đưa ra một lỗi, trừ khi bạn đang cố gắng làm điều gì đó rất cụ thể. Có nhiều trường hợp tôi có thể tưởng tượng rằng việc đưa ra một lỗi thực sự sẽ gây ra nhiều vấn đề hơn thì nó sẽ sửa.
PearsonArtPhoto

Bạn có thể muốn lưu ý rằng MonoBehaviour được đánh vần bằng cách đánh vần tiếng Anh bởi Unity ("MonoBehavior" sẽ không được biên dịch - Tôi làm điều này mọi lúc); mặt khác, đây là một số mã tốt.
Michael Eric Oberlin

Tôi biết rằng tôi đến muộn, nhưng chỉ muốn chỉ ra rằng, câu trả lời đơn này không tồn tại trong trình tải lại của trình soạn thảo, bởi vì thuộc tính tĩnh Instancebị xóa. Một ví dụ về một câu trả lời không thể tìm thấy trong một trong những câu trả lời dưới đây hoặc wiki.unity3d.com/index.php/Singleton (có thể đã lỗi thời, nhưng dường như hoạt động từ thử nghiệm của tôi với nó)
Jakub Arnold

24

Đây là một bản tóm tắt nhanh:

                 Create object   Removes scene   Global    Keep across
               if not in scene?   duplicates?    access?   Scene loads?

Method 1              No              No           Yes        Yes

Method 2              Yes             No           Yes        No

PearsonArtPhoto       No              Yes          Yes        No
Method 3

Vì vậy, nếu tất cả những gì bạn quan tâm là truy cập toàn cầu, cả ba đều có được những gì bạn cần. Việc sử dụng mẫu Singleton có thể hơi mơ hồ về việc chúng tôi muốn khởi tạo lười biếng, duy nhất được thực thi hay truy cập toàn cầu, vì vậy hãy chắc chắn suy nghĩ cẩn thận về lý do bạn tiếp cận với singleton và chọn một triển khai có các tính năng đó, đúng hơn hơn là sử dụng một tiêu chuẩn cho cả ba khi bạn chỉ cần một.

(ví dụ: nếu trò chơi của tôi sẽ luôn có Trình quản lý trò chơi, có thể tôi không quan tâm đến việc khởi tạo lười biếng - có thể đó chỉ là quyền truy cập toàn cầu với sự tồn tại được bảo đảm và tính duy nhất mà tôi quan tâm - trong trường hợp đó, một lớp tĩnh giúp tôi hiểu chính xác những tính năng đó, không có cân nhắc tải cảnh)

... nhưng chắc chắn không sử dụng Phương pháp 1 như văn bản. Tìm kiếm có thể được bỏ qua dễ dàng hơn với cách tiếp cận Awake () của Phương thức 2/3 và nếu chúng tôi giữ người quản lý trong các cảnh thì chúng tôi rất có thể muốn giết người trùng lặp, trong trường hợp chúng tôi tải giữa hai cảnh với người quản lý đã có trong đó.


1
Lưu ý: có thể kết hợp cả ba phương thức để tạo ra phương thức thứ 4 có tất cả bốn tính năng.
Draco18s

3
Sự thúc đẩy của câu trả lời này không phải là "bạn nên tìm kiếm một triển khai Singleton làm tất cả" mà là "bạn nên xác định các tính năng bạn thực sự muốn từ đơn này và chọn một triển khai cung cấp các tính năng đó - ngay cả khi việc triển khai đó là hoàn toàn không phải là một người độc thân "
DMGregory

Đó là một điểm tốt DMGregory. Không thực sự là ý định của tôi để đề xuất "đập tan tất cả cùng nhau" mà là "không có gì về các tính năng này ngăn chúng hoạt động cùng nhau trong một lớp duy nhất." tức là "Lực đẩy của câu trả lời này KHÔNG phải đề nghị chọn một. "
Draco18s

17

Việc triển khai tốt nhất một Singletonmẫu chung cho Unity mà tôi biết là (tất nhiên) là của riêng tôi.

Nó có thể làm mọi thứ , và nó làm rất gọn gànghiệu quả :

Create object        Removes scene        Global access?               Keep across
if not in scene?     duplicates?                                       Scene loads?

     Yes                  Yes                  Yes                     Yes (optional)

Ưu điểm khác:

  • an toàn cho chủ đề .
  • Nó tránh các lỗi liên quan đến việc thu thập (tạo) các trường hợp đơn lẻ khi ứng dụng được thoát bằng cách đảm bảo rằng các singletons không thể được tạo sau đó OnApplicationQuit(). (Và nó cũng như vậy với một cờ toàn cầu duy nhất, thay vì mỗi loại singleton có riêng)
  • Nó sử dụng Bản cập nhật Mono của Unity 2017 (gần tương đương với C # 6). (Nhưng nó có thể dễ dàng được điều chỉnh cho phiên bản cổ đại)
  • Nó đi kèm với một số kẹo miễn phí!

Và bởi vì chia sẻ là quan tâm , đây là:

public abstract class Singleton<T> : Singleton where T : MonoBehaviour
{
    #region  Fields
    [CanBeNull]
    private static T _instance;

    [NotNull]
    // ReSharper disable once StaticMemberInGenericType
    private static readonly object Lock = new object();

    [SerializeField]
    private bool _persistent = true;
    #endregion

    #region  Properties
    [NotNull]
    public static T Instance
    {
        get
        {
            if (Quitting)
            {
                Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] Instance will not be returned because the application is quitting.");
                // ReSharper disable once AssignNullToNotNullAttribute
                return null;
            }
            lock (Lock)
            {
                if (_instance != null)
                    return _instance;
                var instances = FindObjectsOfType<T>();
                var count = instances.Length;
                if (count > 0)
                {
                    if (count == 1)
                        return _instance = instances[0];
                    Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] There should never be more than one {nameof(Singleton)} of type {typeof(T)} in the scene, but {count} were found. The first instance found will be used, and all others will be destroyed.");
                    for (var i = 1; i < instances.Length; i++)
                        Destroy(instances[i]);
                    return _instance = instances[0];
                }

                Debug.Log($"[{nameof(Singleton)}<{typeof(T)}>] An instance is needed in the scene and no existing instances were found, so a new instance will be created.");
                return _instance = new GameObject($"({nameof(Singleton)}){typeof(T)}")
                           .AddComponent<T>();
            }
        }
    }
    #endregion

    #region  Methods
    private void Awake()
    {
        if (_persistent)
            DontDestroyOnLoad(gameObject);
        OnAwake();
    }

    protected virtual void OnAwake() { }
    #endregion
}

public abstract class Singleton : MonoBehaviour
{
    #region  Properties
    public static bool Quitting { get; private set; }
    #endregion

    #region  Methods
    private void OnApplicationQuit()
    {
        Quitting = true;
    }
    #endregion
}
//Free candy!

Điều này là khá vững chắc. Đến từ nền tảng lập trình và nền tảng Unity, bạn có thể giải thích lý do tại sao singleton không được quản lý trong hàm tạo chứ không phải trong phương thức Awake không? Bạn có thể có thể tưởng tượng rằng với bất kỳ nhà phát triển nào ngoài đó, nhìn thấy một Singleton được thi hành bên ngoài một nhà xây dựng là một kẻ đột kích lông mày ...
netpoetica

1
@netpoetica Đơn giản. Unity không hỗ trợ các nhà xây dựng. Đó là lý do tại sao bạn không thấy các nhà xây dựng được sử dụng trong bất kỳ lớp kế thừa nào MonoBehaviourvà tôi tin rằng bất kỳ lớp nào được Unity sử dụng trực tiếp nói chung.
XenoRo

Tôi không chắc chắn tôi làm theo cách sử dụng này. Đây có phải chỉ đơn giản là phụ huynh của lớp trong câu hỏi? Sau khi khai báo SampleSingletonClass : Singleton, SampleSingletonClass.Instancequay lại với SampleSingletonClass does not contain a definition for Instance.
Bến I.

@BenI. Bạn phải sử dụng Singleton<>lớp chung . Đó là lý do tại sao cái chung là con của Singletonlớp cơ sở .
XenoRo

Ồ dĩ nhiên rồi! Nó khá rõ ràng. Tôi không chắc tại sao tôi không thấy điều đó. = /
Ben I.

6

Tôi chỉ muốn thêm rằng nó có thể hữu ích để gọi DontDestroyOnLoadnếu bạn muốn singleton của bạn tồn tại qua các cảnh.

public class Singleton : MonoBehaviour
{ 
    private static Singleton _instance;

    public static Singleton Instance 
    { 
        get { return _instance; } 
    } 

    private void Awake() 
    { 
        if (_instance != null && _instance != this) 
        { 
            Destroy(this.gameObject);
            return;
        }

        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    } 
}

Điều đó rất tiện dụng. Tôi chỉ muốn viết bình luận trên @ phản ứng PearsonArtPhoto để hỏi câu hỏi này chính xác:]
CaptainRedmuff

5

Một tùy chọn khác có thể là chia lớp thành hai phần: lớp tĩnh thông thường cho thành phần Singleton và MonoBehaviour hoạt động như một bộ điều khiển cho cá thể singleton. Bằng cách này, bạn có toàn quyền kiểm soát việc xây dựng của người độc thân và nó sẽ tồn tại trong các cảnh. Điều này cũng cho phép bạn thêm bộ điều khiển vào bất kỳ đối tượng nào có thể cần dữ liệu của singleton, thay vì phải tìm hiểu cảnh để tìm một thành phần cụ thể.

public class Singleton{
    private Singleton(){
        //Class initialization goes here.
    }

    public void someSingletonMethod(){
        //Some method that acts on the Singleton.
    }

    private static Singleton _instance;
    public static Singleton Instance 
    { 
        get { 
            if (_instance == null)
                _instance = new Singleton();
            return _instance; 
        }
    } 
}

public class SingletonController: MonoBehaviour{
   //Create a local reference so that the editor can read it.
   public Singleton instance;
   void Awake(){
       instance = Singleton.Instance;
   }
   //You can reference the singleton instance directly, but it might be better to just reflect its methods in the controller.
   public void someMethod(){
       instance.someSingletonMethod();
   }
} 

Điều này là rất tốt đẹp!
CaptainRedmuff

1
Tôi gặp khó khăn trong việc hiểu phương pháp này, bạn có thể mở rộng thêm một chút về chủ đề này. Cảm ơn bạn.
hex

3

Dưới đây là việc tôi thực hiện một lớp trừu tượng đơn lẻ dưới đây. Đây là cách nó xếp chồng lên 4 tiêu chí

             Create object   Removes scene   Global    Keep across
           if not in scene?   duplicates?    access?   Scene loads?

             No (but why         Yes           Yes        Yes
             should it?)

Nó có một vài lợi thế khác so với một số phương pháp khác ở đây:

  • Nó không sử dụng FindObjectsOfType đó là một kẻ giết người hiệu suất
  • Nó linh hoạt ở chỗ nó không cần tạo một trò chơi trống mới trong trò chơi. Bạn chỉ cần thêm nó trong trình chỉnh sửa (hoặc trong trò chơi) vào một trò chơi bạn chọn.
  • Đó là chủ đề an toàn

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
    {
        #region  Variables
        protected static bool Quitting { get; private set; }
    
        private static readonly object Lock = new object();
        private static Dictionary<System.Type, Singleton<T>> _instances;
    
        public static T Instance
        {
            get
            {
                if (Quitting)
                {
                    return null;
                }
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary<System.Type, Singleton<T>>();
    
                    if (_instances.ContainsKey(typeof(T)))
                        return (T)_instances[typeof(T)];
                    else
                        return null;
                }
            }
        }
    
        #endregion
    
        #region  Methods
        private void OnEnable()
        {
            if (!Quitting)
            {
                bool iAmSingleton = false;
    
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary<System.Type, Singleton<T>>();
    
                    if (_instances.ContainsKey(this.GetType()))
                        Destroy(this.gameObject);
                    else
                    {
                        iAmSingleton = true;
    
                        _instances.Add(this.GetType(), this);
    
                        DontDestroyOnLoad(gameObject);
                    }
                }
    
                if(iAmSingleton)
                    OnEnableCallback();
            }
        }
    
        private void OnApplicationQuit()
        {
            Quitting = true;
    
            OnApplicationQuitCallback();
        }
    
        protected abstract void OnApplicationQuitCallback();
    
        protected abstract void OnEnableCallback();
        #endregion
    }

Có thể là một câu hỏi ngớ ngẩn, nhưng tại sao bạn không tận dụng OnApplicationQuitCallbackOnEnableCallbacknhư abstractthay vì chỉ có sản phẩm nào virtualphương pháp? Ít nhất trong trường hợp của tôi, tôi không có bất kỳ logic thoát / kích hoạt nào và việc ghi đè trống cảm thấy bẩn. Nhưng tôi có thể đang thiếu một cái gì đó.
Jakub Arnold

@JakubArnold Tôi đã không nhìn vào điều này trong một thời gian nhưng thoạt nhìn có vẻ như bạn đúng, sẽ tốt hơn như các phương thức ảo
aBertrand

@JakubArnold Thực ra tôi nghĩ rằng tôi nhớ lại suy nghĩ của mình từ lúc đó: Tôi muốn nhận thức được những người đã sử dụng nó như một thành phần mà họ có thể sử dụng OnApplicationQuitCallbackOnEnableCallback: sử dụng nó như một phương thức ảo làm cho nó ít rõ ràng hơn. Có thể hơi kỳ lạ một suy nghĩ nhưng theo tôi nhớ đó là lý trí của tôi.
aBertrand

2

Thực sự có một cách giả chính thức để sử dụng Singleton trong Unity. Dưới đây là lời giải thích, về cơ bản tạo một lớp Singleton và làm cho các tập lệnh của bạn kế thừa từ lớp đó.


Vui lòng tránh các câu trả lời chỉ liên kết, bằng cách đưa vào câu trả lời của bạn ít nhất là một bản tóm tắt thông tin mà bạn hy vọng người đọc sẽ lượm lặt được từ liên kết. Theo cách đó, nếu liên kết trở nên không có sẵn, câu trả lời vẫn hữu ích.
DMGregory

2

Tôi sẽ thực hiện quá trình thực hiện của mình cho các thế hệ tương lai.

void Awake()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy(gameObject.GetComponent(instance.GetType()));
        DontDestroyOnLoad(gameObject);
    }

Đối với tôi, dòng Destroy(gameObject.GetComponent(instance.GetType()));này rất quan trọng bởi vì một khi tôi đã để lại một kịch bản singleton trên một gameObject khác trong một cảnh và toàn bộ đối tượng trò chơi đã bị xóa. Điều này sẽ chỉ phá hủy thành phần nếu nó đã tồn tại.


1

Tôi đã viết một lớp singleton để dễ dàng tạo các đối tượng singleton. Đây là tập lệnh MonoBehaviour, vì vậy bạn có thể sử dụng Coroutines. Nó dựa trên cái này bài viết Unity Wiki này và tôi sẽ thêm tùy chọn để tạo nó từ Prefab sau.

Vì vậy, bạn không cần phải viết mã Singleton. Chỉ cần tải xuống Lớp cơ sở Singleton.cs này , thêm nó vào dự án của bạn và tạo singleton của bạn mở rộng nó:

public class MySingleton : Singleton<MySingleton> {
  protected MySingleton () {} // Protect the constructor!

  public string globalVar;

  void Awake () {
      Debug.Log("Awoke Singleton Instance: " + gameObject.GetInstanceID());
  }
}

Bây giờ lớp MySingleton của bạn là một singleton và bạn có thể gọi nó bằng Instance:

MySingleton.Instance.globalVar = "A";
Debug.Log ("globalVar: " + MySingleton.Instance.globalVar);

Dưới đây là một hướng dẫn đầy đủ: http://www.bivis.com.br/2016/05/04/unity-reUs-singleton-tutorial/

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.