Automatically fill Dropdown with abstract subclasses

Hi everyone!

I’m currently working on a little project focused on in-game photography, roughly comparable to the photo features from Beyond Good and Evil or Sly Cooper. I’m still at the very beginning, designing the way I bundle motifes into different photo albums. To reward the player some payment in game, I’d like to assign each motif a value that’s paid when the players takes a photo of this motif for the very first time. Additionally, the players recieves a bonus payment for completing a photo album.

Now, for setting up those albums I thought it might be a good idea to use reflection to ensure that every motif is part of the correct album. I’d like to do that by using a ScriptableObject and selecting the topic of the album. Using reflection, the ScriptableObject then automatically populates a collection with all corresponding motifes belonging to that topic so I’m neither missing any nor using them multiple times. Then, I could assign the reward for each motif and the bonus reward for completing the album.

Example:

This is an (incomplete) overview of how I’m currently structuring my motifes.

6359898--707607--Structure.png

Now, if I create a new album, I’d like to select Dogs from a dropdown menu of possible topics. The corresponding collections is then populated with all subclasses of dogs - in this case, Dalmation and Dachshund. The list of possible topics is supposed to be the last layer of abstract classes, so the 3rd layer (currently consisting of Bears, Dogs, and Insects).

Unfortunately, I don’t know how to do that and so far didn’t find anything online that showed me I could achieve that. My ScriptableObject script currently looks like this:

using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;


[CreateAssetMenu(fileName = "New PhotoCollection", menuName = "ScriptableObjects/PhotoCollection")]
public class PhotoCollection : ScriptableObject
{
    [SerializeField]
    private Motif motif;

    // this is a serializable dictionary that enables
    // editing the rewards in the editor
    [SerializeField]
    private MotifRewardDictionary collection;

    [SerializeField]
    private int completionReward;



#if UNITY_EDITOR
    public void OnValidate()
    {
        if (motif == null)
            return;

        //
        var myTypes = Assembly.GetAssembly(typeof(Motif)).GetTypes().Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(motif.GetType()));

        // TODO populate collection


        foreach (KeyValuePair<Motif, PhotoReward> pair in collection)
        {
            if (pair.Value.Amount == 0)
            {
                Debug.LogWarning("Reward for " + pair.Key + " is 0.");
            }
        }

        if (completionReward == 0)
        {
            Debug.LogWarning("CompletionReward is 0.");
        }
    }
#endif


    public void AddPhotoToCollection (Motif motif)
    {
        // the photo doesn't involve a motif that we want for this collection
        if (!collection.ContainsKey(motif))
            return;

        // a photo with this motif was already added to this collection
        if (collection[motif].IsCollected == true)
            return;

        collection[motif].IsCollected = true;
        // TODO trigger payment event
   
        if (IsComplete())
        {
            // TODO trigger payment event
        }
    }


    private bool IsComplete()
    {
        foreach (var value in collection.Values)
        {
            if (value.IsCollected == false)
            {
                return false;
            }
        }

        return true;
    }


}

Can anyone point me into the right direction here?

Thank you in advance!

//Edit: Obviously, some things like !myType.IsAbstract are completely wrong but I tried displaying some concrete subclasses (dachshund, etc) first, but couldn’t even manage to do that - let alone display abstract classes.

What’s your problem here exactly? From what I can tell, Line 31 gives you all subclasses that are NOT abstract. That should be a part of the dropdown right? Do you want to then get all subclasses for a specific type?

Sorry if I’ve been unclear here. Currently my inspector looks like this:

6365388--708348--Inspector_Currently.PNG

I’d like to have a field motif that (via Dropdown) lets me select all abstract classes that are extended by non-abstract classes, making it look something like this:

6365388--708351--Inspector_Supposed.PNG

Will this help?
https://stackoverflow.com/questions/1524562/getting-parent-class-name-using-reflection

You could probably use something along the lines of:

foreach (Type type in Assembly.GetAssembly(typeof(Motif)).GetTypes()
    .Where(myType => myType.IsClass && !myType.IsAbstract && !myType.IsGenericType && myType.IsSubclassOf(typeof(Motif))))

to get all child classes. From there, my guess is that you use the info from the link above to get parent classes and them remove duplicates.

It’s just something I came up with now, you could probably optimize this a lot.

2 Likes

Is there any particular reason that you’re planning to create subclasses for every motif in order to “know” what specific type the photo was taken of?

Instead, I’d recommend to make the distinction by data, not by type, as it seems all you need to do is associating rewards or similar stuff with a specific motif.

That’s by far more maintainable. The specific “type” of motif, or an identifier, the category etc can be modelled using pure data. That can also very well be accomplished with ScriptableObjects, a text file or anything that serves as a simple “database”.

Given the vague description you can already get away with a type that has a some sort of category (or collection ID), a motif ID, and the reward to pay once the motif was added to the collection.

1 Like

Thank you both for your responses.

Yeah, that’s something along the lines of what I’m currently using. Thank you for your suggestion.

Later on, after the basic setup is done, I’d like to provide additional information when a photo is taken. For example, when taking a picture of an animal, I’d like to briefly show a pop up window that displays its name, scientific name, genus, sex, maybe a short description, and alike.

I had imagined it would be best to handle this with the formerly described setup as most values are class specific and dependant on each other and, thus, won’t change between instances, currently leading to something like this:

Code Block

public abstract class Motif
{
    protected Album _album;
    public Album Album
    {
        get
        {
            return _album;
        }
        private set
        {
            _album = value;
        }
    }
}


public abstract class Animal : Motif
{
    protected string name;
    public string Name => name;

    protected string latinName;
    public string LatinName => latinName;
   
    protected Sex sex;
    public Sex Sex => sex;


    public Animal(string name, string latinName, Sex? sex = null) : base()
    {
        this.name = name;
        this.latinName = latinName;

        if (sex == null)
        {
            System.Array values = System.Enum.GetValues(typeof(Sex));
            this.sex = (Sex)values.GetValue(UnityEngine.Random.Range(0, values.Length));
        }
    }
}




public enum Sex
{
    Female,
    Male
}


public abstract class Bear : Animal
{
    public Bear(string name, string latinName) : base(name, latinName)
    {
        _album = Album.Bears;
    }
}


public class BrownBear : Bear
{
    public BrownBear() : base("Brown Bear", "Ursus arctos")
    {

    }
}


public class PolarBear : Bear
{
    public PolarBear() : base("Polar Bear", "Ursus maritimus")
    {

    }
}

I’m well aware that this leads to a whole lot of very similiar code that, making it look kind of redundant, which of course is something I dislike. Then again, I need to ensure that any instance of Motif with the name value “Polar Bear” only ever has the latinName value “Ursus maritimus”, so I’d like to couple those assignments to ensure Data Validity.
However, as I’m still unsatisfied with the current approach I’m thinking about switching to a factory model. Do you think that’s reasonable?