Search Unity

How to make a dropdown of a list of database objects in a class?

Discussion in 'Scripting' started by SourPatch427618, Dec 3, 2019.

  1. SourPatch427618

    SourPatch427618

    Joined:
    Mar 5, 2017
    Posts:
    12
    Hi all, I did some searching and could not really find anything that helped me figure this out. I have a class called MoveDatabase that holds all of my moves inside a scriptable object. Moves are serialized in the inspector. Looks like this.

    MoveDatabase
    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. [CreateAssetMenu(fileName = "Moves", menuName = "Create Move Database/Add Database", order = 5)]
    6. public class MoveDatabase : ScriptableObject
    7. {
    8.     public List<Move> Moves;
    9.  
    10.     //This is called when the object is created from menu
    11.     private void Awake()
    12.     {
    13.         Debug.Log("We are awake");
    14.     }
    15.  
    16.     private void OnEnable()
    17.     {
    18.         //Debug.Log("We are enabled");
    19.    
    20.         int addAmount = 0;
    21.  
    22.         foreach (Move thisMove in Moves)
    23.         {
    24.             //Auto increament the move ID when a new move is created
    25.             thisMove.moveID = addAmount++;
    26.         }  
    27.     }
    28. }
    29.  
    30. [System.Serializable]
    31. public class Move
    32. {
    33.     public string moveName;
    34.     public int moveID;
    35.     public string moveDesc;
    36.     public MoveType moveType;
    37.     public AttackType attackType;
    38.     public float movePower;
    39.     public string bonusStat;
    40.     public float moveCharges;
    41.     public float currentMoveCharges;
    42.     public float accuracy;
    43.     public float statusEffectChance;
    44.  
    45.  
    46.     public enum MoveType
    47.     {
    48.         Light,
    49.         Dark,
    50.         Psychic,
    51.         Space,
    52.         Time,
    53.         Soul,
    54.         Gravity,
    55.         Fire,
    56.         Grass,
    57.         Water,
    58.         Electric,
    59.         Air,
    60.         Earth,
    61.         Ice,
    62.         Normal,
    63.         Metal,
    64.         Toxic,
    65.         None
    66.     }
    67.  
    68.     public enum AttackType
    69.     {
    70.         PhysicalAttack,
    71.         SpecialAttack,
    72.         BoostMove
    73.     }
    74.  
    75.  
    76.  
    77.     public Move(
    78.         string name,
    79.         int id,
    80.         string desc,
    81.         MoveType type,
    82.         AttackType atype,
    83.         float power,
    84.         string bonus,
    85.         float charges,
    86.         float currentCharges,
    87.         float accurate,
    88.         float effectChance
    89.  
    90.         )
    91.     {
    92.         moveName = name;
    93.         moveID = id;
    94.         moveDesc = desc;
    95.         moveType = type;
    96.         attackType = atype;
    97.         movePower = power;
    98.         bonusStat = bonus;
    99.         moveCharges = charges;
    100.         currentMoveCharges = currentCharges;
    101.         accuracy = accurate;
    102.         statusEffectChance = effectChance;
    103.    
    104.     }
    105.  
    106.     public Move()
    107.     {
    108.  
    109.     }
    110.  
    111.     public Move(Move i)
    112.     {
    113.         this.moveName = i.moveName;
    114.         this.moveID = i.moveID;
    115.         this.moveDesc = i.moveDesc;
    116.         this.moveType = i.moveType;
    117.         this.attackType = i.attackType;
    118.         this.movePower = i.movePower;
    119.         this.bonusStat = i.bonusStat;
    120.         this.moveCharges = i.moveCharges;
    121.         this.currentMoveCharges = i.currentMoveCharges;
    122.         this.accuracy = i.accuracy;
    123.         this.statusEffectChance = i.statusEffectChance;
    124.     }
    125.  
    126. }
    In the inspector it looks like this.


    Now I have another class called Monster that's setup the say way as the MoveDatabase. What im trying to do is add a dropdown of all the moves in the monster database that lets me pick the move I want for a specific monster in the same way a enum would allow me to. I know I can do this by making a enum inside the monster class that has all of the moves, but the problem is, if I add a move to the move database the enum in monster would need to be edited as well. Is there a way around this?

    Example - MonsterMove lets me pick a move from move database
     
    Last edited: Dec 3, 2019
  2. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    92
    Why did you decide against making
    Move
    inherit from
    ScriptableObject
    ? If
    Move
    was a
    ScriptableObject
    , you would not have this problem because you could just reference the moves themselves.

    In your particular situation, I cannot think of another solution than auto-generating an enumeration definition based on your
    MoveDatabase
    and using that one in your
    Monster
    class definition.
     
  3. SourPatch427618

    SourPatch427618

    Joined:
    Mar 5, 2017
    Posts:
    12
    If I made "move" inherit from "scriptableobject" wouldn't I end up needing to make a new object per move? I figured it would be more efficient to just hold all the data inside of a single scriptableobject that I can just loop through to get the data I need. Is this not the right way?
     
  4. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    92
    When it comes to programming, there is rarely one best or right way. Often, one has to choose between different approaches and see what works best for one.
    Yes, you would end up like that, however, you would get huge benefits from that. With
    ScriptableObject
    , you get a very nice solution for referencing non-instantiated objects like data configurations, in difference to Prefabs and other solutions requiring an instanced object.

    You yourself, right now you have one database with moves.That is fine, but now you have the problem of having to reference individual moves to configurate your other database, the monster one. Effectively, you want to reference moves.
    ScriptableObjects
    allow you to do exactly that. They remove, in some instances, the need for a database because you can reference the individual moves themselves instead of looking them up in a database. They take a tad longer to create in the editor, but as you might see already, there is a huge gain from doing it this way.

    If you have data that is independent from instanced objects, for instance the maximum health of a hero, with multiple heroes sharing said maximum health configuration, then
    ScriptableObjects
    are a highly usable solution for that. One configuration for maximum health of a hero, being shared by all heroes or in your case, moves, with certain moves being shared by certain monsters.

    If you happen to play games by Blizzard Entertainment, their game Starcraft 2 uses a similar pattern as
    ScriptableObjects
    in their Galaxy Editor, their official modding tool for the game. It is very powerful once you get the hang out of it.
     
    Last edited: Dec 4, 2019
  5. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,524
    You could maybe hypothetically create a custom inspector that would grab the current list of Moves and display them in a dropdown, but that's not your real problem. Your real problem is: when the user selects one of the Moves, how do you serialize their selection in such a way that you can save it and load it back later?

    You can't save a direct reference to a C# object, because when you quit Unity and then load your project again later, that object won't survive the cycle, it will get destroyed and a replacement will (presumably) get created. You need some way of identifying which replacement you want.

    To do that, you need to give each Move some sort of permanent unique identifier; for example, an ID number. Note that you can't just use its position in your list, because if you add or remove stuff in your list then the position of a given Move might change.

    If you change your Moves into ScriptableObjects like Ardenian suggests, then Unity will automatically create these IDs for you, so that you have a way of saving references to a specific Move. And the saving will happen automagically.

    Now, you don't have to do it Unity's way. You could invent your own system for identifying Moves and linking them up again every time you load. You could give every Move some kind of permanent unique number or name, and your Monsters could save that number or name instead of the Move itself, and then you could have a function for looking up the appropriate Move based on that number or name.

    But if you're already using the Unity inspector to create and edit your Moves, it's probably easier just to make them SerializableObjects. The main reason to roll your own system would be if you wanted to load your Moves from a custom data file at run time; e.g. if you have a spreadsheet of Moves that you edit outside Unity.
     
  6. SourPatch427618

    SourPatch427618

    Joined:
    Mar 5, 2017
    Posts:
    12
    I actually prototyped your suggestion earlier today, and honestly prefered making the moves individual scriptable objects. Few issues I had with this.

    I could not find a way in code to grab all the Moves and store them into a list. I ended up making another Scriptable Object called MoveDatabase that held all the moves in a list that I could loop through. This requires me to drag and drop the move into the database each time i make a new move.

    The other issue I have with this that I really only want the Scriptable Objects data to be a reference. So lets say I have the Move Fireball. I want a monster to have this Move but the properties of this Move may change.

    Move
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. [CreateAssetMenu(fileName = "New Move", menuName = "Create Move/Add Move", order = 5)]
    5.  
    6. [System.Serializable]
    7. public class Move : ScriptableObject
    8. {
    9.     public string moveName;
    10.     public string moveDesc;
    11.     public float movePower;
    12.     public float moveCharges;
    13.     public float currentMoveCharges;
    14. }
    This move has been added to a monster however "currentMoveCharges" changes on this move during gameplay. Those changes then affect the original scriptable object. I dont want this. So I tried to do something like this, and I get the warning
    Code (CSharp):
    1. public void TestingMonsterMoves()
    2. {
    3.  
    4. foreach (Move move in moveDatabase.Moves)
    5.     {
    6.             testMove = new Move(move); //Gives Warning
    7.             testMove = move; //Overwrites orginal scriptable object
    8.             testMove = Object.Instantiate(move); //Very slow
    9.             testMove.movePower = 50;
    10.     }
    11. }
     
    Last edited: Dec 5, 2019
  7. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,524
    I suggest that "currentMoveCharges" would properly be considered an attribute of the monster, NOT the move itself.

    If a monster has a bunch of moves and you need to maintain some monster-specific data on each one, you could consider writing a second class along the lines of
    Code (CSharp):
    1. [System.Serializable]
    2. public class MonsterMove
    3. {
    4.     public Move moveDefinition;   // A reference to the underlying Move
    5.     public float currentCharges;  // How many charges THIS MONSTER currently has of this move
    6.     public float cooldown;        // etc.
    7. }
    Marking this class as Serializable means that your Monster class can have a field like
    public MonsterMove myMove;
    and you'll be able to edit the individual sub-variables from the inspector.
     
    Ardenian likes this.
  8. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    1,524
    I haven't tried this, but it looks like ScriptableObjects have an Awake method, so you could probably write your Move class so that in Awake it automatically adds itself to some globally-visible collection somewhere. (However, note that this means the order they get added might be inconsistent, so if order matters for anything you should sort them at some point.)
     
  9. SourPatch427618

    SourPatch427618

    Joined:
    Mar 5, 2017
    Posts:
    12
    I think I got this working the way I want now. This will automatically add the each individual Move to the scriptable object MoveDatabase without the need to drag and drop. From this point the order they are added wont matter because this can be easily sorted if needed.

    Code (CSharp):
    1.  
    2.     private void OnValidate()
    3.     {
    4.         //Clear the list on change
    5.         Moves.Clear();
    6.         //Store the global unique identifiers of type Move into a array
    7.         string[] assetGUID = AssetDatabase.FindAssets("t:Move");
    8.         //Loop through the GUIDs
    9.         foreach (string asset in assetGUID)
    10.         {
    11.             //Get the path to the asset based on the GUID
    12.             string movePath = AssetDatabase.GUIDToAssetPath(asset);
    13.             //Use the asset path to get the Move
    14.             Move moveToAdd = AssetDatabase.LoadAssetAtPath<Move>(movePath);
    15.             //Add move to database
    16.             Moves.Add(moveToAdd);
    17.         }
    18.     }
     
  10. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    92
    This is one way to do it, yes. In my project, I also have several sets holding a bunch of
    ScriptableObjects
    , being one themselves. However, in my case, these sets are also shared by other objects. If your only problem is to have a container containing multiple
    ScriptableObject
    , then introducing a new
    ScriptableObject
    to hold them, for this only purpose, is a little overkill, in my opinion.

    Instead of drag'n'dropping them, I recommend using that little button to the right of a collection element and select it from the selection window. The usability of this solution depends on your total amount of moves, of course, and you still have to go one by one move. Nonetheless, you don't have to introduce a new object type as a result. However, if there is more that you want to do with your moves, like assigning unique information to a bunch of them, then your solution with introducing a
    ScriptableObject
    holding multiple moves is more than viable.

    Using
    ScriptableObject
    , this is not possible, unfortunately. What you would need to do is introducing a so-called wrapper who contains both the move and the properties to the move being changed at runtime through various effects.
    ScriptableObject
    are only for static data, one could say, being shared by ideally more than one object.
    Think about a hero in a game. He has a maximum health and a current health value. The maximum health value, or at least its base value, never changes. The current health value changes all the time. Your maximum health value, the base value for it, would be stored in a
    ScriptableObject
    , while a
    MonoBehavior
    component on your hero object keeps track of the current health, while referencing the maximum health from the
    ScriptableObject
    . This could look like this:

    Code (CSharp):
    1.     public class Stat<T> : ScriptableObject
    2.     {
    3.         /*...*/
    4.     }
    5.  
    6.     public class Vital: Stat<Vital> {
    7.         public float Maximum;
    8.     }
    9.  
    10.     public class HeroLife : MonoBehavior
    11.     {
    12.         public Vital health;
    13.         public float current;
    14.     }
    With a little effort, this can become very powerful, as you can imagine, if you introduce a general
    Vital
    type, with the component wrapping it into an additional changing current value. Energy, Stamina, Damage, Regeneration, they all work roughly the same and can be easily expanded on new stats this way, only requiring you to work on one end, the
    ScriptableObject
    because the component handles the wrapping of current values themselves.
    ScriptableObject
    are not really something to be created at runtime. Remember my rambling about wrappers to the previous quote? You could set it up like this, similar to the solution that Antistone already posted earlier:

    Code (CSharp):
    1.  
    2.     public class Move : ScriptableObject
    3.     {
    4.         public string moveName;
    5.         public string moveDesc;
    6.         public float movePower;
    7.         public float moveCharges;
    8.     }
    9.  
    10.     [System.Serializable]
    11.     public class ActiveMove
    12.     {
    13.         public Move move;
    14.         public float currentCharges;
    15.         public float currentCooldown;
    16.     }
    17.  
    18.     public class Skills : MonoBehaviour
    19.     {
    20.         public List<ActiveMove> moves;
    21.     }
    Don't be afraid to introduce more types and decouple your code. It is neat to have everything in one place, but it will often stop you from moving onto new things.
     
    Last edited: Dec 5, 2019