jacobdufault / fullinspector

Full Inspector supercharges Unity's inspector
MIT License
111 stars 27 forks source link

Hooking up monobehavious #64

Closed HeliosDoubleSix closed 10 years ago

HeliosDoubleSix commented 10 years ago

I love interfaces, however they do not work with monobehaviours. Let me explain with an example.

I want to use Interfaces with none monobehaviours ( which works great already ) AND additionally with monobehaviours/components. So I can pick from a bunch of none mono behaviour ISomeInterface's and they are made new() as they currently do OR a button to plop this onto a new gameObject with an empty class that has a var that can hold an ISomeInterface and reference that ( or pick from existing ones found in the scene ). If the ISomeInterface is a monobehaviour then plop it on an empty gameobject and reference that class. FYI IUnified solves this in reverse not allowing you to use none monobehaviours thus forcing you to use components.

If that made no sense then I'll go into more detail:

Say I have an INumberGenerator interface, and one implementation just makes a random number every second. Your script allows me to have a variable on a monobehaviour that requires this interface of INumberGenerator so I can pick from a popdown menu of all the scripts I could use, however if I have multiple monoBehaviours OR multiple variables that can also take a INumberGenerator, they all create a new() one and keep it to themselves. I cannot share the one instance with several locations, so I cannot use the same number from this random generator with 2 things as they both will create a new() random number generator.

What I would like is to be able to reference other instances that exist anywhere ( preferably thru a nice visual picker like IUnified) , though it would be acceptable if not most sane ( due to serialization ) to only allow this if the reference existed directly on a monobehaviour/component. So say I create a separate object in my scene to hold it, this would then have a simple monobehaviour script that can hold a var that requires INumberGenerator, and I can reference that instance in multiple places.

Ultimately I want this all to be automated, so I can have a class specified by interface from your popdown, press a button and suddenly it is broken off into a separate gameObject in the scene maybe as a child object of the caller, with this simple component/script attached that holds a single var of the same interface, and that is serialized to be the same contents as before 'breaking' it off and hooked up as a reference.. using magic

IUnified has you create variables that are simple empty class to use as a container, that class then specifies an interface via generics, thus allowing unity to serialize it all. Not as nice as being able to use interfaces directly on vars, but it works and allows multiple objects to be linked together component style but limited by interface still. The problem here is you HAVE to have each script be a separate component on an object.

I want both styles :-)

I don't expect you to write this or anything, but is it possible, and help point me in the right direction so I can do it :-D

Or maybe you have a better idea!

Thank you so much for your time, and thank you for Full Inspector!

jacobdufault commented 10 years ago

First, let me make sure I understand you right: You want interfaces that have shared instances across multiple behaviors.

This is tricky but also interesting. I think this is doable (based on some preliminary investigations), but due to the way that serialization works within Full Inspector the shared interfaces are going to have to derive from ScriptableObject (no need for the overhead of MonoBehaviors). This is more or less unavoidable without completely rewriting the serialization adapters, and likely impossible (aside: it would have been easier to do this in FI1, but in FI1 you couldn't just add a reference to a UnityEngine.Object type -- you had to use Ref<ComponentType> instead -- I'm really happy that's gone :)).

The upcoming release handles the ScriptableObject serialization fine, but some extra editor work will need to be done to allow for copying and pasting ScriptableObjects that live in the scene so that you can state which instance should be shared. Either it'll handle it by default or you'll have to add an annotation like [InspectorSharedInstance]. Not sure yet -- haven't implemented anything.

If you want to try it on you're own, I would recommend writing an attribute based property editor. The ObjectPropertyEditor has changed quite a bit to allow for the inline object dropdowns. If you want the current master so you can work off of that, just send me an email and I'll get a build to you.


Note on serialization

Depending on how you reference everything, Json.NET may not handle this situation well. However, for these cases you can just use Full Serializer, which handles inheritance correctly.

Example of a Json.NET error case:

public interface IBaseShared {
    int GetInt();
}

public abstract class BaseShared : BaseScriptableObject, IBaseShared {
    public abstract int GetInt();
}

public class SharedContainer : BaseBehavior {
    public IBaseShared SharedInstance;
}

to fix for Json.NET you need to remove IBaseShared, ie, change the code to

public abstract class BaseShared : BaseScriptableObject {
    public abstract int GetInt();
}

public class SharedContainer : BaseBehavior {
    public BaseShared SharedInstance;
}
HeliosDoubleSix commented 10 years ago

So you think it would be impossible to use monobehaviours, even indirectly? Some reasons this would be useful: You get an object in the editor you can visually see, name, duplicate. As nice as it is being able to edit every class from one inspector, if they get nested quite deep I imagine it could get a bit complex so breaking a piece off onto a separate object gives you control over organisation. You can use Coroutines and InvokeRepeating from a script then, and all those other unity specific things. It is quite annoying not being able to switch between the features of monobehaviour and the lightness of a none-monobehaviour.

What if in the serializer you 'secretly' store the reference to the unity component, so you can then look up the class reference on that instance and pass it thru without anything knowing thats what is happening :-) I confess serialization is a mystery / voodoo to me

