ใน Unity ฉันจะใช้รูปแบบซิงเกิลได้อย่างถูกต้องได้อย่างไร?


35

ฉันได้เห็นวิดีโอและแบบฝึกหัดหลายประการสำหรับการสร้างวัตถุเดี่ยวใน Unity ซึ่งส่วนใหญ่เป็น a GameManagerซึ่งดูเหมือนจะใช้วิธีการต่าง ๆ ในการสร้างอินสแตนซ์และตรวจสอบความถูกต้องของซิงเกิล

มีวิธีที่ถูกต้องหรือค่อนข้างที่ต้องการในการนี

ตัวอย่างหลักสองอย่างที่ฉันพบคือ:

เป็นครั้งแรก

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);
    }
}

ที่สอง

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;
    }
}

ความแตกต่างหลักที่ฉันเห็นระหว่างสองคือ:

วิธีแรกจะพยายามนำทางวัตถุในเกมเพื่อค้นหาตัวอย่างของสิ่งGameManagerที่เกิดขึ้น (หรือควรเกิดขึ้น) เพียงครั้งเดียวดูเหมือนว่ามันจะไม่ได้เพิ่มประสิทธิภาพมากขึ้นเมื่อฉากมีขนาดใหญ่ขึ้นในระหว่างการพัฒนา

นอกจากนี้วิธีแรกจะทำเครื่องหมายว่าวัตถุจะไม่ถูกลบเมื่อแอปพลิเคชันเปลี่ยนฉากซึ่งทำให้แน่ใจได้ว่าวัตถุนั้นยังคงอยู่ระหว่างฉาก วิธีที่สองดูเหมือนจะไม่เป็นไปตามนี้

วิธีที่สองดูเหมือนแปลกในกรณีที่อินสแตนซ์นั้นเป็นโมฆะในผู้ทะเลาะกันมันจะสร้าง GameObject ใหม่และกำหนดองค์ประกอบ GameManger ให้ อย่างไรก็ตามสิ่งนี้ไม่สามารถทำงานได้หากไม่มีส่วนประกอบ GameManager นี้ติดอยู่กับวัตถุในฉากดังนั้นสิ่งนี้ทำให้ฉันสับสน

มีวิธีอื่นใดที่จะแนะนำหรือเป็นลูกผสมของทั้งสองข้างต้น? มีวิดีโอและแบบฝึกหัดมากมายเกี่ยวกับซิงเกิลตัน แต่พวกเขาต่างกันมากมันยากที่จะดึงการเปรียบเทียบระหว่างทั้งสองดังนั้นสรุปได้ว่าข้อใดเป็นวิธีที่ดีที่สุด


GameManager กล่าวว่าควรทำอะไร? ต้องเป็น GameObject หรือไม่
bummzack

1
ไม่ใช่คำถามที่ว่าGameManagerควรทำอย่างไร แต่ควรทำอย่างไรเพื่อให้แน่ใจว่ามีเพียงตัวอย่างเดียวของวัตถุและวิธีที่ดีที่สุดในการบังคับใช้
CaptainRedmuff

บทเรียนนี้อธิบายอย่างมากวิธีการใช้unitygeek.com/unity_c_singletonซิงเกิลฉันหวังว่ามันจะเป็นประโยชน์
Rahul Lalit

คำตอบ:


28

ขึ้นอยู่ แต่ฉันมักจะใช้วิธีที่สาม ปัญหาเกี่ยวกับวิธีการที่คุณใช้คือในกรณีที่วัตถุนั้นถูกรวมไว้เพื่อเริ่มต้นมันจะไม่ลบออกจากต้นไม้และพวกเขายังสามารถสร้างขึ้นได้โดยการโทรหลายครั้งเกินไปซึ่งอาจทำให้สับสนได้

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;
        }
    }
}

ปัญหาเกี่ยวกับการใช้งานของคุณทั้งสองคือการที่พวกเขาไม่ทำลายวัตถุที่สร้างขึ้นในภายหลัง มันสามารถใช้งานได้ แต่มีใครสามารถโยนประแจลิงเข้าไปในงานที่อาจส่งผลให้เกิดข้อผิดพลาดในการแก้ไขข้อผิดพลาดได้ยากมาก ตรวจสอบให้แน่ใจว่าได้เปิดใช้งาน Awake หากมีอินสแตนซ์อยู่แล้วและหากเป็นเช่นนั้นให้ทำลายอินสแตนซ์ใหม่


2
คุณอาจต้องการOnDestroy() { if (this == _instance) { _instance = null; } }ถ้าคุณต้องการมีอินสแตนซ์ที่แตกต่างกันในแต่ละฉาก
Dietrich Epp

