Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Generic Monobehavior Workarounds?

Discussion in 'Scripting' started by ItsBoats, May 3, 2020.

  1. ItsBoats

    ItsBoats

    Joined:
    May 6, 2013
    Posts:
    7
    I'm working on a project where players will be able to make units through visual scripting. I don't like the current way that I'm doing it because I have to create a new script for each input/output type. I want to use generics so I can just write the type whenever I need it, but MonoBehaviours can't be generic. I'm looking for some suggestions on workarounds or alternate solutions.

    Screenshot.png

    Here's what I tried with generics:
    Code (CSharp):
    1. public class NodePort<T> : MonoBehaviour {
    2.  
    3.     private List<NodePort<T>> portConnections = new List<NodePort<T>>();
    4.     private T portValue;
    5.  
    6.     public T GetValue() {
    7.         return portValue;
    8.     }
    9.  
    10.     public void SetValue(T _value) {
    11.         portValue = _value;
    12.     }
    13.  
    14.     private void Update() {
    15.         //Drag from this NodePort to other NodePorts to create connections
    16.     }
    17.  
    18.     public void DrawPortConnections() {
    19.         for (int i = 0; i < portConnections.Count; i++) {
    20.             //Draw line from NodePort.transform.position to portConnections[i].transform.position;
    21.         }
    22.     }
    23. }
    24.  
    25.  
    26. public class TestNode : MonoBehaviour
    27. {
    28.     public NodePort<float> speed;
    29.  
    30.     private void Awake() {
    31.         speed.SetValue(3f);
    32.         Debug.Log(speed.GetValue());
    33.     }
    34. }

    Here is another idea I had that could work, but I don't think I want to be casting back and forth between Type object and other types:
    Code (CSharp):
    1. public class NodePort : MonoBehaviour { //Not generic class this time
    2.  
    3.     private object value;
    4.  
    5.     public void SetValue<T>(T _value) {
    6.         value = _value;
    7.     }
    8.  
    9.     public T GetValue<T>() {
    10.         return (T) value;
    11.     }
    12.  
    13. }

    Here is some code I found from bolt that is creating custom nodes that seems to be doing something similar to what I want, but Im confused that ValueInput<string>("a", string.empty) is being used as a method when its the class name (and Im assuming not a constructor because of the lack of the 'new' keyword):

    Code (CSharp):
    1. public class BasicUnit : Unit {
    2.  
    3.     [DoNotSerialize]
    4.     public ControlInput enter;
    5.  
    6.     [DoNotSerialize]
    7.     public ValueInput A;
    8.  
    9.     protected override void Definition() {
    10.         enter = ControlInput("enter", (flow) => { return exit; });
    11.         A = ValueInput<string>("a", string.Empty);
    12.     }
    13. }
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,971
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,385
    You're just not going to get serializing a generic, even Unity's own UnityEvent<T> needs a concrete type when used:
    https://docs.unity3d.com/ScriptReference/Events.UnityEvent_1.html

    Note how they create a concrete type in place.

    Which you can do:
    Code (csharp):
    1.  
    2. public class NodePort<T> : MonoBehaviour
    3. {
    4.     private List<NodePort<T>> portConnections = new List<NodePort<T>>();
    5.     private T portValue;
    6.  
    7.     public T GetValue()
    8.     {
    9.         return portValue;
    10.     }
    11.  
    12.     public void SetValue(T _value)
    13.     {
    14.         portValue = _value;
    15.     }
    16.  
    17.     private void Update()
    18.     {
    19.         //Drag from this NodePort to other NodePorts to create connections
    20.     }
    21.  
    22.     public void DrawPortConnections()
    23.     {
    24.         for (int i = 0; i < portConnections.Count; i++)
    25.         {
    26.             //Draw line from NodePort.transform.position to portConnections[i].transform.position;
    27.         }
    28.     }
    29. }
    30.  
    Then when you need one:
    Code (csharp):
    1. public class FloatPort : NodePort<float> { }
    And:
    Code (csharp):
    1.  
    2. public class TestNode : MonoBehaviour
    3. {
    4.  
    5.     public FloatPort speed;
    6.  
    7. }
    8.  
    Though I have a question... is there a reason 'NodePort' is a MonoBehaviour? Couldn't you just make it a plain old class? I wonder because it's weird that you'd add both a 'TestNode' and its 'Ports' as components. Wouldn't you rather just attach a 'TestNode' and the ports exist as members of it? What happens if say I add a NodePort<float> and use the same float port on 2 distinct TestNodes?
     
  4. ItsBoats

    ItsBoats

    Joined:
    May 6, 2013
    Posts:
    7
    I think this still gets me into the situation where my class type will need to be generic too to implement it. My use case is a little different than the link you posted, so I might be missing something
    Code (CSharp):
    1. public interface INodePort<T> {
    2.     T GetVal();
    3. }
    4.  
    5. public class NodePort<T> : MonoBehaviour, INodePort<T> {
    6.     private T value;
    7.  
    8.     T GetVal() {
    9.         return value;
    10.     }
    11. }
    This is exactly what I'm doing right now that works. I was just trying to move away from it so I dont have to make 20+ scripts for each type.

    The reason why they are MonoBehaviours is mainly to set up the connections between them by clicking and dragging, and initializing some things in awake. The NodePorts are created from the Node class so two NodePorts wouldnt be on different Nodes.

    Not having NodePorts as MonoBehaviours could work, but it makes formatting my nodes, and detecting when a player is dragging on a NodePort really easy. I'm not sure how I would do it otherwise.
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,385
    So like I said, the generics thing just isn't going to get a work around. It really sucks.

    Even if you use like the new 'SerializeReference' attribute that has a concrete interface type, when you assign it to a generic object, the serialization engine complains it can't serialize the object.

    It's one of those really annoying things that I wish Unity would resolve... but there most be some major design choice made early on that would require potential support breaking issues if they were to change it to include generics otherwise they would have resolved it when they released UnityEvent<T> and the sort. They support List<T> only because they have a very specific way of dealing with it as it's a known container (it's a list). Maybe the type branching that can occur with generic types and generic fields just creates an infinite loop or something. I don't know... speculating.

    ...

    With all that said. I personally would go with plain old classes, this way you don't need to create a bunch unique script files for each one. And you could just quickly implement a bunch in place in a single cs file. And any you don't create could be created in place as needed in some node you want:
    Code (csharp):
    1.  
    2. [System.Serializable]
    3. public class NodePort<T>
    4. {
    5.     private List<NodePort<T>> portConnections = new List<NodePort<T>>();
    6.     [SerializeField]
    7.     private T portValue;
    8.  
    9.     public T GetValue()
    10.     {
    11.         return portValue;
    12.     }
    13.  
    14.     public void SetValue(T _value)
    15.     {
    16.         portValue = _value;
    17.     }
    18.  
    19. }
    20.  
    21. [System.Serializable]
    22. public class FloatPort : NodePort<float> { }
    23.  
    24. [System.Serializable]
    25. public class BoolPort : NodePort<bool> { }
    26.  
    27. [System.Serializable]
    28. public class IntPort : NodePort<int> { }
    29.  
    30. [System.Serializable]
    31. public class StringPort : NodePort<string> { }
    As for the drawing... I'd encapsulate the drawing task away from the port containers. Start with some base node type that hooks into the drag/draw pipe for you and all nodes inherit from:

    Code (csharp):
    1.  
    2. public abstract class BaseNode : MonoBehaviour
    3. {
    4.  
    5.     private INodePort[] _ports;
    6.  
    7.     protected abstract INodePort[] GetNodePorts();
    8.  
    9.     protected virtual void Awake()
    10.     {
    11.         _ports = this.GetNodePorts();
    12.     }
    13.  
    14.     protected virtual void Update()
    15.     {
    16.         foreach(var port in _ports)
    17.         {
    18.             //handle port
    19.         }
    20.     }
    21.  
    22. }
    23.  
    24. public interface INodePort
    25. {
    26.  
    27. }
    28.  
    29. public static class INodePortExtensions
    30. {
    31.     public static T GetValue<T>(this INodePort port)
    32.     {
    33.         var tport = port as NodePort<T>;
    34.         return tport != null ? tport.GetValue() : default(T);
    35.     }
    36.  
    37.     public static void SetValue<T>(this INodePort port, T value)
    38.     {
    39.         var tport = port as NodePort<T>;
    40.         if (tport != null) tport.SetValue(value);
    41.     }
    42. }
    43.  
    44.  
    45.  
    46. [System.Serializable]
    47. public class NodePort<T> : INodePort
    48. {
    49. //snip
    50.  
    And an implementation:
    Code (csharp):
    1.  
    2. public class TestNode : BaseNode
    3. {
    4.  
    5.     public FloatPort speed = new FloatPort();
    6.  
    7.     protected override INodePort[] GetNodePorts()
    8.     {
    9.         return new INodePort[] { speed };
    10.     }
    11.  
    12. }
    13.  
    Now lets say you're using the UnityUI system with Canvas and what not. In that case create a factory that generates the appropriate port visualizations for each kind of port. Then in the Awake/Start of BaseNode you run this factory on the nodes that came back from 'GetNodePorts', place them in the scene however (a child of BaseNode??? like I said, I don't know your UI situation)

    This way the UI is separated from your data. No reason to be clouding up NodePort<T> with that task... those appear to just be data containers and here they act purely as just that. The UI is handled via the UI system.

    This way if you wanted to swap out the UI situation... all your TestNode1, TestNode2, INodePorts, etc all stay exactly the same. All you just hook into a different UI factory in the BaseNode.
     
    ItsBoats likes this.
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,385
    To continue on the factory idea... so something like this:
    Code (csharp):
    1.  
    2. public interface INodeVisualizationFactory
    3. {
    4.  
    5.     INodeVisual CreatePortVisualizer(BaseNode owner, INodePort[] ports);
    6.  
    7. }
    8.  
    9. public interface INodeVisual
    10. {
    11.  
    12. }
    13.  
    14. public class SimpleNodeVisualizationFactory
    15. {
    16.  
    17.     public static readonly SimpleNodeVisualizationFactory Default = new SimpleNodeVisualizationFactory();
    18.  
    19.     public INodeVisual CreatePortVisualizer(BaseNode owner, INodePort[] ports)
    20.     {
    21.         var container = (new GameObject("NodeVisualizer")).AddComponent<NodeVisualizer>();
    22.  
    23.         foreach(var port in ports)
    24.         {
    25.             container.Add(new PortVisualizer(port));
    26.         }
    27.  
    28.         container.transform.parent = owner.transform;
    29.         container.transform.localPosition = Vector3.zero;
    30.         return container;
    31.     }
    32.  
    33. }
    34.  
    And your BaseNode would become:
    Code (csharp):
    1.  
    2. public abstract class BaseNode : MonoBehaviour
    3. {
    4.  
    5.     private INodePort[] _ports;
    6.     private INodeVisual _visual;
    7.  
    8.     protected abstract INodePort[] GetNodePorts();
    9.  
    10.     protected virtual void Awake()
    11.     {
    12.         _ports = this.GetNodePorts();
    13.         _visual = SimpleNodeVisualizationFactory.Default.CreatePortVisualizer(this, _ports);
    14.     }
    15.  
    16. }
    17.  
    Where PortVisualizer and NodeVisualizer are where you implement all your UI stuff.
     
    ItsBoats likes this.
  7. ItsBoats

    ItsBoats

    Joined:
    May 6, 2013
    Posts:
    7
    Thank you so much for the comprehensive answer! I think this is going to get me where I need to be. I'm going to look through everything more thoroughly tonight when I have the chance and try implementing it tomorrow.
     
  8. ItsBoats

    ItsBoats

    Joined:
    May 6, 2013
    Posts:
    7
    I have just about everything redone now taking into mind some of your design patterns. I did manage to leave NodePort generic without having to inherit from it with concrete classes. Was the reasoning behind this just for the sake of serializing NodePort subclasses for use in the inspector? Is there another reason I would need NodePort serialized?
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,385
    It was only so it would be serializable in inspector. If you don't need that, than you don't have to.

    If you needed to serialize for a save file or something, you can use Newtonsoft Json.Net and it supports generic classes. So you'd be fine.