seldomU / relationsinspector

An editor extension for the Unity game engine.
MIT License
41 stars 3 forks source link

Advice/recommendation on slightly adapting the UIEvent backend #3

Open MostHated opened 4 years ago

MostHated commented 4 years ago

I realize that since you open-sourced this, you probably don't want to have to deal with it much anymore, so I can understand if you don't feel like going into it but I did have a question about the UIEvent backend. I wanted to try and modify it a slight bit to track non-UI related events.

I am using the ScriptableObject Event Architecture described in Ryan Hipples 2017 Unity talk in some areas of my project. So the few events that I do have, I am not sure they qualify as "UI Events", but I don't see any EventTriggerType (Ex. PointerEnter, PointerDown, etc) that match the actual type the invocation would be, as I am calling the invoke manually using a Raise() method, which gets sent out to whatever listeners there are for the particular event type. So, in the UIEventUtility, when GetEventRefs is looking for EventTriggerType's it isn't finding anything.

These two sections pretty much make up the entirety of the system, with a few more that only derive from the GameEvent class in order to be considered individual events, then I just pop the GameEventListener on whatever GameObject I want to call something on when an event gets invoked:

Event Base class ```cs // @formatter:off ----------------------------------------------------------- Event Base Object // -- Event Base Object ----------------------------------------------------------------------- public class GameEventBaseObject : ScriptableObject {} // @formatter:on [Serializable] [CreateAssetMenu(fileName = "GameEvent", menuName = "Events/GameEventEvent", order = 0)] public class GameEvent : GameEventBaseObject { // ---------------------------------------------------------------- List of event Listeners // -- List of event Listeners ------------------------------------------------------------- [SerializeField] private List listeners = new List(); // ---------------------------------------------------------------------------- Raise Event // -- Raise Event ------------------------------------------------------------------------- public void Raise() { for (int i = listeners.Count - 1; i >= 0; i--) listeners[i].OnEventRaised(); } // @formatter:off ------------------------------------------------------ Event Registration // -- Event Registration ------------------------------------------------------------------ #region Event Registration public void RegisterListener(GameEventListener listener) // @formatter:on { if (!(listener is null)) listeners.Add(listener); } public void UnregisterListener(GameEventListener listener) { if (!(listener is null)) listeners.Remove(listener); } #endregion } ```
Event Listener ```cs [Serializable] [ExecuteInEditMode] // @formatter:off -------------------------------------------------------------- Event Listener // -- Event Listener -------------------------------------------------------------------------- public class GameEventListener : MonoBehaviour { public GameEvent @event; public UnityEvent Response; private void OnEnable() { if(!(@event is null)) @event.RegisterListener(this);} private void OnDisable() { if(!(@event is null)) @event.UnregisterListener(this); } public void OnEventRaised() { Response.Invoke(); } } // @formatter:on ```

Really, my only question is, do you happen to have any recommendation on how I might adapt the UIEvent base in this situation? Something that might point me in the right direction as to what might be best to try and have RelationshipInspector look for, so I can map out and keep track of the events and where they are going.

As I mentioned, I understand if you are not up to it, or simply don't feel like worrying about RelationshipInspector related things anymore, so no hard feelings if that is the case.

Thanks, -MH

seldomU commented 4 years ago

Just to clarify: do you want to (a) extend the UIEvent backend to include your events or (b) use it as a starting point for a backend that only shows your event type? If you only want to see your event type, it may be easier to start from scratch with something like this:

using UnityEngine;
using System.Collections.Generic;
using RelationsInspector;
using RelationsInspector.Backend;

public class MyEventBackend : MinimalBackend<GameObject, string>
{
    public override IEnumerable<Relation<GameObject, string>> GetRelations( GameObject entity )
    {
        // find event emitters
        var eventEmitters = entity.GetComponentsInChildren<GameEvent>();

        // create relations with all listeners
        foreach( GameEvent emitter in eventEmitters ){
            foreach( GameEventListener listener in emitter.getListeners() ){
                yield return new Relation<GameObject, string>( entity, listener.gameObject, string.Empty );
            }
        }
    }
}

In case you haven't seen them, here are the backend dev docs.