แทนที่จะทำลาย () ใน GameObject คุณควรแจ้งข้อผิดพลาด
Doodlemeat

2
อาจ คุณอาจต้องการเข้าสู่ระบบ แต่ฉันไม่คิดว่าคุณควรแจ้งข้อผิดพลาดเว้นแต่ว่าคุณกำลังพยายามทำสิ่งที่เฉพาะเจาะจงมาก มีหลายกรณีที่ฉันสามารถจินตนาการได้ว่าการเพิ่มข้อผิดพลาดจะทำให้เกิดปัญหามากขึ้นและจะแก้ไข
PearsonArtPhoto

คุณอาจต้องการทราบว่า MonoBehaviour สะกดด้วยการสะกดแบบอังกฤษโดย Unity ("MonoBehavior" จะไม่รวบรวม - ฉันทำสิ่งนี้ตลอดเวลา); มิฉะนั้นนี่คือรหัสที่เหมาะสม
Michael Eric Oberlin

ฉันรู้ว่าฉันมาช้า แต่อยากจะชี้ให้เห็นว่าคำตอบเดียวไม่รอดจากการโหลดซ้ำเพราะInstanceคุณสมบัติคงที่ได้รับการเช็ดตัวอย่างหนึ่งที่ไม่สามารถพบได้ในคำตอบด้านล่าง หรือwiki.unity3d.com/index.php/Singleton (ซึ่งอาจจะล้าสมัย แต่ดูเหมือนว่าจะได้ผลจากการทดลองของฉันด้วย)
Jakub Arnold

24

นี่คือบทสรุปโดยย่อ:

                 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

ดังนั้นหากคุณสนใจคือการเข้าถึงทั่วโลกทั้งสามคนจะได้รับสิ่งที่คุณต้องการ การใช้รูปแบบซิงเกิลอาจจะคลุมเครือเล็กน้อยเกี่ยวกับว่าเราต้องการการสร้างอินสแตนซ์ขี้เกียจบังคับใช้เอกลักษณ์หรือการเข้าถึงทั่วโลกดังนั้นให้คิดอย่างรอบคอบเกี่ยวกับสาเหตุที่คุณมาถึงซิงเกิลและเลือกการใช้งานที่ทำให้คุณสมบัติเหล่านั้นถูกต้อง กว่าการใช้มาตรฐานสำหรับทั้งสามเมื่อคุณต้องการเพียงหนึ่งเดียว

(เช่นถ้าเกมของฉันมี GameManager อยู่ตลอดเวลาบางทีฉันไม่สนใจเรื่องการสร้างอินสแตนซ์ขี้เกียจ - บางทีมันเป็นแค่การเข้าถึงทั่วโลกที่มีการรับประกันความเป็นตัวตนและความเป็นเอกลักษณ์ที่ฉันสนใจ - ในกรณีนี้ โดยไม่ต้องคำนึงถึงฉากในการโหลด)

... แต่ไม่ควรใช้วิธีที่ 1 ตามที่เขียนไว้แน่นอน การค้นหาสามารถข้ามได้ง่ายขึ้นด้วยวิธีการ Awake () ของ Method2 / 3 และถ้าเรารักษาผู้จัดการไว้ในฉากต่างๆเราน่าจะฆ่ากันซ้ำ ๆ ในกรณีที่เราโหลดระหว่างสองฉากกับผู้จัดการที่มีอยู่แล้ว


1
หมายเหตุ: มันเป็นไปได้ที่จะรวมทั้งสามวิธีเพื่อสร้างวิธีที่ 4 ที่มีทั้งสี่คุณสมบัติ
Draco18s

3
แรงผลักดันของคำตอบนี้ไม่ใช่ "คุณควรมองหาการใช้งานแบบ Singleton ที่ทำได้ทั้งหมด" แต่ "คุณควรระบุว่าคุณลักษณะใดที่คุณต้องการจากซิงเกิลนี้และเลือกการใช้งานที่ให้คุณสมบัติเหล่านั้น - แม้ว่าการใช้งานนั้นจะเป็นอย่างไร ไม่ใช่ซิงเกิลเลย "
DMGregory

นั่นเป็นจุดที่ดี DMGregory ไม่ใช่ความตั้งใจของฉันจริง ๆ ที่จะแนะนำ "ทุบมันทั้งหมดเข้าด้วยกัน" แต่นั่น "ไม่มีอะไรเกี่ยวกับคุณสมบัติเหล่านี้ที่ป้องกันไม่ให้พวกเขาทำงานร่วมกันในชั้นเรียนเดียว" นั่นคือ "แรงผลักดันของคำตอบนี้ไม่แนะนำให้เลือกอย่างใดอย่างหนึ่ง "
Draco18s