If this is possible, you are then effectively connecting things like nodes, each node does its own thing but you can also connect a node to multiple places. At this point a visual editor that is spatial/node based starts having benefits too as you can break out of hierarchies, just a thought ;-D This is kinda how I expected node based editors to work in Unity with context sensitive ways to plug one thing into another based on interfaces

I have used this to make classes that instead of using a float or int, use my own type based on interface.This allows you to pick in the editor from dozens of different number generator, or share a variable between multiple components. So I have a script that Instances a model over an array of Vector3 points. It requires a script that is interface IPointGen, I then have a bunch of IPointGen scripts say 2D Grid, 3D grid, Circular Grid so on, so I pick 2D grid, It has public vars for column count and width, they use my custom value type so I can then again pick from multiple scripts that can generate float values, say a random number generator, a number generator that changes based on music volume, one that is based on sine wave, etc, so I hook up one that makes random number between min and max every 0.5 seconds. They all hook up to each other with delegates to notify of value change. So I change the value of how often the number generator runs to 0.2 seconds it notices the change and notifies the 2D Grid, so it redraws the grid of points and notifies the "Instance over points" script, and it runs and boom I am instancing cubes all over the constantly changing point grid. Lots of little dumb reusable blocks of logic. This however only works with IUnified which has its own cons, but workflow wise is brilliant, performance/memory wise probably not so good :-)

Ah the current master would be helpful I hate redoing work, thanks :-D

On Fri, Jun 13, 2014 at 1:38 AM, jacobdufault notifications@github.com wrote:

First, let me make sure I understand you right: You want interfaces that have shared instances across multiple behaviors.

This is tricky but also interesting. I think this is doable (based on some preliminary investigations), but due to the way that serialization works within Full Inspector the shared interfaces are going to have to derive from ScriptableObject (no need for the overhead of MonoBehaviors). This is more or less unavoidable without completely rewriting the serialization adapters, and likely impossible (aside: it would have been easier to do this in FI1, but in FI1 you couldn't just add a reference to a UnityEngine.Object type -- you had to use Ref instead -- I'm really happy that's gone :)).

The upcoming release handles the ScriptableObject serialization fine, but some extra editor work will need to be done to allow for copying and pasting ScriptableObjects that live in the scene so that you can state which instance should be shared. Either it'll handle it by default or you'll have to add an annotation like [InspectorSharedInstance]. Not sure yet -- haven't implemented anything.

If you want to try it on you're own, I would recommend writing an attribute based property editor. The ObjectPropertyEditor has changed quite a bit to allow for the inline object dropdowns. If you want the current master so you can work off of that, just send me an email and I'll

get a build to you.

Note on serialization

Depending on how you reference everything, Json.NET may not handle this situation well. However, for these cases you can just use Full Serializer http://github.com/jacobdufault/fullserializer, which handles inheritance correctly.

Example of a Json.NET error case:

public interface IBaseShared { int GetInt();} public abstract class BaseShared : BaseScriptableObject, IBaseShared { public abstract int GetInt();} public class SharedContainer : BaseBehavior { public IBaseShared SharedInstance;}

to fix for Json.NET you need to remove IBaseShared, ie, change the code to

public abstract class BaseShared : BaseScriptableObject { public abstract int GetInt();} public class SharedContainer : BaseBehavior { public BaseShared SharedInstance;}

— Reply to this email directly or view it on GitHub https://github.com/jacobdufault/fullinspector/issues/64#issuecomment-45964601 .

jacobdufault commented 10 years ago

MonoBehaviors would still work perfectly, it would just be painful to use them with Json.NET.

What if in the serializer you 'secretly' store the reference to the unity component, so you can then look up the class reference on that instance and pass it thru without anything knowing thats what is happening :-) I confess serialization is a mystery / voodoo to me

