Search Unity

Resolved [PropertyDrawers] Is there a way to customize how fields in a unit are drawn?

Discussion in 'Visual Scripting' started by AlexVillalba, May 17, 2022.

  1. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Last edited: Sep 7, 2023
    suraim1013 likes this.
  2. suraim1013

    suraim1013

    Joined:
    Apr 26, 2021
    Posts:
    8
    Hi.
    I am having a similar problem.

    I want to use "EditorGUILayout.Popup" for Node in VisuialScripting (Bolt).
     
  3. Starpaq2

    Starpaq2

    Joined:
    Mar 14, 2013
    Posts:
    77
    I'm not sure how much "customization" you are looking for.

    Currently you can use the attribute:
    Code (CSharp):
    1. [UnitHeaderInspectable]
    This will display the a type field in the upper portion of the unit/node within the graph. If you need further customization from that, you may want to look into the attribute within the unity visual scripting package to determine how to adjust it further. I would assume that any changes at that level may quickly run into compatibility conflicts in future updates.
     
    Pandazole likes this.
  4. PanthenEye

    PanthenEye

    Joined:
    Oct 14, 2013
    Posts:
    2,077
    I'm fairly sure that considering compatibility at this point is moot since 1.7.x seems to be the last major version of the current iteration of Visual Scripting and there won't be any major changes to the tool this year.

    The preview of 1.8 with high performance interpreter has been pulled from public access. Unity seem to have refocused on the next iteration of Visual Scripting for Unity 2023. And at that point, it'll have a completely different runtime API for the high performance interpreter and completely different API for the GUI as well since they're migrating from current IMGUI to Graph Tools Foundation which is UI Toolkit based. So anything in production with UVS 1.7.x will likely remain 1.7 based until the end of the product's life cycle.
     
    Starpaq2 likes this.
  5. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Thank you for the suggestion but that's not what I'm looking for. I would like, for example, to be able to put a custom button next to a ValueInput.
     
  6. Trindenberg

    Trindenberg

    Joined:
    Dec 3, 2017
    Posts:
    398
    Code (CSharp):
    1. [Widget(typeof(MyUnit))]
    2. public class MyUnitWidget : UnitWidget<MyUnitUnit>
    3. {
    4.     public MyUnitWidget(FlowCanvas canvas, MyUnitUnit unit) : base(canvas, unit)
    5.     {
    6.  
    7.     }
    8.     public override void DrawForeground()
    9.     {
    10.     Do stuff
    11.     }
    12.  
    13. }
    A small example, but there are many things you can override.
     
    Opeth001 and Starpaq2 like this.
  7. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Hi @Trindenberg , thank you, I think you almost nailed it, but I'm trying that and the unit keeps drawing as usual.

    EDIT: Nevermind, it is working :) Now I have to figure out how to draw what I want but this is the right path. Thank you!
     
    Last edited: May 19, 2022
    Opeth001 likes this.
  8. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Some progress:
    Code (CSharp):
    1.  
    2.     [Widget(typeof(ValueInput))]
    3.     public class CustomValueInput : ValueInputWidget
    4.     {
    5.         public CustomValueInput(FlowCanvas canvas, ValueInput port) : base(canvas, port)
    6.         {
    7.  
    8.         }
    9.  
    10.         public override void DrawForeground()
    11.         {
    12.             if(port.type == typeof(LocalizationKey))
    13.             {
    14.                 base.DrawForeground();
    15.                 Rect localizationButton = new Rect(position.position + new Vector2(position.width, 0.0f), new Vector2(position.height, position.height));
    16.              
    17.                 if(GUI.Button(localizationButton, "L"))
    18.                 {
    19.  
    20.                 }
    21.             }
    22.             else
    23.             {
    24.                 base.DrawForeground();
    25.             }
    26.         }
    27.     }
    It's not possible to inherit from ValueInput so I can't use a derived type in the Widget attribute.

    upload_2022-5-19_14-20-19.png

    The Rect of the entire row is available through the position property. There are other positions like iconPosition, handlePosition, inspectorPosition... for each part of the row.
     
    Hikiko66, PanthenEye and Starpaq2 like this.
  9. suraim1013

    suraim1013

    Joined:
    Apr 26, 2021
    Posts:
    8
    These are so great!!!
    Thank you.
     
  10. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    I want to share some findings:

    If you want to add more elements to the header of your custom unit/node, you must override several things in your class derived from Widget:

    Code (CSharp):
    1. [Widget(typeof(SaySequenceNode))]
    2.     public class CustomNodeWidget : UnitWidget<SaySequenceNode>
    3.     {
    4.         protected override bool showHeaderAddon
    5.         {
    6.             get
    7.             {
    8.                 return true;
    9.             }
    10.         }
    11.  
    12.         public CustomNodeWidget(FlowCanvas canvas, SaySequenceNode unit) : base(canvas, unit)
    13.         {
    14.  
    15.         }
    16.  
    17.         protected override void DrawHeaderAddon()
    18.         {
    19.             Rect labelPosition = new Rect(iconPosition.x, headerAddonPosition.y + 5, headerAddonPosition.width, headerAddonPosition.height - 5);
    20.             GUI.Label(labelPosition, "Example text to show how the header addon works", EditorStyles.textArea);
    21.         }
    22.  
    23.         protected override float GetHeaderAddonWidth()
    24.         {
    25.             return position.width - iconPosition.width * 1.5f;
    26.         }
    27.  
    28.         protected override float GetHeaderAddonHeight(float width)
    29.         {
    30.             return 65.0f;
    31.         }
    32.     }
    upload_2022-5-19_19-10-38.png
     
    Starpaq2 likes this.
  11. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    You can also replace whatever you need by overriding DrawForeground:

    Code (CSharp):
    1.  
    2.     [Widget(typeof(SaySequenceNode))]
    3.     public class CustomNodeWidget : UnitWidget<SaySequenceNode>
    4.     {
    5.         public CustomNodeWidget(FlowCanvas canvas, SaySequenceNode unit) : base(canvas, unit)
    6.         {
    7.  
    8.         }
    9.  
    10.         public override void DrawForeground()
    11.         {
    12.             // Extracted from assembly
    13.             this.BeginDim();
    14.             base.DrawForeground();
    15.             this.DrawIcon();
    16.             if (this.showSurtitle)
    17.                 this.DrawSurtitle();
    18.             if (this.showTitle)
    19.                 this.DrawTitle();
    20.             if (this.showSubtitle)
    21.                 this.DrawSubtitle();
    22.             if (this.showIcons)
    23.                 this.DrawIcons();
    24.             //if (this.showSettings) Commented out because it is private in the base class
    25.             //    this.DrawSettings();
    26.             if (this.showHeaderAddon)
    27.                 this.DrawHeaderAddon();
    28.             if (this.showPorts)
    29.                 this.DrawPortsBackground();
    30.             this.EndDim();
    31.         }
    32.     }
     
  12. Trindenberg

    Trindenberg

    Joined:
    Dec 3, 2017
    Posts:
    398
    It is very customizable if you know what you're doing :)
     
    AlexVillalba likes this.
  13. suraim1013

    suraim1013

    Joined:
    Apr 26, 2021
    Posts:
    8
    How can I change the size of the red frame part of the image (Port?) instead of the header part?
     

    Attached Files:

    • node.PNG
      node.PNG
      File size:
      16.7 KB
      Views:
      230
  14. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Do you mean to make the node/unit wider?
     
  15. suraim1013

    suraim1013

    Joined:
    Apr 26, 2021
    Posts:
    8
    Yes.
    I would like to adjust the size of the area by specifying the Width and Height values directly.

    Footer could be area adjusted.
     
  16. suraim1013

    suraim1013

    Joined:
    Apr 26, 2021
    Posts:
    8
    Sorry. It is the header, not the footer.
    I made a mistake.
     
  17. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    The only thing that comes to my mind is using the DrawHeaderAddon method and draw an empty label that has a fixed width and zero height, at the position of the icon. That will make the entire unit wider.
     
  18. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Another code snippet I want to share, just in case you want to create a Bolt unit that listens to a C# event of an object in the scene:
    Code (CSharp):
    1. [Bolt.UnitCategory("Sequence nodes")]
    2.     [Bolt.UnitTitle("Entity spawned")]
    3.     [Bolt.UnitSubtitle("Listens to the EntitySpawned event of a EntitySpawner.")]
    4.     public class EntitySpawnedEventNode : MachineEventUnit<LevelEntity>
    5.     {
    6.         public ValueInput Spawner
    7.         {
    8.             get;
    9.             private set;
    10.         }
    11.  
    12.         public ValueOutput SpawnedObject
    13.         {
    14.             get;
    15.             private set;
    16.         }
    17.  
    18.         protected override string hookName
    19.         {
    20.             get
    21.             {
    22.                 return "Custom";
    23.             }
    24.         }
    25.  
    26.         protected override void Definition()
    27.         {
    28.             base.Definition();
    29.  
    30.             Spawner = ValueInput<EntitySpawner>("Spawner", null);
    31.             SpawnedObject = ValueOutput<LevelEntity>("SpawnedObject", null);
    32.         }
    33.  
    34.         public override void StartListening(GraphStack stack)
    35.         {
    36.             base.StartListening(stack);
    37.             GraphReference reference = stack.ToReference();
    38.             EntitySpawner spawner = Flow.FetchValue<EntitySpawner>(Spawner, reference);
    39.             spawner.EntitySpawned += (spawner, spawnedEntity) =>
    40.                                         { Trigger(reference, spawnedEntity); };
    41.         }
    42.  
    43.         protected override void AssignArguments(Flow flow, LevelEntity args)
    44.         {
    45.             base.AssignArguments(flow, args);
    46.             flow.SetValue(SpawnedObject, args);
    47.         }
    48.  
    49.         protected override bool ShouldTrigger(Flow flow, LevelEntity args)
    50.         {
    51.             return true;
    52.         }
    53.     }
    upload_2022-5-28_22-11-51.png

    In this example, there are spawner objects in the scene that spawn "entities" (other scene objects). The node subscribes to the EntitySpawned event of the spawner and triggers the event in the flow graph when it happens, returning the just spawned entity.
    I found this by digging into the disassembled code of Bolt and I think everything I have overridden is necessary.
    BTW the spawner object is passed to the graph via Variables in the object that contains the FlowMachine component.
     
  19. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    If you want to create a node that suspends the execution of the flow graph until a condition is met, you have to inherit from WaitUnit:

    Code (CSharp):
    1. [Bolt.UnitCategory("Sequence nodes")]
    2.     [Bolt.UnitTitle("Wait for 2D collision")]
    3.     [Bolt.UnitSubtitle("The execution flow is suspended until two colliders overlap.")]
    4.     public class WaitFor2DCollisionSequenceNode : WaitUnit
    5.     {
    6.         public ValueInput ColliderA
    7.         {
    8.             get;
    9.             private set;
    10.         }
    11.  
    12.         public ValueInput ColliderB
    13.         {
    14.             get;
    15.             private set;
    16.         }
    17.  
    18.         protected override void Definition()
    19.         {
    20.             base.Definition();
    21.  
    22.             ColliderA = ValueInput<Collider2D>("Collider A", null);
    23.             ColliderB = ValueInput<Collider2D>("Collider B", null);
    24.         }
    25.  
    26.         protected override IEnumerator Await(Flow flow)
    27.         {
    28.             Collider2D colliderA = flow.GetValue<Collider2D>(ColliderA);
    29.             Collider2D colliderB = flow.GetValue<Collider2D>(ColliderB);
    30.             ContactFilter2D filter = new ContactFilter2D();
    31.             filter.SetLayerMask(1 << colliderB.gameObject.layer);
    32.             filter.useTriggers = true;
    33.             List<Collider2D> overlappedColliders = new List<Collider2D>();
    34.  
    35.             bool hasCollided = false;
    36.  
    37.             while (!hasCollided)
    38.             {
    39.                 overlappedColliders.Clear();
    40.  
    41.                 if(Physics2D.OverlapCollider(colliderA, filter, overlappedColliders) > 0)
    42.                 {
    43.                     for(int i = 0; i < overlappedColliders.Count; ++i)
    44.                     {
    45.                         if(ReferenceEquals(overlappedColliders[i], colliderB))
    46.                         {
    47.                             hasCollided = true;
    48.                             break;
    49.                         }
    50.                     }
    51.                 }
    52.  
    53.                 yield return null;
    54.             }
    55.  
    56.             yield return exit; // When exit is returned, the execution flow continues
    57.         }
    58.     }
    You must override the Await method which acts as a coroutine.

    upload_2022-6-2_17-3-23.png
     
    Last edited: Jun 3, 2022
  20. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346
    Important thing to add to every custom unit:
    Code (CSharp):
    1. Succession(input, output);
    This must appear in the Definition method (if it has a control input and control output), otherwise the nodes that are connected to the output will look grayed in the graph window.
     
  21. AlexVillalba

    AlexVillalba

    Joined:
    Feb 7, 2017
    Posts:
    346