Why do ObjectPickers display objects of incompatible type?

Assume you have a generic Pet class with an Animal constraint:

public abstract class Pet<T> : Animal where T : Animal
{
}
public abstract class Animal : ScriptableObject
{
}

And you create two types of Pets, a cat and a dog:

[CreateAssetMenu(menuName = "CatSO")]
public class Cat : Pet<Cat>
{
}
[CreateAssetMenu(menuName = "DogSO")]
public class Dog : Pet<Dog>
{
}

Now a base class for all pet owners with a pet field:

public class Owner<T> : MonoBehaviour where T : Animal
{
    [SerializeField] protected Pet<T> ownersPet;
}

And finally a derived version for dog owners:

public class DogOwner : Owner<Dog>
{
}

Now you assign a DogOwner component to a gameObject, and create a CatSO and a DogSO.

When I use the object picker for the ownersPet field in the DogOwner component I can see both CatSO and DogSO, despite only being able to assign the DogSO.

You can imagine how much this slows design down when you have hundreds of pets of different species.

Here’s a screenshot from 2021.3.4f1

Is this a bug? Is there a way to fix it?

the field is of type Pet therefore it will allow assignment of all matching types (ie all subclasses of Pet).

you can move the field into DogOwner and make it of type Dog

or a custom property drawer perhaps, one for each type of owner (DogOwnerDrawer, CatOwnerDrawer etc) and restrict assignment there if you really need the field in the base class

It doesn’t allow assignment of all Pet subclasses though, which is great! I want to only be able to assign Dogs to classes deriving from Owner and so on.

It just still shows up in the object picker’s list as an option even though it can’t be assigned.

Anyone able to explain this issue?

I feel as if it was already explained. The field is of type Pet, so both cat and dog qualify for this signature.

Isn’t the field of type Pet seeing as DogOwner inherits from Owner?
If it’s of type Pet why can’t I assign a Cat to it?

I’m sorry but I still don’t understand.

I mean it’s probably something to do with Unity’s partial support of serialising generics, and how the object picker sees the value. Unity does serialisation directly on the fields, so something at some point is still seeing Pet declared in the base class.

Unity’s object picker is very basic and lacks any form of customisation, so the issue could just be there.

To be honest the above architecture is more limiting than anything, and you would probably do better not use a structure like this in the context of Unity. Remember, composition over inheritance!

Understanding what you’re trying to accomplish here would probably allow us to help better.

Okay, good to know.

So the whole DogOwner thing is just an example of the issue, what I’m actually trying to accomplish is a generic state machine, such that a PlayerStateMachine inhertis from StateMachine and has a State currentState which runs a list of Action ScriptableObjects and changes state when a Condition is true so on and so forth. Basically a generic version of the state machine used on Unity’s Open Project 1.

Imagine trying to design a new enemy, and you’re fleshing out it’s transition table with conditions upon which it should change from one state to another, but each time you open the object picker you have to scroll through all the other conditions meant for other state machines. The generic solution solves accidentally assigning incompatible types, but sadly the object picker still shows them.

Looking back at the code, can’t protected Pet<T> ownersPet; just become protected T ownersPet;? That would fix the picking issue.

Edit: The declaration of your Monobehaviour might have to change to: public class Owner<T> : Monobehaviour : where T : Pet<T>

You’re right it would in this simplified instance.
But I can’t apply that change to the state machine I’m working on (and lesson learnt, I won’t over simplify these small details in the future).

Simplifying the problem down is fine, though having an idea of what your trying to accomplish is the most helpful, as often the solution is not to find a solution to the current problem, but change what you’re trying to do.

Nonetheless, the long and short is that - based on my experience - the object picker requires a concrete type to function properly, and doesn’t play well with generics like SomeType, though whether that’s due to serialisation or the picker itself, I wouldn’t know.

Probably the latter, as when I use Odin serialisation to serialise UnityEngine.Object’s via Interfaces, the object picker (understandably) has zero support for that and shows nothing.

To that end it might be better to look at introducing your own inspector code to provide the work-flow that you want, which is what I do. Hell, I seldom ever use the Object Picker these days.

Thanks spiney199, I’ll give that a try and see if I can find my serialized happy place (:

Hi! The object selector receives the correct type (Pet1[Dog]) which is used to confirm if the selected item can actually be assigned. The problem lies with how the assets are filtered when shown in the view, which is a completely different system. In that system, the type is converted to a string, which ends up being Pet1. So this is why you can see “New Cat” and “New Dog”.

As a workaround, with the latest version of 2022.2 alpha, you can create your own advanced custom picker. You will have to set your search engine to advanced:


Then you can create your own picker script:

using System;
using System.Linq;
using UnityEditor.Search;
using UnityEditor.SearchService;

public static class CustomSelectors
{
    [AdvancedObjectSelectorValidator("animal_picker")]
    static bool CanOpenAnimalPicker(ObjectSelectorSearchContext context)
    {
        if (context.requiredTypes != null && context.requiredTypes.All(t => typeof(Animal).IsAssignableFrom(t)))
            return true;
        return false;
    }

    [AdvancedObjectSelector("animal_picker", 0)]
    static void AnimalPicker(AdvancedObjectSelectorEventType eventType, in AdvancedObjectSelectorParameters parameters)
    {
        if (eventType != AdvancedObjectSelectorEventType.OpenAndSearch)
            return;

        var selectContext = parameters.context;
        var viewFlags = SearchFlags.OpenPicker;

        var searchQuery = BuildInitialQuery(selectContext);
        var selectHandler = parameters.selectorClosedHandler;
        var trackingHandler = parameters.trackingHandler;
        var viewState = new SearchViewState(
            SearchService.CreateContext("asset", searchQuery, viewFlags), selectHandler, trackingHandler,
            selectContext.requiredTypeNames.First(), selectContext.requiredTypes.First());
        viewState.ignoreSaveSearches = true;
        SearchService.ShowPicker(viewState);
    }

    static string BuildInitialQuery(in ObjectSelectorSearchContext selectContext)
    {
        var query = string.Empty;
        var types = selectContext.requiredTypes.ToArray();
        var typeNames = selectContext.requiredTypeNames.ToArray();
        for (int i = 0; i < types.Length; ++i)
        {
            string name;
            if (types[i].IsGenericType && types[i].GetGenericTypeDefinition() == typeof(Pet<>))
                name = GetPetType(types[i]);
            else
                name = types[i]?.Name ?? typeNames[i];
            if (query.Length != 0)
                query += ' ';
            query += $"t:{name}";
        }
        return query;
    }

    static string GetPetType(Type petType)
    {
        var genericTypes = petType.GenericTypeArguments;
        if (genericTypes == null || genericTypes.Length != 1)
            return string.Empty;
        return genericTypes[0].Name;
    }
}

The picker will only open when the required types are of type “Animal”, and it will set the query to the exact “Animal” type:

Note: There seems to be a small bug when reopening an advanced custom picker, the previous query gets restored instead of the query of the new picker. If that is the case, just press escape and the proper query should be restored.

Wow Sebastien that’s tremendously helpful info, thank you!

1 Like