Yea, you can do something like this, but I'm hesitant to introduce such a feature because it seems like it would be brittle without quite a bit of work, not to mention that I'm not even sure if it's possible with protobuf-net.

If this is possible, you are then effectively connecting things like nodes, each node does its own thing but you can also connect a node to multiple places. At this point a visual editor that is spatial/node based starts having benefits too as you can break out of hierarchies, just a thought ;-D This is kinda how I expected node based editors to work in Unity with context sensitive ways to plug one thing into another based on interfaces

One of the long-term plans I have is to do some experimental editor UX, which would include a node-based visualizer. However, this won't happen for quite a while (3+ months) if it ever does.

Can you send me an email (myname@gmail.com) so that I can send you the master build?

HeliosDoubleSix commented 10 years ago

Hmm Serialization is ruining my dreams!!

Well I'd love to have input and help in anyway I can with the experimental UX, I'm well versed in all the hicks law, fitts law, occams razor human interface stuff :-D

I'm just trying to find a way to use Unity with little reusable blocks of code that link up in a contextual/interface based fashion and smart variables that you can hook up to number generators or other kinds of logic based on context/interface. That to me just seems like how it should work, I'd be interested to know your thoughts on this when you have time, maybe I am missing something, but that's my idea of modular components, not sticking sendMessage on everything :-S

I have looked at a lot of assets on the store and they miss the mark, are too complex or too proprietary. C# itself handles all this stuff but isn't exposed in a helpful way in the editor. I feel it is very close with your tool, just the serialization problem with the mono/none mono divide needing to be bridged :-/ It seems crazy to have to write your own entire scene organizer and inspector. For it to work it has to leverage the existing structure unity provides which means monobehaviours/components, gameobjects, prefabs, scenes.

The new GUI stuff is bringing with it a few side effects that may help, weak referenced delegates based event system of some sort, and some kind of behaviours system where you can pick from a list of scripts, and then pick from any variables inside that to modify them, not sure if that is called binding? though it appears to be just delegate based, but looks nice to have it in the visual editor for sure.

As they say "the new UI is following 'normal' c# patterns. We use interfaces for knowing what gets call backs, and a whole bunch of other good things. No magic methods being called. It's designed to be super extendible. All the components and API we provide are written against public Unity API, no internal tricks or hidden stuff." So it looks like they have learned their lesson :-)

But I digress, thanks for listening

On Fri, Jun 13, 2014 at 9:51 PM, jacobdufault notifications@github.com wrote:

MonoBehaviors would still work perfectly, it would just be painful to use them with Json.NET.

What if in the serializer you 'secretly' store the reference to the unity component, so you can then look up the class reference on that instance and pass it thru without anything knowing thats what is happening :-) I confess serialization is a mystery / voodoo to me

Yea, you can do something like this, but I'm hesitant to introduce such a feature because it seems like it would be brittle without quite a bit of work, not to mention that I'm not even sure if it's possible with protobuf-net.

If this is possible, you are then effectively connecting things like nodes, each node does its own thing but you can also connect a node to multiple places. At this point a visual editor that is spatial/node based starts having benefits too as you can break out of hierarchies, just a thought ;-D This is kinda how I expected node based editors to work in Unity with context sensitive ways to plug one thing into another based on interfaces

One of the long-term plans I have is to do some experimental editor UX, which would include a node-based visualizer. However, this won't happen for quite a while (3+ months) if it ever does.

Can you send me an email (myname@gmail.com) so that I can send you the master build?

— Reply to this email directly or view it on GitHub https://github.com/jacobdufault/fullinspector/issues/64#issuecomment-46058408 .

Wenzil commented 10 years ago

I have thought of something similar myself but wasn't sure if it was possible to implement. It would be very similar in nature to traditionnal dependency injection frameworks, but here configuration would take place in the editor. Here's the way I thought it could maybe work:

Define a generic "bean" BaseScriptableObject that wraps a single generic variable. This variable holds the object you want to share across multiple behaviors, and is serialized with the bean. Because it is a ScriptableObject, it appears in the project library and can be duplicated, edited, deleted, etc.

public class Bean<T> : BaseScriptableObject
{
    [SerializeField]
    [ShowInInspector]
    public T sharedObject { get; private set; }
}

Now we need a way to create beans from the editor, and i'm not sure if this is possible. You would have to somehow select the generic type first. If this is possible, keep on reading!

