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. Dismiss Notice

GraphView: State Machine Graph

Discussion in 'UI Toolkit' started by james7132, Aug 28, 2020.

  1. james7132

    james7132

    Joined:
    Mar 6, 2015
    Posts:
    166
    upload_2020-8-28_13-30-55.png

    I'm trying to make my own state machine editor for controlling character animations (This is for a custom DOTS ScriptableObject with project specific animation data, so Mecanim AnimationControllers aren't a very viable solution). So far I've been able to build out a decent set of states and transitions with it and all of the nice benefits of GraphView are great to have for fast development of these kinds of complex tools. However, unlike ShaderGraph/VFXGraph, these graphs, while having directed edges, each edge can represent multiple objects (transitions), and visually make more sense directly connecting to a node instead of ports. Having to hook up the output of a rightmost node to the input on the left is not particularly visually appealing and makes it hard to read the graph:
    upload_2020-8-28_13-38-17.png

    So my questions are as follows:
    • Can I have edges coming from the center of a node without ports?
    • Can I alter the way the rendering of the edges so that they can have Mecanim style "multiple selections"? (where it shows a triple >>> instead of a single > to visually signify there are multiple transitions?
    These aren't blockers, but severely affect readability for our use cases since these graphs will have good number of cycles in them.

    On another note: How do I enable node renaming?
     
    Last edited: Aug 28, 2020
    stevphie123 and LudiKha like this.
  2. jocelyn_legault

    jocelyn_legault

    Unity Technologies

    Joined:
    Jan 16, 2019
    Posts:
    12
    Hey @james7132

    You're somewhat in luck (not full on, pure, 100% unadulterated luck, but luck nonetheless ;)).
    Turns out that in the early infancy of GraphView (almost 4 years ago), I did something very similar to what you're trying to do. So the answer to

    is YES! I even have the code I used back then. However, you will have to do some archaeology to figure out exactly how to adapt this ancient code to the more modern version of GraphView. The attached code doesn't work with a more recent version of Unity. For your information, we used to have presenters (which were more or less models, or rather view models) that don't exist anymore.
    The gist of what you need to do, IIRC, if to override your nodes and edges so they behave as you want. In the attached zip file, you'll find the code you're after under
    Asset/Editor
    (look for
    StateMachineEdge.cs
    and
    StateMachineStateNode.cs
    ). Looking at the code, it seems that I used to connect the node directly, without using ports. It's might be that in the more modern GraphView, this is no longer possible (not sure). In which case I think I'd try to add an invisible port in the middle of the node, but I can't guarantee this is the best course of action either. The only thing that is 100% certain is that you'll have to override your nodes and edges.
    Please also note that it's possible that the edge manipulators (what handles the action of connecting nodes together with an edge) might not play nice with overridden edges. I sincerely don't know. You'll have to experiment.

    For your other question:

    Simply add the Renamable capability to your node.

    Code (CSharp):
    1. public MyRenamableNode()
    2. {
    3.     capabilities |= Capabilities.Renamable;
    4. }
     

    Attached Files:

  3. james7132

    james7132

    Joined:
    Mar 6, 2015
    Posts:
    166
    Oh thanks for such a detailed response, on the weekend too! I was banging my head against the CsReference code and even going as far as to try to reflect into UnityEditor.Graphs to try to get something that looks remotely workable. This is great!

    I noticed the mesh generation code for EdgeControl is not public and there doesn't seem to be an easy way to alter it. Might be a bit harder to add the >/>>> geometry that I want, or I might just use different colors to achieve the same goal.

    EDIT: It seems like the code you provided directly uses GL to draw the arrows over the edge, which works too, though I'm not entirely sure it will work with the zoom/pan that the GraphView currently supports.

    It seems like they're required to bind to a port. How do I make them invisible? Sorry I'm still a bit new to all of the controls in UIElements.

    Where is this functionality shown? There's no additional context when right clicking and no text field is shown.

    Out of curiosity, is there any Unity-side goals or use cases for something like this? This seems like a good fit for DOTS Animation authoring if mapping Mecanim Animation Controllers ends up being too incompatible.

    I'll update the thread with any success or failures I have with this endeavor.
     
  4. jocelyn_legault

    jocelyn_legault

    Unity Technologies

    Joined:
    Jan 16, 2019
    Posts:
    12
    I'm not sure either.

    To hide:
    Code (CSharp):
    1. myElement.style.visibility = Visibility.Hidden;
    To show back after being hidden:
    Code (CSharp):
    1. myElement.style.visibility = StyleKeyword.Null;
    Hum. Looks like it's not implemented on the nodes. :oops: You'd need to do some extra work to replace the node's label with a renamable label. There is currently no such thing in UI Toolkit, but we have our own in Visual Scripting. I should try and get it to UI Toolkit at some point. I the mean time, I guess I can share our implementation (I've taken out the parts that were only pertinent to VS... Not 100% sure it works right out of the box, but it should guide you getting there) :

    Code (CSharp):
    1. using System;
    2. using UnityEditor.Experimental.GraphView;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace Packages.VisualScripting.Editor.Elements
    6. {
    7.     public class RenamableLabel : GraphElement
    8.     {
    9.         Label m_Label;
    10.  
    11.         TextField m_TextField;
    12.  
    13.         public VisualElement TitleEditor => m_TextField ?? (m_TextField = new TextField { name = "titleEditor", isDelayed = true });
    14.         public VisualElement TitleElement => m_Label;
    15.  
    16.         public bool IsFramable() => false;
    17.  
    18.         public bool EditTitleCancelled { get; set; } = false;
    19.  
    20.         GraphView m_GraphView;
    21.         Action<string> m_RenameAction;
    22.  
    23.         GraphView GraphView => m_GraphView ?? (m_GraphView = GetFirstAncestorOfType<GraphView>());
    24.  
    25.         public RenamableLabel(string text, Action<string> renameAction)
    26.         {
    27.             name = "renamableLabel";
    28.  
    29.             m_RenameAction = renameAction;
    30.  
    31.             ClearClassList();
    32.  
    33.             m_Label = new Label() { text = text, name = "label" };
    34.             Add(m_Label);
    35.  
    36.             m_TextField = new TextField { name = "textField", isDelayed = true };
    37.             m_TextField.style.display = DisplayStyle.None;
    38.             Add(m_TextField);
    39.  
    40.             var textInput = m_TextField.Q(TextField.textInputUssName);
    41.             textInput.RegisterCallback<FocusOutEvent>(_ => OnEditTextFinished());
    42.  
    43.             RegisterCallback<MouseDownEvent>(OnMouseDownEvent);
    44.  
    45.             capabilities |= Capabilities.Renamable;
    46.  
    47.             this.AddManipulator(new ContextualMenuManipulator(OnContextualMenuEvent));
    48.         }
    49.  
    50.         void OnContextualMenuEvent(ContextualMenuPopulateEvent evt)
    51.         {
    52.             GraphView.BuildContextualMenu(evt);
    53.         }
    54.  
    55.         void OnEditTextFinished()
    56.         {
    57.             m_TextField.style.display = DisplayStyle.None;
    58.  
    59.             if (m_Label.text != m_TextField.text)
    60.             {
    61.                 m_RenameAction?.Invoke(m_TextField.text);
    62.             }
    63.         }
    64.  
    65.         void OpenTextEditor()
    66.         {
    67.             m_TextField.SetValueWithoutNotify(m_Label.text);
    68.             m_TextField.style.display = DisplayStyle.Flex;
    69.             m_TextField.Q(TextField.textInputUssName).Focus();
    70.             m_TextField.SelectAll();
    71.         }
    72.  
    73.         void OnMouseDownEvent(MouseDownEvent e)
    74.         {
    75.             if ((e.clickCount == 2) && e.button == (int)MouseButton.LeftMouse && IsRenamable())
    76.             {
    77.                 OpenTextEditor();
    78.                 e.PreventDefault();
    79.                 e.StopImmediatePropagation();
    80.             }
    81.         }
    82.     }
    83. }
    84.  
    I definitely would like to get a way for 3rd parties to easily create state machines using GraphView or something similar not too far in the future, but I can't give you a time estimate for that landing in a public release of Unity or one of its package at this time.
     
  5. james7132

    james7132

    Joined:
    Mar 6, 2015
    Posts:
    166
    Ah so it's just like the MDN documented visibility CSS property. That's good to know.

    Alright. Thanks for the snippet. I had previously just replaced it with a text-field and just used that instead. This is notably cleaner.

    Perfectly understandable. This is a very widely applicable use case, though it's pretty clear from some of the changes we've highlighted in this thread it seems like there are quite a few blockers. Hopefully when the official solution is out, it'll look better than whatever I'm hacking together. Haha.
     
  6. jocelyn_legault

    jocelyn_legault

    Unity Technologies

    Joined:
    Jan 16, 2019
    Posts:
    12
    We sure have our work cut out for us! ;)
     
  7. calpolican

    calpolican

    Joined:
    Feb 2, 2015
    Posts:
    400
    @james7132 where you able to achieve this?
    I'm trying to do a very simple undirected graph right now. I want to use this for internal handling of the map of my game.
    The issue I'm having is that ports have a direction field in them (i.e. Direction.Input), the default behaviour rejects output-output connection or input-input connections. I'd like to override this function to get just one type of port but I have no clue about what's the function that I want to override, and I'm a bit lost in the docs. Any ideas?
     
    Last edited: Jan 18, 2021
  8. james7132

    james7132

    Joined:
    Mar 6, 2015
    Posts:
    166
    This is for a hobby project of mine, and unfortunately I haven't had any time since to try my hand at this. It'd be great if this had official support, but it seems neither I nor UT have had resources to further dedicate to this.

    I ended up forking the GraphView code directly off of the CsReference GitHub repo and removing the directionality of ports, pinning them to the center of the node. This works OK-ish, though it requires a lot of additional handler code for dragging and dropping transitions, which I have had zero time to get around to doing.
     
  9. vanderFeest

    vanderFeest

    Joined:
    Apr 12, 2017
    Posts:
    8
    For anyone coming across this when looking for the rename bit, I solved it using parts of @jocelyn_legault 's code and it works very nicely like this (snippet includes renaming with F2):

    Code (CSharp):
    1.  
    2. /** part of a class for this purpose called RenamableNode **/
    3.  
    4.     Label titleLabel;
    5.     TextField renameField;
    6.  
    7.     public string ID {
    8.         get => title;
    9.         set {
    10.             if (string.IsNullOrEmpty(value) || value.Equals("(Unnamed)"))
    11.             {
    12.                 title = "(Unnamed)";
    13.                 titleLabel.style.unityFontStyleAndWeight = FontStyle.Italic;
    14.             }
    15.             else
    16.             {
    17.                 title = value;
    18.                 titleLabel.style.unityFontStyleAndWeight = FontStyle.Normal;
    19.             }
    20.         }
    21.     }
    22.  
    23.     public RenamableNode()
    24.     {
    25.         capabilities |= Capabilities.Renamable;
    26.  
    27.         titleContainer.RegisterCallback<MouseDownEvent>(MouseRename, TrickleDown.TrickleDown);
    28.  
    29.         focusable = true;
    30.         pickingMode = PickingMode.Position;
    31.         RegisterCallback<KeyDownEvent>(KeyboardShortcuts, TrickleDown.TrickleDown);
    32.  
    33.         titleLabel = titleContainer.Q<Label>();
    34.  
    35.         renameField = new TextField { name = "textField", isDelayed = true };
    36.         renameField.style.display = DisplayStyle.None;
    37.         renameField.ElementAt(0).style.fontSize = titleLabel.style.fontSize;
    38.         renameField.ElementAt(0).style.height = 18f; // still need to figure out how to make these values depend on the label's font size
    39.         renameField.style.paddingTop = 8.5f;
    40.         renameField.style.paddingLeft = 4f;
    41.         renameField.style.paddingRight = 4f;
    42.         renameField.style.paddingBottom = 7.5f;
    43.         titleContainer.Insert(1, renameField);
    44.  
    45.         VisualElement textInput = renameField.Q(TextField.textInputUssName);
    46.         textInput.RegisterCallback<FocusOutEvent>(EndRename);
    47.     }
    48.  
    49.     void MouseRename(MouseDownEvent evt)
    50.     {
    51.         if(evt.clickCount == 2 && evt.button == (int)MouseButton.LeftMouse && IsRenamable())
    52.             StartRename();
    53.     }
    54.  
    55.     private void KeyboardShortcuts(KeyDownEvent evt)
    56.     {
    57.         if(evt.keyCode == KeyCode.F2 && IsRenamable())
    58.             StartRename();
    59.     }
    60.  
    61.     void StartRename()
    62.     {
    63.         titleLabel.style.display = DisplayStyle.None;
    64.         renameField.SetValueWithoutNotify(ID);
    65.         renameField.style.display = DisplayStyle.Flex;
    66.         renameField.Q(TextField.textInputUssName).Focus();
    67.         renameField.SelectAll();
    68.     }
    69.  
    70.     void EndRename(FocusOutEvent evt)
    71.     {
    72.         titleLabel.style.display = DisplayStyle.Flex;
    73.         renameField.style.display = DisplayStyle.None;
    74.  
    75.         if (ID != renameField.text)
    76.         {
    77.             ID = renameField.text;
    78.         }
    79.     }
    80.  
     
    MiguelCoK likes this.
  10. stevphie123

    stevphie123

    Joined:
    Mar 24, 2021
    Posts:
    74
    Can we have this functionality somewhat officially supported in the graphview? Having just two ports, one on each side, is a bit of a mess with so many nodes in the graphview, which the situation I'm facing right now -____-

    Having all ports can dynamically switching sides when moved is more robust than what the graphview has now