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

Hi everybody, I would like to customize how a field of a specific type is drawn inside a unit, in the flow diagram, using something like PropertyDrawers, as I do for Inspector fields. Is this possible?

EDIT 2023: I have published an article about this in my website: Custom Bolt units (Unity's visual scripting nodes) - Jailbroken

1 Like

Hi.
I am having a similar problem.

I want to use “EditorGUILayout.Popup” for Node in VisuialScripting (Bolt).

I’m not sure how much “customization” you are looking for.

Currently you can use the attribute:

[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.

1 Like

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.

1 Like

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.

[Widget(typeof(MyUnit))]
public class MyUnitWidget : UnitWidget<MyUnitUnit>
{
    public MyUnitWidget(FlowCanvas canvas, MyUnitUnit unit) : base(canvas, unit)
    {

    }
    public override void DrawForeground()
    {
    Do stuff
    }

}

A small example, but there are many things you can override.

2 Likes

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 :slight_smile: Now I have to figure out how to draw what I want but this is the right path. Thank you!

1 Like

Some progress:

    [Widget(typeof(ValueInput))]
    public class CustomValueInput : ValueInputWidget
    {
        public CustomValueInput(FlowCanvas canvas, ValueInput port) : base(canvas, port)
        {

        }

        public override void DrawForeground()
        {
            if(port.type == typeof(LocalizationKey))
            {
                base.DrawForeground();
                Rect localizationButton = new Rect(position.position + new Vector2(position.width, 0.0f), new Vector2(position.height, position.height));
             
                if(GUI.Button(localizationButton, "L"))
                {

                }
            }
            else
            {
                base.DrawForeground();
            }
        }
    }

It’s not possible to inherit from ValueInput so I can’t use a derived type in the Widget attribute.

8140193--1056671--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.

3 Likes

These are so great!!!
Thank you.

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:

[Widget(typeof(SaySequenceNode))]
    public class CustomNodeWidget : UnitWidget<SaySequenceNode>
    {
        protected override bool showHeaderAddon
        {
            get
            {
                return true;
            }
        }

        public CustomNodeWidget(FlowCanvas canvas, SaySequenceNode unit) : base(canvas, unit)
        {

        }

        protected override void DrawHeaderAddon()
        {
            Rect labelPosition = new Rect(iconPosition.x, headerAddonPosition.y + 5, headerAddonPosition.width, headerAddonPosition.height - 5);
            GUI.Label(labelPosition, "Example text to show how the header addon works", EditorStyles.textArea);
        }

        protected override float GetHeaderAddonWidth()
        {
            return position.width - iconPosition.width * 1.5f;
        }

        protected override float GetHeaderAddonHeight(float width)
        {
            return 65.0f;
        }
    }

8141000--1056860--upload_2022-5-19_19-10-38.png

1 Like

You can also replace whatever you need by overriding DrawForeground:

    [Widget(typeof(SaySequenceNode))]
    public class CustomNodeWidget : UnitWidget<SaySequenceNode>
    {
        public CustomNodeWidget(FlowCanvas canvas, SaySequenceNode unit) : base(canvas, unit)
        {

        }

        public override void DrawForeground()
        {
            // Extracted from assembly
            this.BeginDim();
            base.DrawForeground();
            this.DrawIcon();
            if (this.showSurtitle)
                this.DrawSurtitle();
            if (this.showTitle)
                this.DrawTitle();
            if (this.showSubtitle)
                this.DrawSubtitle();
            if (this.showIcons)
                this.DrawIcons();
            //if (this.showSettings) Commented out because it is private in the base class
            //    this.DrawSettings();
            if (this.showHeaderAddon)
                this.DrawHeaderAddon();
            if (this.showPorts)
                this.DrawPortsBackground();
            this.EndDim();
        }
    }

It is very customizable if you know what you’re doing :slight_smile:

1 Like

How can I change the size of the red frame part of the image (Port?) instead of the header part?

8142554--1057172--node.PNG

Do you mean to make the node/unit wider?

Yes.
I would like to adjust the size of the area by specifying the Width and Height values directly.

Footer could be area adjusted.

Sorry. It is the header, not the footer.
I made a mistake.

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.

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:

[Bolt.UnitCategory("Sequence nodes")]
    [Bolt.UnitTitle("Entity spawned")]
    [Bolt.UnitSubtitle("Listens to the EntitySpawned event of a EntitySpawner.")]
    public class EntitySpawnedEventNode : MachineEventUnit<LevelEntity>
    {
        public ValueInput Spawner
        {
            get;
            private set;
        }

        public ValueOutput SpawnedObject
        {
            get;
            private set;
        }

        protected override string hookName
        {
            get
            {
                return "Custom";
            }
        }

        protected override void Definition()
        {
            base.Definition();

            Spawner = ValueInput<EntitySpawner>("Spawner", null);
            SpawnedObject = ValueOutput<LevelEntity>("SpawnedObject", null);
        }

        public override void StartListening(GraphStack stack)
        {
            base.StartListening(stack);
            GraphReference reference = stack.ToReference();
            EntitySpawner spawner = Flow.FetchValue<EntitySpawner>(Spawner, reference);
            spawner.EntitySpawned += (spawner, spawnedEntity) =>
                                        { Trigger(reference, spawnedEntity); };
        }

        protected override void AssignArguments(Flow flow, LevelEntity args)
        {
            base.AssignArguments(flow, args);
            flow.SetValue(SpawnedObject, args);
        }

        protected override bool ShouldTrigger(Flow flow, LevelEntity args)
        {
            return true;
        }
    }

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.

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:

[Bolt.UnitCategory("Sequence nodes")]
    [Bolt.UnitTitle("Wait for 2D collision")]
    [Bolt.UnitSubtitle("The execution flow is suspended until two colliders overlap.")]
    public class WaitFor2DCollisionSequenceNode : WaitUnit
    {
        public ValueInput ColliderA
        {
            get;
            private set;
        }

        public ValueInput ColliderB
        {
            get;
            private set;
        }

        protected override void Definition()
        {
            base.Definition();

            ColliderA = ValueInput<Collider2D>("Collider A", null);
            ColliderB = ValueInput<Collider2D>("Collider B", null);
        }

        protected override IEnumerator Await(Flow flow)
        {
            Collider2D colliderA = flow.GetValue<Collider2D>(ColliderA);
            Collider2D colliderB = flow.GetValue<Collider2D>(ColliderB);
            ContactFilter2D filter = new ContactFilter2D();
            filter.SetLayerMask(1 << colliderB.gameObject.layer);
            filter.useTriggers = true;
            List<Collider2D> overlappedColliders = new List<Collider2D>();

            bool hasCollided = false;

            while (!hasCollided)
            {
                overlappedColliders.Clear();

                if(Physics2D.OverlapCollider(colliderA, filter, overlappedColliders) > 0)
                {
                    for(int i = 0; i < overlappedColliders.Count; ++i)
                    {
                        if(ReferenceEquals(overlappedColliders[i], colliderB))
                        {
                            hasCollided = true;
                            break;
                        }
                    }
                }

                yield return null;
            }

            yield return exit; // When exit is returned, the execution flow continues
        }
    }

You must override the Await method which acts as a coroutine.

Important thing to add to every custom unit:

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.