Search Unity

Avoiding Singleton: How to design a class containing data that is used across the system.

Discussion in 'Scripting' started by zee_ola05, Sep 21, 2014.

  1. zee_ola05

    zee_ola05

    Joined:
    Feb 2, 2014
    Posts:
    166
    Example Scenario:
    I am making a tower defense game, and I have a class that contains tower data (tower type, damage, cost, range, etc.) Now, multiple classes (Upgrade System, Tower Factory, Store, etc) need access to these data. All of these are read-only.

    Questions:
    1. Is making a singleton class acceptable in this scenario? Why? Why not?
    2. What could be a better design?
     
  2. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    Code (CSharp):
    1. //InventoryManager.cs
    2. //C#
    3. using UnityEngine;
    4. using System;    //allows us to use IndexOutOfRangeException
    5. using System.Collections;
    6.  
    7. [System.Serializable]
    8. public class TowerData
    9. {
    10.     public string name;
    11.     public int type;
    12.     public int damage;
    13.     public int cost;
    14.     public float range;
    15.     //etc...
    16. }
    17.  
    18. public class InventoryManager : MonoBehaviour
    19. {
    20.     #region this stuff handles static instance management
    21.         private static InventoryManager s_instance;
    22.         public static InventoryManager instance { get { return s_instance; } }
    23.      
    24.         public void Awake()
    25.         {
    26.             if (!staticallyInstanced) { return; }    /*    if this script isn't set to statically
    27.                                                         instance itself, early out                */
    28.          
    29.             if (s_instance != null)
    30.             {//only one instance can co exist
    31.                 if (s_instance != this)
    32.                 {
    33.                     //Destroy(this);    //    remove duplicate instances
    34.                     //                    //    (destroys this script component from this gameObject only
    35.                     GameObject.Destroy(gameObject);        //    remove duplicate instances
    36.                                                         //    (destroys the gameObject we're attached to)
    37.                 }
    38.                 return;        //don't initialize twice
    39.             }
    40.             s_instance = this;        //at this point InventoryManager.instance refers to this script
    41.         }
    42.     #endregion
    43.  
    44.     public bool staticallyInstanced;    //this bool controls whether we statically instance this script
    45.  
    46.     public TowerData[] towerData;    //this is assigned inside the inspector
    47.  
    48.     //allows you to access the towerData indirectly via indexing
    49.     public TowerData this[int index]
    50.     {
    51.         get
    52.         {
    53.             if (index < 0 || index >= towerData.Length) { throw new IndexOutOfRangeException(); }
    54.             return towerData[index];
    55.         }
    56.     }
    57.     //returns the length of the towerData array
    58.     public int Length { get { return towerData.Length; } }
    59.  
    60.     //these methods can be called e.g. InventoryManager.GetTowerData(3);    //returns the 3rd inventory item defined
    61.     public static TowerData[] GetTowerData()
    62.     {//returns the towerData array inside the static instance
    63.         return s_instance.towerData;
    64.     }
    65.     public static TowerData GetTowerData(int index)
    66.     {//returns the towerData in the towerData array inside the static instance
    67.         return s_instance[index];
    68.     }
    69.     public static int CountTowerData()
    70.     {//returns the towerData array length inside the static instance
    71.         return s_instance.Length;
    72.     }
    73. }
    Here's an example in C#

    The way this basically works is you have one InventoryManager script attached to an object somewhere in your game (this object can exist across scenes if you also add a DontDestroyOnLoad line)
    if you check the staticallyInstanced flag in the inspector to on, the script will statically store itself and perform duplicate instance checking when the script awakes.
    You define your tower data inside the inspector on the InventoryManager, and you can then access that data without knowing anything about the script, simply by calling it's static methods, e.g.

    int itemCount = InventoryManager.CountTowerData();
    foreach(TowerData data in InventoryManager.GetTowerData()) {
    Debug.Log(data.name);
    }

    Hope this helps! :)
     
    Last edited: Sep 21, 2014
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    @TWiX - your design is a singleton. So I guess you're saying that yes, a singleton design approach is acceptable.

    As for my take.

    Yes, I believe the Singleton design pattern is an acceptable approach for this. Especially if everything is read-only. I personally am not afraid of the singleton pattern and have used it several times in my professional career as a software developer of nearly a decade.

    You just need to accept the upfront issue. That being that this object is now only single instanced, and it is tightly couple to all your code. But if you know that for the life of your application that this single instance would only ever have 1 instance, then what's the big deal?
     
  4. BmxGrilled

    BmxGrilled

    Joined:
    Jan 27, 2014
    Posts:
    239
    The design I gave can either be singleton OR modular... if you uncheck the statically instanced flag, you must access the script directly to modify it's contents. It's all up to designers decision. :)
     
  5. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    Sup zee_ola05! I'm going to add my thoughts to this thread.

    Why are singletons discouraged?
    Singletons are generally discouraged for the same reason that global variables are generally discouraged. It is easy to couple classes together and break encapsulation.

    Having a public static instance always available means that you could have every other script make calls to the singleton. If you removed this one singleton class from your project, you could possibly need to make changes to every other script in the project. This is the opposite of encapsulation taken to the extreme.

    What is the alternative to using singletons?
    The alternative would be that you have some Manager class that has responsibility for first initializing the data class. The Manager would then pass the data class, using a constructor or initialization function, to the Upgrade System, Tower Factory, Store, etc.

    Why would a singleton be better?
    Sometimes using singletons actually DEcouple your code. If you singleton is widely used by many many separated systems, then by having a singleton you might avoid the Manager class described above. You can keep the systems of your game separated/encapsulated from each other without having to "stitch them together" using a Manager class.

    Using the alternative Manager technique described above, if you have a lot of things that need to get data from your data class, you might end up making a bunch of managers to take the data class, then pass it down to other systems with their own managers who might pass the data class further down to sub-systems with sub-managers... This feels *more* coupled to me, since now these separate systems need both the data class AND the managers to work properly.

    Final thoughts
    Truthfully, if I was making banking software or maybe writing all the code myself, I might ban the use of singletons. The idea of rogue code running from anywhere without explicit initialization makes me feel like I don't have as much control.

    But we make games which have constant design changes, and we work with other coders, so being able to drop in a widely used singleton and just access it is too easy and quick to avoid!
     
    Last edited: Sep 21, 2014
    jParmentier likes this.