Skip to content

[FEATURE] Add constructed generic candidate support and popup performance improvements#100

Open
dichotomy-corp wants to merge 2 commits intomackysoft:mainfrom
dichotomy-corp:feature/constructed-generic-candidate-support
Open

[FEATURE] Add constructed generic candidate support and popup performance improvements#100
dichotomy-corp wants to merge 2 commits intomackysoft:mainfrom
dichotomy-corp:feature/constructed-generic-candidate-support

Conversation

@dichotomy-corp
Copy link

Description

This PR adds support for constructed generic candidate types in the SubclassSelector popup when the field type is a closed generic interface or base class (Unity 2023.2+), along with several performance improvements to the popup display path.

Motivation

Currently, when a field is declared as [SerializeReference, SubclassSelector] IValueProvider<int>, the popup correctly discovers non-generic concrete classes that explicitly close the type parameter (e.g. class ConcreteIntProvider : IValueProvider<int>). However, it does not discover generic classes like ConstantValueProvider<T> : IValueProvider<T> — even though the type arguments can be inferred from the field type and the constructed type ConstantValueProvider<int> is fully serializable.

This means users are forced to write one boilerplate subclass per type argument:

[Serializable]
public class ConstantIntValueProvider : ConstantValueProvider<int> { }

This PR removes that requirement by constructing closed generic types at editor time via MakeGenericType, and also addresses a few performance bottlenecks that become more noticeable when scanning all loaded assemblies.

Example

using System;
using UnityEngine;

public class TestBehavior : MonoBehaviour
{
    [SerializeReference, SubclassSelector] private IValueProvider<int> intValueProvider;
    [SerializeReference, SubclassSelector] private IValueProvider<Color> colorValueProvider;
    [SerializeReference, SubclassSelector] private IValueProvider<Vector3> vector3ValueProvider;
    [SerializeReference, SubclassSelector] private IValueProvider<CustomData> customDataValueProvider;
}

public interface IValueProvider<out T>
{
    T GetValue();
}

[Serializable]
public class ConstantValueProvider<T> : IValueProvider<T>
{
    [SerializeField] private T value;
    
    public T GetValue()
    {
        return  value;
    }
}

[Serializable]
public class CustomData
{
    public string customName;
    public Vector4 customData;
}
example_screenshot

A note on design decisions

I understand that the existing !candiateType.IsGenericType filter in DefaultIntrinsicTypePolicy and the overall architecture were likely deliberate choices. If any of these changes conflict with design goals I'm not aware of — such as keeping the intrinsic policy conservative, avoiding MakeGenericType in editor code, or maintaining the separation of filtering responsibilities between provider and service — I'm completely open to adjusting or withdrawing parts of this PR. I've tried to keep the changes minimal and contained to the 2023.2+ code path where possible.


Changes made

1. Constructed generic candidate support (Unity 2023.2+)

DefaultIntrinsicTypePolicy.cs

  • Changed !candiateType.IsGenericType to !candiateType.ContainsGenericParameters. This allows closed constructed generics (e.g. ConstantValueProvider<int>) to pass through while still blocking open generic definitions (e.g. ConstantValueProvider<T>). This is the only change that affects the non-2023.2 code path.

Unity_2023_2_OrNewer_TypeCandiateProvider.cs

  • Extended GetTypesWithGeneric to detect open generic type definitions in the assembly scan, attempt to infer and map their type parameters from the field's base type, and construct closed generic types via MakeGenericType.
  • Added three private helper methods: TryCloseGenericType (walks interfaces and base class chain to find matching generic definitions), TryMapTypeArguments (maps generic parameters from the candidate to concrete type arguments from the field type), and TryMakeGenericTypeSafe (wraps MakeGenericType in a try-catch for constraint violations).

2. Generic-aware display names

TypeMenuUtility.cs

  • Added GetNiceGenericName and GetNiceGenericFullName helpers that produce human-readable names for constructed generic types (e.g. ConstantValueProvider<Int32> instead of ConstantValueProvider`1[[System.Int32, ...]]).
  • Updated GetSplittedTypePath to use these helpers for the popup menu paths.
  • Updated OrderByType to use GetNiceGenericName as the fallback sort key.

SubclassSelectorDrawer.cs

  • Updated GetTypeName to use GetNiceGenericName so that the inline label displays correctly for constructed generic types (the backtick in raw type.Name caused ObjectNames.NicifyVariableName to produce an empty string).

3. Performance improvements

These changes address noticeable lag (~500ms) when opening the popup, particularly when generic field types trigger a full assembly scan.

TypeCandiateService.cs

  • Removed the redundant intrinsicTypePolicy.IsAllowed and typeCompatibilityPolicy.IsCompatible filtering in GetDisplayableTypes, since both providers already apply these filters internally. Simplified the constructor to only require ITypeCandiateProvider.

TypeSearchService.cs

  • Updated the TypeCandiateService construction to match the simplified constructor.

TypeMenuUtility.cs

  • Added a Dictionary<Type, AddTypeMenuAttribute> cache for GetAttribute to avoid repeated Attribute.GetCustomAttribute reflection calls (invoked multiple times per type during sorting and path resolution on every popup open).
  • Added a Dictionary<Type, string[]> cache for GetSplittedTypePath results.
  • Added a Dictionary<string, string> cache for ObjectNames.NicifyVariableName results via a new CachedNicifyVariableName method. All caches include null guards for safety.

AdvancedTypePopup.cs

  • Moved the OrderByType sorting from BuildRoot (called by Unity on every Show) into SetTypes (called once at construction), so the type array is sorted once and reused across popup opens.

All static caches are naturally cleared on domain reload (script recompilation), so newly added subclasses are always picked up.

Add support for discovering and constructing closed generic candidate
types (e.g. ConstantValueProvider<int>) when the field type is a closed
generic interface or base class (Unity 2023.2+). Previously, only
non-generic concrete classes that explicitly closed the type parameter
were shown in the popup.

Changes:
- Relax intrinsic type policy to allow constructed generics through
- Infer type arguments and construct closed types via MakeGenericType
- Add generic-aware display names for popup menu and inline labels
- Merge dual assembly scans into a single pass
- Remove redundant filtering in TypeCandiateService
- Cache attribute lookups, type paths, and nicified names
- Pre-sort type array once at construction instead of on every Show
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant