I’m trying to display a struct that has a custom PropertyDrawer in a graph made with UIToolkit.
The property drawer works fine in the editor but I’m stumped making it appear in my graph.
I initially looked into getting the drawer to use CreateInspectorGUI() and add it to my tree, but I’d need a serialized property, and can’t access it from my struct, or my graph node, as i would for a custom inspector.
Is there any other way I could re-use the custom property drawer in my graph?
I manually handle it. Each node stores its data as json. So the struct will be populated when the node is created from the graph, and when the graph is saved the struct data will be stored in the graph’s data.
Unity’s property drawers are (unfortunately) very much tied to serialisation. Far as I know I don’t believe there is a way to grab a specific property drawer and draw it without a serialised property.
Oops sorry I thougt I replied to you earlier.
So for added details :
I’m using the EventRef struct from FMOD, basically setting anted parameters for a sound event to send later to fmod. Their drawer is well made and works great in the inspector.
We have a graph based dialogue/cinematic tool made with UIElements, that is reading and exporting json files that are later used to display dialog and launch action, similar to how bolt/playmaker etc would do.
I’d like our sound graph node to get the same drawer used by EventRef, but as the graph isn’t linked at all with Unity’s serialization, I have no way of getting to a SerilaizedPropery/SerializedObject, at least that’s where I’m blocked for now.
Right, I think I get it now. Okay, like spiney199 says, UITK’s PropertyField is tied to Unity’s serialization. So it can’t exactly be done without a SerializedProperty. Here are some ideas for workarounds:
1. Create a temporary ScriptableObject to hold the struct.
I’ve done this, I’ve seen other people do it, I think Unity’s VisualScripting/Bolt does this sometimes; it’s not pretty but it works. Basically, when you deserialize JSON into a node, instead of holding the EventReference struct in the UITK node, you create a temporary ScriptableObject to store it in a serialized field. Then you can create a SerializedObject from that ScriptableObject and a SerializedProperty to bind a PropertyField to it. You’ve got to remember to call DestroyImmediate on the ScriptableObject when you remove the node, to avoid memory leaks.
2. Make a custom element that replicates the drawer yourself, without the binding system.
It may sound like a daunting task, but if you already know enough about UITK, it could be relatively easy. This struct is just a GUID and a string. Also, it should be faster than FMOD’s IMGUI drawer (embedding a lot of IMGUI drawers in UITK can get slow).
You could look at FMOD’s drawer and replicate what it does. You could even use some of the methods called by that drawer, specially the ones for the buttons that interact with the list events. If you get stuck with it, you could ask me or anyone else in this forum. I’m happy to help.
3. You could move some or all of your graph’s serialization to Unity’s serialization.
Unity’s serialization has gotten pretty capable in recent years. We’ve got serialization of generic types and, more importantly for this case, we’ve got SerializeReference. SerializeReference makes serialization of graphs made of POCOs a lot easier than it used to be. Plus, you’d get all the other benefits of Unity’s serialization like Undo/Redo, Drawers, Decorators, Editors, automatically identifying modified assets for saving them, etc.
I don’t know enough about your project to be sure, but I bet it could be a very solid option. I’ve seen some Github repos about Graphs using SerializeReference and UITK; it seems like plausible idea to me. I myself am doing graphs with UITK and Unity Serialization and I like it (although most of mine are graphs of ScriptableObject subassets because of some unrelated needs). I used to go around Unity features pre-emptively, but with the years, as I learn and Unity improves, I’ve come to prefer trying to take full advantage of the Editor instead. I think it’s made life easier.
Thanks for the detailed answer!
I’ve had both 1 and 2 in mind as fallbacks for this issue yeah. The FMOD drawer is a bit beefy and references serializedproperty in a lot of places so not super easy to reintegrates. And given of the scope of this feature which was just a nice to have, it’s clearly out of my current scope.
That said, option 3 seems really interesting because handling undo/redo natively would be a huge bonus. The SerializeReference sounds intersting indeed, but I’m not sure how to tie my editorwindow to unity’s serialization system. Should I just implement “ISerializationCallbackReceiver” for the window?
I’m not sure I understand how ISerializationCallbackReceiver for the window could be used. You can certainly have serialized fields in a window, then create SerializedProperties for those fields, then use them with UITK, and detect changes to those fields with ISerializationCallbackReceiver or even OnValidate. After all, EditorWindow inherits from ScriptableObject.
In case it helps, here’s what I do in this cases: Instead of storing graphs in JSON, I store them in a ScriptableObject asset, with a C# data structure that’s serializable by Unity. This way I can skip the complexity of converting JSON back and forth. The data inside the ScriptableObject doesn’t have to be used directly at runtime; it can also be transformed into something for runtime usage, but it’s probably simpler to do it this way.
Then I have an EditorWindow that recreates its UI every time a graph SO asset is selected. Every time it recreates its UI, it creates a SerializableObject for the selected graph, so the UI can be bound to it. It behaves like the inspector, but only for my specific ScriptableObjects; it also has a lock toggle in the top right to prevent inspecting a different graph when selection changes.
Thanks, your example is really helping.
Didn’t think about creating temporary SerializedObjects for the graphs themselves, that sounds like a good direction for the rewrite of this tool soon.
We were a bit wary of tying it too much to unity with scriptable objects for the graphs but worst case I can always have the SO export to json themselves.
This has the advantage that you can plug in a derived PD even with subassets that are scriptables with polymorphism.
I’m also making a graph tool and have Nodes as Scriptables with polymorphism. Even though Scriptables maintain proper polymorphic type without SR, polymorphic Drawers don’t work on them by default. So this way I can draw them the same as in a CustomInspector including the polymorphism of the Drawers.
That said, SR is probably the right way to go as Oscar says, as everything else will in turn be wired automatically by Unity. As it so happens I was doing some tests just now and I will convert my code to SR today and remove the ScriptableObjects. Polymorphic SOs as subassets with Undo/Redo are a pain, even with SR on the List.
I see you took the json approach for the underlying type, instead of ScriptableObject. Have you looked through the ShaderGraph package code? If not, then they use the same underlying type with a graph tool, so take a look at that. Although fair warning, it’s not trivial.
Hi, I think I should say that SR do have cons. Here are some of them:
It’s easy to make mistakes and lose their references when refactoring. Every time you rename an asmdef, and every time you rename or move a namespace, you need to remember to add the undocumented attribute: UnityEngine.Scripting.APIUpdating.MovedFrom. You also need to add it when you rename the classes themselves. ScriptableObject classes are much easier to move and rename, especially when there are many of them.
When there are lots of SR objects that reference each other, the editor can struggle performance-wise. I think it makes harder for the Editor to split the check for changes in the graph’s serialized data. Also, some interactions with SR SerializedProperties need reflection. ScriptableObjects can optimize these things by themselves, and they also make it easier for you to edit their data directly, without the binding system, when you need a small performance boost.
ScriptableObjects can use things like Unity’s own Selection System. Implementing your own selection system can get complicated, and it won’t be as powerful as working on top of Unity’s Selection System, because it opens the door to other features. For example, it allows to show an inspector for each of your nodes, which can be very nice when they hold a lot of configurations. I love to be able to press Alt+P to open an inspector for each selected node to compare their data.
SR is a lot more recent and rare. From time to time, it still has bugs, and they can take some time to be fixed.
I have some graphs that use ScriptableObjects as nodes for these reasons, among many other smaller ones. Here are some ideas to make them work:
Don’t make your base node class abstract. This allows for Unity to keep the reference to the nodes with a missing script when there’s an error while refactoring. The references will be restored when you fix the error. Also, you can use the undocumented property of SerializedProperty objectReferenceInstanceIDValue to get the instance id of a node with a missing script, then assign its value to Selection.activeInstanceID to show it in the inspector and manually fix the script reference in extreme error cases.
I recommend creating your own Attribute analogous to CustomEditor to be added to your own special kind of VisualElements that are drawn inside the nodes. You can then create the graph specific UI that corresponds to a node type and pass the Target node to it. It’s not too hard to do thanks to the TypeCache.
Sometimes, you can accidentally change the binding in a field that corresponds to a node if you bind the whole window to the graph. For example, you could rebind a field bound to the name of the node to the name of the graph. I have a BindingStopper element that solves this problem easily by stopping bindings that come from outside it.
Yes, it can be a bit complicated to handle Undo/Redo. I advise to never put the SR attribute in UnityEngine.Object fields. The documentation page for [RegisterCreatedObjectUndo](http://Polymorphic SOs as subassets with Undo/Redo are a pain, even with SR on the List) is a lot more thorough than it used to be now; it makes things easier. In general, things are okay if one follows some rules, like:
Remember to call EditorUtility.SetDirty after making changes.
Set references to an object that’s going to be destroyed to null, and record the change, before destroying it.
Add the object as a subasset before calling RegisterCreatedObjectUndo.
If you have Objects that have just been created with references to other Objects that have also just been created, call RegisterCompleteObjectUndo with all of them after having called RegisterCreatedObjectUndo in all of them. This will allow them to remember each other after Undo->Redo.
You can also call RegisterCompleteObjectUndo before destroying objects to help them remember their references after Undo, in case you can’t set some references to null for some reason.
I agree. Scriptables are far easier to use when dealing with a single Node type (no derivations). Undo can be supported easily even if it’s a bit more involved than SR where you can just do a RecordObject on the database Scriptable.
In my case, initially, I had a Node:Scriptable with a DerivedNode: Node. I managed to figure out the Undo for everything apart from the derived one. I might try the flow you wrote later to see if it fixes things with Scriptables.
I’m not sure where you drew this from though.
Is it from this section?
Because it’s vague on what kind of changes it’s talking about. It could be changes like changing a float on the previously tracked object and then creating an asset/gameobject.
And there’s little consideration for Assets in it. It refers to objects, but objects are GameObjects in a scene as well. When you call new GameObject, if it automatically gets linked to the currently open scene, there’s no distinction about the equivalent in Assets, where you manually need to add them as subassets.
That was my frustration with Nodes as Scriptables as well. Initially I used them for easy inspectors and serialization, as you said, not to mention by default Undo/Redo bc of SerializedObjects. Perhaps I tried all wrong ways but the correct one, but I couldn’t make sense of the documentation or existing forum threads.
For the Selection part, I just have Lists of each thing, Nodes, Connections etc and each one has a PropertyDrawer, so comparing is not difficult (nor do I have a complex UI for Inspector either). Although it’s because I don’t expect many nodes at any given asset. But I agree with the point in general. To be fair though, it’s not too difficult to add multiple node selection on your List and add a Context menu that creates an EditorWindow that displays their default inspector in a horizontal layout. A bit more code, but should be nothing excessive. Obviously even better if you can use the already existing systems, provided there are no other issues.
This seems to work for Adding a new node and Redo works as well, readding the asset properly as well as keeping the reference in the nodes list (I have a Contextual Menu for ScriptableObject nodes which set the node as Selection.activeObject and then call EditorGUIUtility.Ping to check it’s correct). The only problem now is that polymorphic PropertyDrawers don’t work on polymorphic ScriptableObjects, as it would for plain classes. Is this sth that your comment on customEditor like attribute with TypeCache can help with? Can you elaborate on that point.
Also SR on ScriptableObjects, even with polymorphism, might does not work. Even the base class PropertyDrawer has issues with the managed reference value, iirc.
I will probably send feedback on documentation to clarify how to work with Assets.
Hi. Most of the tips I wrote about Undo/Redo I got from personal experience.
I think that part means that Unity flushes all calls to RecordObject when you call Undo.RegisterCreatedObjectUndo. Flushing means that Unity checks the Objects being recorded to see if something changed. Unity normally flushes those calls when some user events happen, like key presses or mouse releases, but it also does so when you call some Undo methods, like the ones for creating and for destroying objects. That means you should do the changes you want to record before creating or destroying objects with Undo.
I found some parts of it a bit tricky. Like making my own selection system that supports Undo/Redo but is decoupled from the graph’s serialized data to avoid messing with version control. It wasn’t too hard but it did take some thinking. Lots of kudos to you if you found it easy.
I’d recommend setting up everything you need in your n Object before calling RegisterCreatedObjectUndo. That way you don’t need to call RecordObject on it, only on db. You also don’t need to set it dirty after creating it. I recommend using AssetDatabase.SaveAssetIfDirty instead of SaveAssets; it avoids saving unrelated things and it’s better for performance.
SerializeReference isn’t supported with types that inherit from UnityEngine.Object.
Yes the idea is that you can make your own equivalent to CustomEditors, but to be shown inside a graph-like interface. PropertyDrawers are nice to handle SerializedProperties, but for ScriptableObjects you want something that handles Serialized Objects. I’ll try to post an example later :).
I understood that. What I meant is that it’s vague which type of change pertain to which one, when it comes to AssetDatabase changes. e.g. AddObjectToAsset is a change which going by the docs, you can’t differentiate if it’s part of changes supported by RecordObject (parentAsset) or by RegisterCreatedObjectUndo.
This comes back to the personal experience answer(I see you recommended some things later on for the posted code). The docs don’t cover enough ground.
Here I meant selection of Items on a ListView. No undo/redo needed (well depending on your use case), but for just showing a Window that offers side by side default inspectors of selection changes of items on the ListView, it shouldn’t be too hard, right? Not talking about the Selection api for UnityEngine.Objects.
e.g. now that I’ve converted the nodes to plain classes in the db Scriptable and there’s no Selection.activeObject option being not an Object, I select an item and then grab its underlying guid (node poc property) and send it off to the graph tool. I search for the Node(Graph Node) using the same guid as its the Graph Node’s viewDataKey. Then I add this to the graph’s selection (AddToSelection) and call FrameSelection. Since the ListView supports multiple item selection, I can select one or more and frame the group.
Out of curiosity what did you mean would mess with version control?!
Thanks for the recommendations! I’ll try them out.
I only considered doing that because of some other thread where some Unity employee responded. Perhaps the use case isn’t directly transferable but the node is an asset in the end. Also in the posted code above, I change the new node’s Name property (which in this case is not the ScriptableObject.name) so it made sense to dirty it too.
I agree, I use SaveAssetIfDirty in my code, but for the post I thought it wasn’t important. It was to post an example :p.
You’re right. I skipped some of the documentation when I took a look about a month ago, because I was trying to get it working for plain classes so I didn’t fully read it.
I guess I could get the type of the editor I want with CustomEditor(through TypeCache) and have it produce a VisualElement just like in my first post which is then added to either the Inspector of the db Scriptable (ListView_ItemBind) or the Graph Node’s container. It would effectively work on serializedObjects by default. I opted for PropertyDrawer because it would be, theoretically, used automatically by the ListView to render their objects, if e.g. MakeItem creates a PropertyField and BindItem binds the array property to the PropertyField. For plain classes you can just do a PropertyField for either the graph part or the editor of the db asset, so it’s simple. I might be sidestepping “best practices” here and there, but it’s doable.
That said, your info so far has been very appreciated. Interested in the solution you have in mind as well.
It’s nice to be able to solve some parts that wasted time for me, just to get the nagging part of not solving it out of my system xD.
UPDATE: Tried your recommendation about RegisterCreatedObjectUndo having all modifications before it, but it loses the Derived Scriptable node reference in the list on Redo. The asset itself gets recreated, but the db List shows an empty reference(it restores the fact that it had 2 items, just not the reference value).
It sounds to me like you’re using GraphView. Am I right? If that’s so, the selection system I was talking about is already handled by GraphView (i.e. AddToSelection). I’m not using it because it’s both experimental and to be deprecated. It doesn’t seem to be receiving support anymore. Plus the API is a bit unfriendly to some use cases. But if you’re using it, you don’t have to worry about developing that particular feature.
Hehe. A naive approach I considered initially was to add a serialized bool to each selectable element that isn’t a UnityEngine.Object to know if it’s selected; that way it supports Undo/Redo. The thing is that that bool is also registered in the asset’s YAML and in version control. I ended up having a separate, editor-only, ScriptableObject to track the selection of these kinds of elements; it’s basically a glorified serialized list of selected element IDs with some performance optimizations. I think GraphView does something similar.
Right, so, my suggestion is more valuable if at least some of your ScriptableObject nodes can contain enough data that it’s worth it to show it in their own Inspector Window. If it isn’t, you could get by just showing an InspectorElement for the ScriptableObject. You can add an empty VisualElement in MakeItem. On BindItem, you clear its contents, add the corresponding InspectorElement, and add a hidden ObjectField that tracks the specific entry to rebind the List item if the Object it references changes (it can happen when the list is reordered or an entry is removed from the middle). Alternatively, I haven’t tested it, but maybe you could create a PropertyDrawer for the base ScriptableObject type that does the thing I suggested for BindItem inside it.
I have an element handles this sort of list in my UITKEditorAid package if you need it; it’s called ListOfInspectors. It doesn’t use ListView, but it’s meant to be used with subassets like this case.
So you could do something similar with your nodes, just show an InspectorElement inside them, if the node doesn’t contain a lot of data. If you have some nodes that do contain a lot of data, you may want to have a dedicated UI to show for nodes in the GraphView, to avoid cluttering the window. The idea would be that, if you want to see all the configuration options of a node, you can select it to see them in the inspector. GraphView does have its own Graph Inspector panel nested in the window, but I hate that it takes so much space in the window, and it’s not as powerful (no properties window with ALT+P, for example).
I uploaded a gist with my solution for custom controls in case it helps. This is where the TypeCache comes in handy. If you want to have node specific UI to be shown in the GraphView, or anywhere else you need a specific UI that corresponds to a specific type, and you can’t use property drawers or custom editors, you can use this. It’s very useful in cases where the same data must look different in different windows.
That’s weird. You mean it works with Node but not with not with DerivedNode? Does it work when you do call RecordObject with the DerivedNode? I mean, it shouldn’t make a difference, but I’m curious. Can you verify that the DerivedNode is being recreated on Redo by putting some Debug.Log in it’s OnEnable? If it doesn’t, there’s probably a problem with the DerivedNode implementation itself, it’s hard to know for sure without looking at it.
Other things that could be different is if you are adding multiple nodes at the same time (e.g. because of copy-paste). In those cases, you should first add each one to the asset and RegisterCreatedObjectUndo, then call RegisterCompleteObjectUndo on each of them if they have references between themselves, and then finally add them to your list while having called RecordObject.
Yeah I’m using GraphView. I know it’s getting deprecated and it’s not fully there yet, but my use cases (two graph tools for two similar cases) are not complex enough to ward me off of it and it has more than enough API to cover what I need. Also the data within each node type are at best 6-10 properties and most of them are value types. One use case will have a single VideoClip as a reference type, but the second will have a node type that’s akin to a media gallery (so Lists of images and Videos). Second use case, might be outsourced to a different tech though (might go to web tech, depending on other factors). That said, I know about GameFoundationTools and how it’s going to contain a more complex solution for creating graph tools, but it’s quite far from being released or even to the point of GraphView being removed etc. This tool is a first version and to be used in near future, so no point futureproofing. Fortunately the way the Scriptables are, you can forgo the tool if you’re fine with creating a fixed graph on the fly by creating the nodes yourself through code, so I’m not too worried about when this tool will become useless. And making it allows me to disengage from its content authoring and move on to other stuff.
UPDATE: I’ve stayed off of direct references between nodes. They all have guids and connections in my model class only have those guids, so SR would be limited just to the polymorphic Node types referenced in the main asset’s List.
Ok I understand what you meant about version control. It is annoying to always see such changes in working tree. I also understand now what you meant about rolling your own selection API. I wonder though how transferable GraphView’s selection api is, or at least as a concept, since it’s open source (well…a large part of it, not sure how far it goes until it’s all native calls, as usual…).
I’ve seen InspectorElement but haven’t used it so far. I’ll try it, thx! As for UITKEditorAid I’ll look into it tomorrow a bit more. I’ve already come across this repo before and starred it anyhow :).
That’s exactly what I referred to in my initial reply. The StackOverflow link does pretty much what most of your link for custom controls does, in terms of figuring out what to draw with. Instead of TypeCache though it just cycles through all Types in the assembly, so TypeCache is probably a good optimization there. But the concept is the same, look for all PropertyDrawers with an attribute “CustomPropertyDrawer” where the target type == .
Then you grab a SerializedProperty of the object (which in ListView is exactly what you get from GetArrayElementAtIndex. So you do drawer.OnPropertyGUI and the resulting VisualElement, you add just like you said in BindItem.
DerivedNode deriving from ScriptableObject just doesn’t draw with the right Drawer no matter what, so manual PropertyDrawer selection was the solution. Although now I’ve reverted to pocos and SR, so the correct PD is rendering depending on type. SR is a bit finicky every now and then due to Undo/Redo, but not enough to be an actual issue. But I’ll reevaluate ScriptableObjects if the Undo testbench is robust.
Yes. The previously posted code does not work, as in fully. Basically it recreates the asset correctly on Redo, but fails to reset the reference to the asset in the db Scriptable’s List.
The setup is super simple as this is a test project for Undo stuff alone.
[Serializable]
public class Node : ScriptableObject
{
public string Name;
public Vector3 Position;
}
[Serializable]
public class DerivedNode : Node
{
public VideoClip Clip;
}
public class DataSO : ScriptableObject
{
public List<Node> nodes;
}
If I try to create a Node and a DerivedNode with the code in my previous post, on Redo, both subassets are recreated, but only Node is readded in the DataSO.nodes list. The list does revert to 2 items, but the second which is the DerivedNode is set to None and does not ping the subasset if double clicked.
If I try the code I posted before that does RecordObjects on both DataSO and Node (or DerivedNode), it fully reverts the Created nodes on Redo, plus the List in DataSO has correct references.
As you can see from the previous code posts, the only referencing is between the DataSO and the subassets.
I could try Adding both of them on the DataSO as subassets and then do a single RecordObject on the DataSO, but the point was to do two fully isolated actions and group them as one Undo entry. Documentation is not very clear on how they get bundled though. Sane person would expect they would retain order of submission (reverted on Undo), just like if they’d been two separate Undo tasks/groups.
@npatch1 Hi I have some questions about the Undo stuff. Can we talk over direct message over to avoid hijacking this thread with non-UITK conversation?