Life and Style Media - Tutorials

Version 2.0 Alpha 5



In this tutorial, we will take you through how to create an Inspector for your type, and a Widget for adding a drop down to select your type. We'll do this by designing a way of defining ports on a unit, using a LudiqScriptableObject. That is, a Scriptable Object that has LudiqGUI based drawers. We'll create a button that uses a Generic Menu drop down of definitions to select from.


Scriptable Object Definition


In order to define ports on our eventual unit, we will need to have assets we can create. They should contain a series of inputs and outputs for us to grab and define them with. We want to be able to use the type dropdown that Ludiq provides. In order to do this we will create a LudiqScriptableObject.

Lets create the asset for port definitions. Name the type CustomPortDefinition, with a C# file of the same. You should have 2 Dictionaries, with a string key for the label, and value of type, 'System.Type' for defining our ports type. Add the Ludiq serialization attribute 'Serialize' above both inputs and outputs. Dictionarys don't serialize out of the gate with Unity. Serialize is a better serialization then what is native to Unity. All your data will retain.

Do not forget to add the CreateMenuItem. We will need this to create new definitions in the editor.

using
Ludiq;
using
System;
using
System.Collections.Generic;

[
CreateAssetMenu
(fileName =
"New Port Definition"
, menuName =
"Bolt/Extensions/Custom Port Definition"
)]
public class
CustomPortDefinition
:
LudiqScriptableObject

{
[
Serialize
]
public
Dictionary
<
string
,
Type
> inputs =
new
Dictionary
<
string
,
Type
>();
[
Serialize
]
public
Dictionary
<
string
,
Type
> outputs =
new
Dictionary
<
string
,
Type
>();
}


Lets save and you should now see this:



Hmmmm.... well it says we need an inspector. Lets create one of our own!

Create a C# script for this called CustomPortDefinitionInspector. All inspectors require a specific constructor. It takes an Accessor as its base.

What is an Accessor? An Accessor is an equivalent to a SerializedProperty. Its used to refer to a field or property through easy reflection, and deals with all the pains automatically. You'll have a massive list of things you can use to draw with and get reflected information with. Really makes the process very painless.

We'll keep this one as simple as possible.

using
Ludiq;

public class
CustomPortDefinitionInspector
:
Inspector

{
public
CustomPortDefinitionInspector
(
Accessor
accessor) : base(accessor) { }

}


You'll notice it doesn't change. We need a way to register this with the actual type. We have new assembly attributes for doing this with many different inherited types. We will be using RegisterEditor. NOT INSPECTOR! I know that is tricky, but Inspectors are used for everything. Editor will be used when it is for an editor. Even though the type inherits from Inspector.

using
Ludiq;

[
assembly
:
RegisterEditor
(
typeof
(
CustomPortDefinition
), typeof(
CustomPortDefinitionInspector
))]

public class
CustomPortDefinitionInspector
:
Inspector

{
public
CustomPortDefinitionInspector
(
Accessor
accessor) : base(accessor)
}


At this point we are still going to get errors. We have two abstract methods to override. OnGUI and GetHeight. We'll give GetHeight a large value and move on. Say 500f. After we have figured out the stuff we are drawing, we'll come back and do it right.


using
Ludiq;

[
assembly
:
RegisterEditor
(
typeof
(
CustomPortDefinition
), typeof(
CustomPortDefinitionInspector
))]

public class
CustomPortDefinitionInspector
:
Inspector

{
public
CustomPortDefinitionInspector
(
Accessor
accessor) : base(accessor)

protected override float
GetHeight(
float
width,
GUIContent
label)
{
return
500f
;
}

protected override void
OnGUI(
Rect
position,
GUIContent
label)
{

}
}




Drawing Fields


Lets get ourselves some fields. Something we can see on here.

There is some really handy stuff under the type LudiqGUI. Right now, we are going to be concerned with maybe one or two methods. A very important one is:

LudiqGUI
.Inspector(
Accessor
accessor,
Rect
position, [
GUIContent
label =
null
])


This method will draw a field or property by taking an accessor, and reading its data to determine and draw the proper Inspector. If there is no inspector created for a type, then you get the yellow warning box.