The last step is to have a way to inject the sharedObject of a bean into suitable behaviors. E.g. say we have a behavior which exposes a field of type ISomeInterface and we want to inject a SomeClass bean inside that.

public interface ISomeInterface {}
public class SomeClass : ISomeInterface {}
public class ArbitraryBehavior : BaseBehavior
{
    [InjectBean]
    [SerializeField]
    [ShowInInspector]
    public ISomeInterface someInterfacedMember;
}

Notice the [InjectBean] attribute. FullInspector needs to know somehow that this someInterfacedMember needs to be inspected / serialized in a special way. It would function very similarly to how we can already drag-and-drop ScriptableObjects into behaviors, except here you would assign/type-check the wrapped sharedObject as opposed to the SriptableObject itself.

So as a user, all you'd need to do is

  1. click on "Create Bean" or some such in the editor
  2. select the type you want for the sharedObject inside the bean (the bean is instantiated with the selected generic type and is added to the project library)
  3. edit the sharedObject inside the bean as you please from the project library
  4. expose the field you want to inject into and give it the [InjectBean] attribute
  5. drag and drop the bean into the field you exposed
  6. repeat 4 and 5 for as many behaviors as you want, effectively sharing the sharedObject

I think it would be a neat addition to FullInspector, but again I'm not sure if it could work! Thoughts?

jacobdufault commented 10 years ago

I think this is possible, but you'll likely have to do some code generation in scripts to get past the generics issue (though the code gen will be really simple -- just a simple text replacement from an initial script you define). It should be completely doable as a module without any internal FI modifications (except for adding custom serialization converters).

The code gen script could just be:

public class {0} : BaseBean<{1}> {}

where BaseBean is

public abstract class BaseBean<T> : BaseScriptableObject
{
    [SerializeField]
    [ShowInInspector]
    public T sharedObject { get; private set; }
}

With regards to actual bean usage, you could customize the serializers so that adding [InjectBean] causes different serialization, but it would be a tricky path fraught with peril (you essentially have to mimic the entire UnityObject serialization schema). Instead, what would probably be easier is to just define a BeanReference<T> type and then write custom converters for that. So your sample just becomes

public interface ISomeInterface {}
public class SomeClass : ISomeInterface {}
public class ArbitraryBehavior : BaseBehavior
{
    [SerializeField]
    [ShowInInspector]
    public BeanReference<ISomeInterface> someInterfacedMember;
}

where BeanReference<T> is something like

public class BeanReference<T>
{
    public BaseBean<T> sharedInstance;
}

The overall idea seems nice (it seems to nicely generalize configuration and shared instances); however, I'm not going to have time to implement something like this for a few months. If you end up doing it, I'd be happy to take a look and include it as a module.

jacobdufault commented 10 years ago

Or why not just change the example to

public interface ISomeInterface {}
public class SomeClass : ISomeInterface {}
public class ArbitraryBehavior : BaseBehavior
{
    [SerializeField]
    [ShowInInspector]
    public BaseBean<ISomeInterface> someInterfacedMember;
}

and then write a custom property editor for BaseBean<T>, which will completely avoid the need for custom serialization routines. Then you just need to create a dialog that automates the bean creation scripts and voila! good to go.

jacobdufault commented 10 years ago

I've got a prototype working.

image

It's a few hundred lines of editor code, a fair amount dealing with getting around Unity serialization (specifically generic scriptable object types).

Usage looks like this:

using FullInspector;

public struct SampleStruct {
    public int Val1;
    public int Val2;
}

public class SharedInstanceTest : BaseBehavior {
    public SharedInstance<SampleStruct> SharedStruct;
}

and SharedInstance is defined as thus:

namespace FullInspector {
    public class SharedInstance<TInstance> : BaseScriptableObject {
        public TInstance Instance;
    }
}
Wenzil commented 10 years ago

That's excellent! I wonder if the standard object picker button (the small circle) could act as the "Select Instance" button here? It would be fantastic also if you could create/edit the shared instance from the project library directly and then drag-and-drop it wherever you need.

jacobdufault commented 10 years ago

Like this? :)

I don't think I'll do the little circle thing -- atm you can still drag-and-drop onto it. The circle button isn't as intuitive of a UX and I would have to rewrite the ObjectPropertyEditor (currently I just call into the Unity one).

Wenzil commented 10 years ago

That was quick! Very nice :)