17

การนำไปใช้ที่ดีที่สุดของSingletonรูปแบบทั่วไปสำหรับ Unity ที่ฉันรู้จักคือ (แน่นอน) ของฉันเอง

มันสามารถทำทุกอย่างและทำอย่างประณีตและมีประสิทธิภาพ :

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

     Yes                  Yes                  Yes                     Yes (optional)

ข้อดีอื่น ๆ :

  • มันปลอดภัยต่อเธรด
  • มันหลีกเลี่ยงข้อบกพร่องที่เกี่ยวข้องกับการซื้อ (การสร้าง) เดี่ยวกรณีเมื่อโปรแกรมประยุกต์ที่ถูกเลิกโดยมั่นใจว่า singletons OnApplicationQuit()ไม่สามารถสร้างขึ้นหลังจาก (และจะทำเช่นนั้นด้วยการตั้งค่าสถานะเดียวแทนแต่ละประเภทเดี่ยวมีของตนเอง)
  • ใช้การอัพเดทโมโนของ Unity 2017 (เทียบเท่ากับ C # 6) (แต่สามารถดัดแปลงได้อย่างง่ายดายสำหรับเวอร์ชั่นโบราณ)
  • มันมาพร้อมกับขนมฟรี!

และเนื่องจากการแบ่งปันนั้นห่วงใยนี่คือ:

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!

นี่เป็นของแข็งที่ค่อนข้างสวย มาจากพื้นหลังการเขียนโปรแกรมและพื้นหลังที่ไม่ใช่ความสามัคคีคุณสามารถอธิบายได้หรือไม่ว่าทำไมซิงเกิลตันไม่ได้รับการจัดการใน Constructor แทนที่จะใช้วิธี Awake? คุณคงนึกภาพออกได้ว่าสำหรับนักพัฒนาคนใดก็ตามการเห็น Singleton ที่บังคับใช้อยู่ด้านนอกของ Constructor นั้นเป็นสิ่งที่น่า
ขนลุก

1
@netpoetica Simple ความสามัคคีไม่สนับสนุนตัวสร้าง นั่นเป็นเหตุผลที่คุณไม่เห็นคอนสตรัคเตอร์ที่ใช้ในการสืบทอดคลาสใด ๆMonoBehaviourและฉันเชื่อว่าคลาสใดก็ตามที่ Unity ใช้โดยทั่วไปโดยตรง
XenoRo

ฉันไม่แน่ใจว่าฉันทำตามวิธีการใช้สิ่งนี้ นี่หมายถึงการเป็นผู้ปกครองของชั้นเรียนหรือไม่? หลังจากประกาศSampleSingletonClass : Singleton, กลับมาพร้อมกับSampleSingletonClass.Instance SampleSingletonClass does not contain a definition for Instance
Ben I.

@BenI คุณต้องใช้Singleton<>คลาสทั่วไป นั่นคือเหตุผลที่คนทั่วไปเป็นลูกของSingletonชั้นฐาน
XenoRo

โอ้แน่นอน! มันค่อนข้างชัดเจน ฉันไม่แน่ใจว่าทำไมฉันไม่เห็นสิ่งนั้น = /
เบ็น I.

6

ฉันแค่อยากจะเพิ่มว่ามันอาจจะมีประโยชน์ในการโทรDontDestroyOnLoadถ้าคุณต้องการให้ซิงเกิลของคุณยืนกรานในฉากต่างๆ

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);
    } 
}

มันมีประโยชน์มาก ฉันเพิ่งจะแสดงความคิดเห็นในการตอบกลับของ @ PearsonArtPhoto เพื่อถามคำถามนี้:]
CaptainRedmuff

5

ตัวเลือกอื่นอาจแบ่งคลาสออกเป็นสองส่วน: คลาสแบบสแตติกปกติสำหรับส่วนประกอบ Singleton และ MonoBehaviour ที่ทำหน้าที่เป็นตัวควบคุมสำหรับอินสแตนซ์ซิงเกิล วิธีนี้คุณสามารถควบคุมการก่อสร้างของซิงเกิลได้อย่างเต็มที่และจะคงอยู่ในฉากต่างๆ นอกจากนี้ยังช่วยให้คุณเพิ่มตัวควบคุมลงในวัตถุใด ๆ ที่อาจต้องการข้อมูลของซิงเกิลตันแทนที่จะต้องขุดผ่านฉากเพื่อค้นหาส่วนประกอบเฉพาะ

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();
   }
} 