If you want to extend the UIEvent backend, it's going to be painfull because your graph nodes can be multiple types of objects and your methods have to distinguish between all the types. Init would have to also return all the GameEvent components of the target object. GetEventRef would have to check if the component is a GameEvent and if so, create references to all the listeners.

I hope this gets you started. I haven't looked at this stuff in a long time, so I may not get it all right.

MostHated commented 4 years ago

I apologize for any confusion, I am just hoping to show my own event types and have no need for the actual/current UIEvents. I wasn't if since it was based on UnityEvent for the invocation if I would need to utilize the stuff that was in the current UIEvent backend for it to work properly.

I definitely appreciate the reply, link, and example, I will give that a go. At a minimum, as you mentioned, that should at least give me a relatable and directly relevant base to start from. 👍

seldomU commented 4 years ago

No worries. Let me know if I can help with anything else or if the docs are lacking.

MostHated commented 4 years ago

After trying some thing, I was at least able to get a result (not a good one, mind you, lol. It ended up with a circular resolution). Looks like I will probably have to go a bit more "all in" and use the IGraphBackend but at least I have a decent idea of how to move forward.

Thanks again, -MH

MostHated commented 4 years ago

I spent some more time last night trying to get a good setup going, but I ended up getting a bit frustrated after about 2 hours due to the set of errors below that kept coming up. Always together and in the following order.