GUI is heavy on math, but simple math! Thankfully there isn't too much to worry about in this tutorial.

To figure out the rectangle area we are drawing, you can start by assigning a new local variable. Assign it the incoming position. You will end up with the proper inspectors width, x and y. All you will need to be concerned with is how to setup the correct height.

We can do this by getting the inspector from the inputs and outputs accessor. It will automatically find the height for that type, no matter its state.

The outputs y should be the current y position, plus the inputs height. Now, it will always be below the inputs full rectangle. No matter how many items we add.

protected override void
OnGUI(
Rect
position,
GUIContent
label)
{
var
inputPosition = position;
inputPosition.height = accessor[
"inputs"
].Inspector<
DictionaryInspector
>().GetCachedHeight(inputPosition.width, label, accessor.Inspector());

LudiqGUI
.Inspector(accessor[
"inputs"
], inputPosition);

var
outputPosition = position;
outputPosition.height = accessor[
"outputs"
].Inspector<
DictionaryInspector
>().GetCachedHeight(outputPosition.width, label, accessor.Inspector());

outputPosition.y += inputPosition.height;

LudiqGUI
.Inspector(accessor[
"outputs"
], outputPosition);
}





Last bit is going to be the height. Just do what you did before in OnGUI, where you needed the fields inspector height. Add both inputs and outputs height together. That is your return value for GetHeight.

protected override float
GetHeight(
float
width,
GUIContent
label)
{
var
inputs = accessor[
"outputs"
].Inspector<
DictionaryInspector
>().GetCachedHeight(width, label, accessor.Inspector());

var
outputs = accessor[
"outputs"
].Inspector<
DictionaryInspector
>().GetCachedHeight(width, label, accessor.Inspector());

return
inputs + outputs;
}


Custom Unit