นี่เป็นสิ่งที่ดีมาก!
CaptainRedmuff

1
ฉันมีปัญหาในการทำความเข้าใจวิธีการนี้คุณสามารถขยายเพิ่มเติมอีกเล็กน้อยในเรื่องนี้ ขอขอบคุณ.
hex

3

นี่คือการใช้คลาสนามธรรมด้านล่างของฉัน นี่คือวิธีการที่ซ้อนกันกับเกณฑ์ 4

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

             No (but why         Yes           Yes        Yes
             should it?)

มันมีข้อดีอีกสองสามข้อเมื่อเทียบกับวิธีอื่น ๆ ที่นี่:

  • ไม่ได้ใช้FindObjectsOfTypeซึ่งเป็นนักฆ่าประสิทธิภาพ
  • มีความยืดหยุ่นในการที่มันไม่จำเป็นต้องสร้างเกมเปล่าใหม่ในระหว่างเกม คุณเพียงแค่เพิ่มเข้าไปในตัวแก้ไข (หรือระหว่างเกม) ลงในเกมของโครงการที่คุณเลือก
  • มันด้ายปลอดภัย

    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
    }

อาจเป็นคำถามที่โง่ แต่ทำไมคุณถึงทำแบบนั้นOnApplicationQuitCallbackและแทนที่จะOnEnableCallbackเป็นวิธีที่abstractว่างเปล่าvirtual? อย่างน้อยในกรณีของฉันฉันไม่มีตรรกะเลิก / เปิดใช้งานและมีการแทนที่ว่างเปล่ารู้สึกสกปรก แต่ฉันอาจจะพลาดอะไรบางอย่าง
Jakub Arnold

@JakubArnold ฉันไม่ได้ดูสิ่งนี้ในขณะที่ แต่ก่อนอื่นดูเหมือนว่าคุณถูกต้องจะดีกว่าเป็นวิธีการเสมือน
aBertrand

@JakubArnold ที่จริงฉันคิดว่าฉันจำความคิดของฉันตั้งแต่นั้นมา: ฉันต้องการที่จะตระหนักถึงผู้ที่ใช้สิ่งนี้เป็นองค์ประกอบที่พวกเขาสามารถใช้OnApplicationQuitCallbackและOnEnableCallback: การมีมันเป็นวิธีการเสมือนจริงทำให้มันชัดเจนน้อยลง อาจคิดแปลก ๆ เล็กน้อย แต่เท่าที่ฉันจำได้ว่านั่นคือเหตุผลของฉัน
aBertrand

2

จริงๆแล้วมีวิธีการหลอกให้ใช้ Singleton ใน Unity นี่คือคำอธิบายโดยทั่วไปแล้วสร้างคลาส Singleton และทำให้สคริปต์ของคุณสืบทอดจากคลาสนั้น


โปรดหลีกเลี่ยงคำตอบสำหรับลิงค์เท่านั้นโดยรวมไว้ในคำตอบของคุณอย่างน้อยข้อมูลสรุปที่คุณหวังว่าผู้อ่านจะได้รับจากลิงค์ ด้วยวิธีนี้หากลิงก์ไม่สามารถใช้งานได้คำตอบก็จะมีประโยชน์
DMGregory

2

ฉันจะพยายามนำไปใช้กับคนรุ่นต่อไปในอนาคต

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

สำหรับฉันบรรทัดนี้Destroy(gameObject.GetComponent(instance.GetType()));มีความสำคัญมากเพราะเมื่อฉันออกจากสคริปต์เดี่ยวในเกมอื่นออกมาในฉากและวัตถุในเกมทั้งหมดจะถูกลบ สิ่งนี้จะทำลายองค์ประกอบหากมีอยู่แล้ว


1

ฉันเขียนคลาสซิงเกิลที่ทำให้ง่ายต่อการสร้างวัตถุเดี่ยว มันเป็นสคริปต์ MonoBehaviour ดังนั้นคุณสามารถใช้ Coroutines มันขึ้นอยู่กับบทความ Unity Wiki นี้และฉันจะเพิ่มตัวเลือกในการสร้างจาก Prefab ในภายหลัง

ดังนั้นคุณไม่จำเป็นต้องเขียนรหัส Singleton เพียงดาวน์โหลดSingleton.cs Base Classนี้เพิ่มไปยังโครงการของคุณและสร้าง singleton ของคุณขยาย:

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

  public string globalVar;

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

ตอนนี้คลาส MySingleton ของคุณคือซิงเกิลและคุณสามารถเรียกมันได้โดยอินสแตนซ์:

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

นี่คือบทแนะนำที่สมบูรณ์: http://www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.