Relations inspector: Vertex is null. ``` [Error] Relations inspector: Vertex is null. Log.LogItem() Log.Error() RelationsInspector.Graph`2.AddVertex() RelationsInspector.Graph`2.AddVertex() RelationsInspector.GraphBuilder`2.Build() RelationsInspector.Workspace`2.InitGraph() RelationsInspector.Workspace`2..ctor() Activator.CreateInstance() RIInternal.CreateWorkspace() RIInternal.InitWorkspace() RIInternal.SetTargetObjects() RIInternal/<>c__DisplayClass18_0.b__0() RelationsInspectorWindow.Update() EditorApplication.Internal_CallUpdateFunctions() ```
System.Collections.Generic.Dictionary`2[TKey,TValue].FindEntry() ArgumentNullException ``` [Exception] ArgumentNullException: Value cannot be null. System.Collections.Generic.Dictionary`2[TKey,TValue].FindEntry() at <9577ac7a62ef43179789031239ba8798>:0 System.Collections.Generic.Dictionary`2[TKey,TValue].ContainsKey() at <9577ac7a62ef43179789031239ba8798>:0 RelationsInspector.Graph`2[T,P].ContainsVertex() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RelationsInspector.Graph`2[T,P].AddVertex() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RelationsInspector.GraphBuilder`2[T,P].Build() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RelationsInspector.Workspace`2[T,P].InitGraph() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RelationsInspector.Workspace`2[T,P]..ctor() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 MonoCMethod.InternalInvoke() at <9577ac7a62ef43179789031239ba8798>:0 MonoCMethod.InternalInvoke() at <9577ac7a62ef43179789031239ba8798>:0 MonoCMethod.DoInvoke() at <9577ac7a62ef43179789031239ba8798>:0 MonoCMethod.Invoke() at <9577ac7a62ef43179789031239ba8798>:0 RuntimeType.CreateInstanceImpl() at <9577ac7a62ef43179789031239ba8798>:0 Activator.CreateInstance() at <9577ac7a62ef43179789031239ba8798>:0 Activator.CreateInstance() at <9577ac7a62ef43179789031239ba8798>:0 RIInternal.CreateWorkspace() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RIInternal.InitWorkspace() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RIInternal.SetTargetObjects() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RIInternal+<>c__DisplayClass18_0.b__0() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 RelationsInspectorWindow.Update() at <2cfa2d4fe9ad47c79ea67a3c0f4cc640>:0 Debug.LogException() RelationsInspectorWindow.Update() EditorApplication.Internal_CallUpdateFunctions() ```

I would have to switch to one of the other graph types, run the graph build, clear it, then try again. Usually it would happen 2-3 times, then sometimes mine might work again. The Scene Hierarchy graph seems to always work pretty well, but other ones, not so much.

I suppose, though, the fact that it works at all in 2020.2 after that much time is pretty lucky, lol. I mostly just wanted to report the issue, in case it happens to be something of an easy fix.

There were a few other small issues. One with the asset reference graph, due to the reflected method no longer existing in AssetDatabase from what I saw, but since it was just in one of the local files and not in the DLL, I was able to get it squared away (at least I thought, it doesn't error anymore and "Assets" shows up in the graph window, but then that is all that happens. I simply tried to swap out for what looked like it might be a comparable method to the guid => instanceid one that was being used initially).

This was what I did to at least calm it down a bit. I need to go back and look if there is a better replacement method to use that I just didn't notice. This one didn't quite seem correct.

GetAssetRef ```cs public static AssetRef GetAssetRef(string assetPath) { var assetGUID = AssetDatabase.AssetPathToGUID(assetPath); if (assetGUID is null) Debug.LogError("assetGUID is null"); #if UNITY_2020_2 var guidToInstanceID = typeof(AssetDatabase).GetMethod("GetMainAssetOrInProgressProxyInstanceID", BindingFlags.Static | BindingFlags.NonPublic); #else var guidToInstanceID = typeof(AssetDatabase).GetMethod("GetInstanceIDFromGUID", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); #endif if (guidToInstanceID is null) { Debug.LogError("GuidToInstanceID returned null"); return null; } var instanceID = (int) guidToInstanceID.Invoke(null, new object[] {assetGUID}); return new AssetRef {instanceID = instanceID, name = assetPath}; } ```

The other one was a quick fix

Details ```cs // Assets/Editor/GraphBackends/TypeHierarchy/TypeInheritanceBackend.cs // Line: 28 static Assembly[] allAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray(); ```

Thanks, -MH

seldomU commented 4 years ago

That vertex being null is one of the objects yielded by the backend's Init function. Did you overwrite the default Init? Are you initializing your graph by dragging a gameobject into the window? I should add a better error message there. The second error follows from the first.

I'll look into the other backend issues. Thank you for your help!

MostHated commented 4 years ago

Ah, ok, that makes sense. I was in process of trying to use a list as a dropdown as the selection for the objects, to select the ScriptableObject events, which I got setup but then the system kept telling me that I could not map the relationships because I wasn't including the Scene object.

Since the Init method said "Init turns the inspection target objects into root entities of the graph", I was trying to get the dropdown item to be a target, and it sounded like I needed to override Init, as I wanted the Event to be the root object and then you see the branches to show who the listeners are.

in OnGui I was getting a selection, using that to grab an event from the list, then I though when you reset target using it, it might use that as the root object, but then it still kept saying I wasn't using the Scene object, so I am going to have to try and go back through the examples and wiki now that I know that the Init was the cause of the issue.

OnGUI ```cs if (!(eventNameArray is null)) { EditorGUI.BeginChangeCheck(); selectedEvent = EditorGUILayout.Popup(selectedEvent, eventNameArray, EditorStyles.toolbarPopup); if (EditorGUI.EndChangeCheck()) { var selectEvent = eventList.FirstOrDefault(x => x.name == eventNameArray[selectedEvent]); api.ResetTargets(new object[] {selectEvent}); Debug.Log(eventNameArray[selectedEvent]); } } ```
seldomU commented 4 years ago

The code I posted above uses GameObject as graph node type. If you want your target object to be a GameEvent, then the node type has to be either GameEvent or one of its base classes. But because you want GameEventListener nodes as well, the node type has to be a class that both of them derive from. I guess that has to be either UnityEngine.Object or Object. The basic backend would then look like this:

using UnityEngine;
using System.Collections.Generic;
using RelationsInspector;
using RelationsInspector.Backend;

public class MyEventBackend : MinimalBackend < Object, string >
{
    public override IEnumerable<Relation< Object, string >> GetRelations(Object entity)
    {
        var emitter = target as GameEvent;
        if (emitter == null)
            yield break;

        foreach(GameEventListener listener in emitter.getListeners()){
            yield return new Relation<Object, string>(entity, listener.gameObject, string.Empty);
        }
    }
}