We are going to create a unit. This unit is going to teach you a few things:

  • What a Widget is.
  • How to color the Unit.
  • How to add a button to the header.
  • Finding all assets and displaying in a drop down menu.
  • Utilizing variadic ports, defined by the Dictionaries.


  • Unit Definition

    Only special thing we need here, is a field to reference the selected definition.

    using
    Ludiq;
    using
    Ludiq.Bolt;

    public class
    CustomPortsUnit
    :
    Unit

    {
    public
    CustomPortDefinition
    definition;

    protected override void
    Definition()
    {

    }
    }


    Widget

    Now for Widgets!

    Widgets are the visual design of a type. You can change colors, add headers, and even modify the behaviour in a graph. Perhaps you want to prevent the Unit from being movable. Maybe you shouldn't be able to delete it. Those various things are possible.

    Since we are making a Widget for a unit, there is already
    UnitWidget
    <
    TUnit
    >
    . Lets inherit that. This will be the bare minimum to have it work.

    using
    Ludiq;
    using
    Ludiq.Bolt;

    [
    assembly
    :
    RegisterWidget
    (
    typeof
    (
    CustomPortsUnit
    ),
    typeof
    (
    CustomPortsUnitWidget
    ))]

    public class
    CustomPortsUnitWidget
    :
    UnitWidget
    <
    CustomPortUnit
    >
    {
    public
    CustomPortUnitWidget
    (
    FlowCanvas
    canvas,
    CustomPortsUnit
    unit) :
    base
    (canvas, unit)
    {

    }
    }


    Making the Header


    Now that we have everything setup. We are going to start seeing changes to the unit. We should generate the unit now and put it in the graph. Everytime you save you will be able to see the changes.



    We are going to start getting a little more complex as we go.

    We want to draw our own header. So override DrawHeaderAddon. We can leave the contents blank. We'll come back to it.

    In order to draw the contents, we need to make room for it. We'll draw extra space by overriding GetHeaderAddonHeight and GetHeaderAddonWidth. Assign 18f for the height, and 115f for the width.

    using
    Ludiq;
    using
    Ludiq.Bolt;

    [
    assembly
    :
    RegisterWidget
    (
    typeof
    (
    CustomPortsUnit
    ),
    typeof
    (
    CustomPortsUnitWidget
    ))]

    public class
    CustomPortsUnitWidget
    :
    UnitWidget
    <
    CustomPortUnit
    >
    {
    public
    CustomPortUnitWidget
    (
    FlowCanvas
    canvas,
    CustomPortsUnit
    unit) :
    base
    (canvas, unit)
    {
    }

    protected override bool
    showHeaderAddon =>
    true
    ;

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

    protected override float
    GetHeaderAddonWidth()
    {
    return
    115f
    ;
    }

    protected override void
    DrawHeaderAddon()
    {

    }
    }


    Save it and you will now have some extra spacing.




    We want to have a button in that space. We also want the label to reflect the selected CustomPortDefinition. If there is none selected, it should say "(None)". All of this should go in DrawHeaderAddon. Then add a button. Widgets have access to a property called 'headerAddonPosition'. This property takes into account, your overrides for the height and width, as well as the padding and position offset, which is handled for you. Super simple!

    protected override void
    DrawHeaderAddon()
    {
    GUIContent
    content = new
    GUIContent
    ();

    if
    (unit.definition ==
    null
    )
    {
    content =
    new
    GUIContent
    (
    "(None)"
    );
    }
    else

    {
    content =
    new
    GUIContent
    (unit.definition.name);
    }


    if
    (
    GUI
    .Button(headerAddonPosition, content))
    {

    }
    }


    Save it! You now have a button at the top of your unit with the proper naming. Success!




    Find and Show Definitions


    We have just a few more things to do. We need to:

  • Get all of our assets of type CustomPortDefinition.

  • Create a new drop down menu.

  • Set the units current definition base on menu selection.

  • Ping the graph to update ports when a new input or output is removed or added.


  • I decided to not include on type change. It would have complicated this tutorial much more. If you got this far, I'm sure you can figure that out!

    Searching for the definitions is very easy. Unity provides a single method to do this. We will only do this when you click the button. It is not wise to cycle every frame, with such a heavy method.

    var
    definitions =
    Resources
    .FindObjectsOfTypeAll<
    CustomPortDefinition
    >();

    We'll then create a new menu, and open it.

    var
    menu =
    new
    GenericMenu
    ();
    menu.ShowAsContext();

    Nothing will happen at this point. So lets cycle through the menu items in a foreach loop, and add them to the menu. This should go after the menu is created, but before it is open. With AddItem, it comes with a type called GenericMenu.MenuFunction. It is essentially an action for when you select the menu item. Perfect. Lets assign the current definition in the loop for each item we create. That is all for that! Here is the whole thing.

    protected override void
    DrawHeaderAddon()
    {
    GUIContent
    content = new
    GUIContent
    ();

    if
    (unit.definition ==
    null
    )
    {
    content =
    new
    GUIContent
    (
    "(None)"
    );
    }
    else

    {
    content =
    new
    GUIContent
    (unit.definition.name);
    }


    if
    (
    GUI
    .Button(headerAddonPosition, content))
    {
    var
    definitions =
    Resources
    .FindObjectsOfTypeAll<
    CustomPortDefinition
    >();
    var
    menu =
    new
    GenericMenu
    ();

    foreach
    (
    CustomPortDefinition
    definition
    in
    definitions)
    {
    menu.AddItem(
    new
    GUIContent
    (definition.name),
    false
    ,
    new
    GenericMenu.MenuFunction
    (() =>
    {
    unit.definition = definition;
    }));
    }

    menu.ShowAsContext();
    }
    }


    You now have a drop down to select from any definition! Excellent.




    Variadic Ports


    Now we should go back to editing CustomPortsUnit.cs. We are going to fill out definition, and start drawing our ValueInputs and ValueOutputs. With this, we will show you how to draw ports that can be based on something else. In this case, we will iterate over the inputs and outputs in the definition, get the name and the type.

    We need to first check if definition and inputs and outputs dictionaries are null. To prevent errors being thrown from our unit. Inside the body of the if statements, we should convert the keys of each the inputs and outputs to a list. Ludiq has a converter, that can turn the Enumerator into a List. It is called ToListPooled();

    public class
    CustomPortsUnit
    :
    Unit

    {
    public
    CustomPortDefinition
    definition;

    protected override void
    Definition()
    {
    if
    (definition?.inputs !=
    null
    && definition?.outputs !=
    null
    )
    {
    var
    inputKeys = definition.inputs.Keys.ToListPooled();
    var
    outputKeys = definition.outputs.Keys.ToListPooled();
    }
    }
    }


    Next, we should iterate with a for loop, if the inputs and outputs have items. So they should be larger then a count of 0.

    var
    inputKeys = definition.inputs.Keys.ToListPooled();
    var
    outputKeys = definition.outputs.Keys.ToListPooled();

    if
    (definition.inputs.Count >
    0
    )
    {

    }

    if
    (definition.outputs.Count >
    0
    )
    {

    }


    Just for safe keeping, we should iterate over the actual keys, so no argument gets thrown. Then create a new Value Inputs, and output.

    var
    inputKeys = definition.inputs.Keys.ToListPooled();
    var
    outputKeys = definition.outputs.Keys.ToListPooled();

    if
    (definition.inputs.Count >
    0
    )
    {
    for
    (
    int
    i =
    0
    ; i < inputKeys.Count; i++)
    {
    var
    input = ValueInput(definition.inputs[inputKeys[i]], inputKeys[i]);
    }
    }

    if
    (definition.outputs.Count >
    0
    )
    {
    for
    (
    int
    i =
    0
    ; i < outputKeys.Count; i++)
    {
    var
    output = ValueOutput(definition.outputs[outputKeys[i]], outputKeys[i]);
    }
    }


    You may now notice, it is kinda sporadic in its behaviour. They don't update immediately or on first selection.


    Redefine Ports


    We need to create a way of detecting if we added or removed any items from the definition. That way we can know if we should define the ports again.

    Lets create a new method that returns bool. We will call it ShouldChange. Within the method, we should do a comparison statement to see if the last frames input and output count is less then, or greater then the current frames input and output count. We also will need to find out if the definition is different.

    private int
    lastInputCount;
    private int
    lastOutputCount;
    private
    CustomPortDefinition
    lastDefinition;

    public bool
    ShouldChange()
    {
    if
    (definition == null) {
    lastDefinition = null;
    return false
    ;
    }
    else

    {
    if
    (lastDefinition != definition)
    {
    lastDefinition = definition;
    return true
    ;
    }

    if
    (lastInputCount > definition.inputs.Count || lastInputCount < definition.inputs.Count)
    {
    lastInputCount = definition.inputs.Count;
    return true
    ;
    }

    if
    (lastOutputCount > definition.outputs.Count || lastOutputCount < definition.outputs.Count)
    {
    lastOutputCount = definition.outputs.Count;
    return true
    ;
    }

    return false
    ;
    }


    Good. There is a delegate all graphs have. It is 'onChanged'. You can bind any parameterless void method to it. We specifically, want to redefine the unit when the graph is declared, having been changed.

    When we add the unit, we need to bind Define to it. When we remove the unit, we need to unbind it. We have two methods for this to override and we can effectively call graph.Changed() and our graph will define its ports safely.

    public override void
    AfterAdd()
    {
    base
    .AfterAdd();

    graph.onChanged += Define;
    }

    public override void
    BeforeRemove()
    {
    graph.onChanged -= Define;

    base
    .AfterRemove();
    }


    Almost done! Go back to the Widget. At the bottom of the DrawHeaderAdd method, the very last line should utilize both the method and delegate. We will ask unit.ShouldChange(). If it should. Do graph.Changed(). Which we are bound to now. It'll update! You are effectively done with this tutorial. Save and witness the beauty.



    Widget Colors


    The unit works. On the other hand, you may want to spruce it up a bit. How about adding some color?

    Override baseColor in the Widget. It takes a NodeColorMix type. You can set how much of each to use.

    protected override
    NodeColorMix
    baseColor =>
    new
    NodeColorMix
    ()
    {
    blue = 1f,
    red = 0.3f,
    gray = 1f
    };