Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Automatically fill Dropdown with abstract subclasses

Discussion in 'Scripting' started by Karrzun, Sep 28, 2020.

  1. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    123
    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.

    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:

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Reflection;
    6.  
    7.  
    8. [CreateAssetMenu(fileName = "New PhotoCollection", menuName = "ScriptableObjects/PhotoCollection")]
    9. public class PhotoCollection : ScriptableObject
    10. {
    11.     [SerializeField]
    12.     private Motif motif;
    13.  
    14.     // this is a serializable dictionary that enables
    15.     // editing the rewards in the editor
    16.     [SerializeField]
    17.     private MotifRewardDictionary collection;
    18.  
    19.     [SerializeField]
    20.     private int completionReward;
    21.  
    22.  
    23.  
    24. #if UNITY_EDITOR
    25.     public void OnValidate()
    26.     {
    27.         if (motif == null)
    28.             return;
    29.  
    30.         //
    31.         var myTypes = Assembly.GetAssembly(typeof(Motif)).GetTypes().Where(myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf(motif.GetType()));
    32.  
    33.         // TODO populate collection
    34.  
    35.  
    36.         foreach (KeyValuePair<Motif, PhotoReward> pair in collection)
    37.         {
    38.             if (pair.Value.Amount == 0)
    39.             {
    40.                 Debug.LogWarning("Reward for " + pair.Key + " is 0.");
    41.             }
    42.         }
    43.  
    44.         if (completionReward == 0)
    45.         {
    46.             Debug.LogWarning("CompletionReward is 0.");
    47.         }
    48.     }
    49. #endif
    50.  
    51.  
    52.     public void AddPhotoToCollection (Motif motif)
    53.     {
    54.         // the photo doesn't involve a motif that we want for this collection
    55.         if (!collection.ContainsKey(motif))
    56.             return;
    57.  
    58.         // a photo with this motif was already added to this collection
    59.         if (collection[motif].IsCollected == true)
    60.             return;
    61.  
    62.         collection[motif].IsCollected = true;
    63.         // TODO trigger payment event
    64.    
    65.         if (IsComplete())
    66.         {
    67.             // TODO trigger payment event
    68.         }
    69.     }
    70.  
    71.  
    72.     private bool IsComplete()
    73.     {
    74.         foreach (var value in collection.Values)
    75.         {
    76.             if (value.IsCollected == false)
    77.             {
    78.                 return false;
    79.             }
    80.         }
    81.  
    82.         return true;
    83.     }
    84.  
    85.  
    86. }

    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.
     
    Last edited: Sep 29, 2020
  2. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    488
    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?
     
  3. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    123
    Sorry if I've been unclear here. Currently my inspector looks like this:

    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:

    Inspector_Supposed.PNG
     
  4. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    488
    Will this help?
    https://stackoverflow.com/questions/1524562/getting-parent-class-name-using-reflection

    You could probably use something along the lines of:
    Code (CSharp):
    1. foreach (Type type in Assembly.GetAssembly(typeof(Motif)).GetTypes()
    2.     .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.
     
    Karrzun likes this.
  5. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    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.
     
    Karrzun likes this.
  6. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    123
    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 (CSharp):
    1. public abstract class Motif
    2. {
    3.     protected Album _album;
    4.     public Album Album
    5.     {
    6.         get
    7.         {
    8.             return _album;
    9.         }
    10.         private set
    11.         {
    12.             _album = value;
    13.         }
    14.     }
    15. }
    16.  
    17.  
    18. public abstract class Animal : Motif
    19. {
    20.     protected string name;
    21.     public string Name => name;
    22.  
    23.     protected string latinName;
    24.     public string LatinName => latinName;
    25.    
    26.     protected Sex sex;
    27.     public Sex Sex => sex;
    28.  
    29.  
    30.     public Animal(string name, string latinName, Sex? sex = null) : base()
    31.     {
    32.         this.name = name;
    33.         this.latinName = latinName;
    34.  
    35.         if (sex == null)
    36.         {
    37.             System.Array values = System.Enum.GetValues(typeof(Sex));
    38.             this.sex = (Sex)values.GetValue(UnityEngine.Random.Range(0, values.Length));
    39.         }
    40.     }
    41. }
    42.  
    43.  
    44.  
    45.  
    46. public enum Sex
    47. {
    48.     Female,
    49.     Male
    50. }
    51.  
    52.  
    53. public abstract class Bear : Animal
    54. {
    55.     public Bear(string name, string latinName) : base(name, latinName)
    56.     {
    57.         _album = Album.Bears;
    58.     }
    59. }
    60.  
    61.  
    62. public class BrownBear : Bear
    63. {
    64.     public BrownBear() : base("Brown Bear", "Ursus arctos")
    65.     {
    66.  
    67.     }
    68. }
    69.  
    70.  
    71. public class PolarBear : Bear
    72. {
    73.     public PolarBear() : base("Polar Bear", "Ursus maritimus")
    74.     {
    75.  
    76.     }
    77. }


    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?