Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice

Traits programming in Unity (ISingleton) (default interfaces, source generators)?

Discussion in 'Experimental Scripting Previews' started by Kamyker, Dec 15, 2022.

  1. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    I was wondering if there are any good ways in Unity to get this right. For ex. let's say I have a
    MonoBehaviour that extends other class (meaning I can't change base class). The only way seems source generators or default interfaces.

    Example:
    MyClass:
    Code (CSharp):
    1. public class MyClass : OtherClassMonoBehaviour, ISingleton<MyClass>
    2. {
    3.     void Awake()
    4.     {
    5.         ((ISingleton<MyClass>)this).AwakeManual();
    6.     }
    7. }
    ISingleton:
    Code (CSharp):
    1. public interface ISingleton<T> : IAwakeManual where T : MonoBehaviour
    2. {
    3.     void IAwakeManual.AwakeManual()
    4.     {
    5.         var thisMono = (T)this;
    6.         if(Singleton<T>.I != null)
    7.         {
    8.             Object.Destroy(thisMono.gameObject);
    9.             return;
    10.         }
    11.         Singleton<T>.I = thisMono;
    12.         Object.DontDestroyOnLoad(thisMono.gameObject);
    13.     }
    14. }
    15.  
    16. public interface IAwakeManual
    17. {
    18.     void AwakeManual();
    19. }
    20.  
    21. public static class Singleton<T> where T : MonoBehaviour
    22. {
    23.     public static T I;
    24. }
    This seems far from optimal as requires the AwakeManual call. Are there any other ways?
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,882
    This looks like overengineering to me, though it could be because of the singleton example since that is two lines of code normally and does and should not require destroying any object to begin with. In fact, if the singleton is non-null this must raise an exception by definition of a singleton. And if this is meant to support „disabled domain reload“ it needs to null the singleton ref in a InitializeOnLoadMethod instead.

    Do you have a real use case or are you just experimenting with this out of curiosity?
     
  3. TJHeuvel-net

    TJHeuvel-net

    Joined:
    Jul 31, 2012
    Posts:
    838
    Its quite inconvenient indeed, we use a `SingletonMonoBehaviour<T>`. This defines a public static T Instance, MyClass would inherit from `SingletonMonoBehaviour<MyClass>`.
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,882
    My god, how many monobehaviour singletons do you have in your projects that you‘ll find the need for a common base class? This has „serious design flaw“ written all over it.

    Providing such a base class will actually entice devs to make even more singletons without thinking of the actual need or alternatives, such as a pure static (utility) class or scriptable objects or using a central, static reference accessor (possibly a singleton but preferably not) that handles all the cases of obtaining and releasing references for subsystems. And dependency injection.

    Classes that I would consider doing a singleton for in most projects:
    • audio manager
    • game or app (data) manager
    That makes about three singletons in most cases and that should be about it.
     
  5. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    As I said base class can't be changed and in addition idea is to have many "traits" possible to add to any class/MonoBehaviour.

    My singleton is a bit longer than that with more options but it's just an example of trait. There's destroy order (newest, oldest) with default value defined in interface that can be overriden to change the behavior.

    It's a bit like some of Unity's interfaces (IPointerEnterHandler) which suddenly make MonoBehaviours receive new callbacks but with default behavior defined.
     
  6. Deleted User

    Deleted User

    Guest

    Unity uses a component system. Maybe instead of trying to add traits to your components, you can make your components your "traits". The following code isn't perfect, but the basic idea is using
    RequireComponent
    to add traits to your script.

    Code (CSharp):
    1. class Singleton : MonoBehaviour
    2. {
    3.  
    4.   private static Dictionary<Type, MonoBehaviour> _singletons;
    5.  
    6.   void Awake()
    7.   {
    8.     if (_singletons == null)
    9.     {
    10.       _singletons = new();
    11.     }
    12.     var hasSingleton = false;
    13.     var components = GetComponents<MonoBehaviour>(); // will not include Transform by default
    14.     foreach (var component in components)
    15.     {
    16.       var type = component.GetType();
    17.       if (type == typeof(Singleton)) continue;
    18.  
    19.       if (_singletons.ContainsKey(type)
    20.       {
    21.         Destroy(component);
    22.       }
    23.       else
    24.       {
    25.         _singletons.Add(type, component);
    26.         hasSingleton = true;
    27.       }
    28.     }
    29.     if (hasSingleton)
    30.     {
    31.       DontDestroyOnLoad(gameObject);
    32.     }
    33.   }
    34.  
    35.   public static T Get<T>() where T : MonoBehaviour
    36.   {
    37.     if (_singletons == null) return null;
    38.    
    39.     return _singletons.TryGetValue(typeof(T), out var component)
    40.       ? (T)component
    41.       : null
    42.       ;
    43.   }
    44. }
    45.  
    46. // say that it's a singleton with `RequireComponent`
    47. [RequireComponent(typeof(Singleton))]
    48. class MyComponent : MonoBehaviour
    49. {
    50. }
     
    Last edited by a moderator: Jan 3, 2023
    DragonCoder likes this.
  7. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    Would have to be Singleton<MyComponent> but generics don't work with RequireComponent or rather whole MonoBehaviour and Unity's serialization.
     
  8. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    Well, that's interesting. After adding extension method I no longer have to cast it to interface to call default method. C# can be weird sometimes.
    Code (CSharp):
    1. public static class ISingletonEx
    2. {
    3.     public static void AwakeManual<T>(this ISingleton<T> obj) where T : MonoBehaviour
    4.         => obj.AwakeManual();
    5. }
    6. public class MyClass : OtherClassMonoBehaviour, ISingleton<MyClass>
    7. {
    8.     void Awake()
    9.     {
    10.         this.AwakeManual();
    11.     }
    12. }
     
  9. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    You're already using the interface here, no reason to cast here.
     
  10. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    You have to cast to use default interface method. Try yourself.
     
  11. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    Only if the type is an implementation of the interface, if the type is already the interface type, that would even generate a warning that the cast is redundant. I tested it with sharplab.
    At this point your object is already automatically casted.
     
  12. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    No idea what you're talking about, it simply doesn't compile with default method. "this" is not interface type.
     
  13. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    I talk about your extension method in your post above:
     
  14. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    What you're saying doesn't work. Send link to your sharplab.
     
  15. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    https://sharplab.io/
    There is no warning but an info, an IDE should still point out that the cast is redundant.
    My point here is that you wrote "C# can be weird sometimes.", but in your example you're already using the interface so it's nothing weird.
     
  16. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    Code (CSharp):
    1. public class MyClass : ITest
    2. {
    3.     private void MyMethod(ITest test)
    4.     {
    5.         test.InterfaceMethod();
    6.     }
    This makes no sense. Remove parameter and change test to this:
     
  17. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    My example here only refers to the case when the type is already the interface type. The method can be in another class as well, or a static method such as an extension method that I originally referred to.

    As I said, I only related everything to an extension method, not the casting within the class that implemented the interface.
     
    Spy-Master likes this.
  18. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    I see, what I meant is that I don't see any reason why C# compiler couldn't treat default methods the same way as extension methods. It's weird that it works for extensions but not for default.
     
  19. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    Like I said, it's not weird since the extension method already uses the interface. basically is just a static method where you pass the object as a parameter. You don't use "this", but the "obj" parameter, that is already of the type of the interface. it would look different if you used a different type such as the MonoBehaviour that implement the interface.
    Code (CSharp):
    1. public static class ISingletonEx
    2. {
    3.     public static void AwakeManual<T>(this ISingleton<T> obj) where T : MonoBehaviour
    4.         => obj.AwakeManual();
    5. }
     
  20. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    I'm not saying that using extension method without casting is weird. I'm saying that not being able to use interface default method without casting is weird.

    Extension method for ISingleton<T> works for MyMono but default interface method for ISingleton<T> doesn't work for MyMono...
     
  21. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    Note: extension methods are syntax/IDE sugar, it becomes a normal static method call "ISingletonEx.AwakeManual(obj)" after compiling.
     
  22. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    is not weird either, but has a reason: a class can have several interfaces with the same interface members, which means that it is possible to have multible default implementations for the same method, and with the casting you say: you want call exactly that default implementation of this specific interface. The version without casting will always call the implementation of the derivate class.
     
    Last edited: Dec 22, 2022
  23. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    These several interfaces can also have extension methods with exactly same name and it simply gives an error. Again, it's weird that's allowed for extensions and not for defaults.

    Another person saying that casting requirement is weird: https://github.com/dotnet/csharplang/discussions/6171
     
    Last edited: Dec 22, 2022
  24. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    Extension methods are not member of the class itself, its only a static method with the interface as parameter, the parameter is already casted. If you used the Monobehavior, you would have to cast.

    The default implementation is part of the interface, not the class that implemented it. Since it has not implemented the interface, you can't access it without casting because your class does not have this method. Default implementation is not inheritance.

    This allows a library to expand an interface by a new method, without libraries/programs that use it have to recompile. That is also the reason behind interface default implementation.

    You can also save the casted instance, that makes no sense in the case of the singleton, but if you have several methods that are often accessed in conjunction with the class
    Code (CSharp):
    1. public class MyClass : MonoBehaviour, ISingleton<MyClass>
    2. {
    3.     private ISingleton<MyClass> _self;
    4.  
    5.     public void Awake()
    6.     {
    7.         _self = this;
    8.         _self.AwakeManual();
    9.     }
     
  25. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    I know but I still wish C# would cast it implicitly to interface and call default method. It makes the most sense to me.
     
  26. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    792
    Sometimes, however, extension methods are enough, they are already similar to Traits: Untested:

    Code (CSharp):
    1.  
    2. public static class Singleton<T> where T : MonoBehaviour
    3. {
    4.     public static T Instance;
    5. }
    6. public static class ISingletonEx
    7. {
    8.  
    9.  
    10.     public static void AwakeManual<T>(this ISingleton<T> obj) where T: MonoBehaviour
    11.     {
    12.         if(obj is MonoBehaviour mono)
    13.         {
    14.             if(Singleton<T>.Instance != null)
    15.             {
    16.                 Object.Destroy(mono.gameObject);
    17.                 return;
    18.             }
    19.             Singleton<T>.Instance = mono;
    20.             Object.DontDestroyOnLoad(mono.gameObject);
    21.         }
    22.     }
    23. }
    With C# 10, interfaces can have static members, so Singleton<T> would not be needed then.
     
  27. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    For singletons example yes but not for anything that has local non-static data. It's not enough for most of traits.

    For ex. making destroyable customizable:
    Code (CSharp):
    1. public interface ISingleton<T> : IAwake where T : MonoBehaviour
    2. {
    3.     bool DontDestroyOnLoad => true;
    4.  
    5.     void IAwake.AwakeManual()
    6.     {
    7.         var thisMono = (T)this;
    8.         if(Singleton<T>.I != null)
    9.         {
    10.             Object.Destroy(thisMono.gameObject);
    11.             return;
    12.         }
    13.         Singleton<T>.I = thisMono;
    14.         if(DontDestroyOnLoad)
    15.             Object.DontDestroyOnLoad(thisMono.gameObject);
    16.     }
    17. }
     
  28. Deleted User

    Deleted User

    Guest

    No I mean in this case, the
    Singleton
    isn't a generic, but tells Unity that its
    GameObject
    and all other components on the
    GameObject
    are singletons.

    That's why the
    Singleton
    class in my example has a map from type to singleton instance.
     
  29. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,087
    Then I'd have to use some weird
    (MyType)Singleton.Get(typeof(MyType))
    casting to get instance, not to mention worse performance than generics.