Search Unity

Code Modularity & Scriptable Objects

Discussion in 'Scripting' started by SonicShade, Nov 22, 2019.

  1. SonicShade

    SonicShade

    Joined:
    Dec 29, 2015
    Posts:
    8
    Hi everyone,

    So, I've seen these two talks about scriptable objects mentioned in this article:
    https://blogs.unity3d.com/2017/11/20/making-cool-stuff-with-scriptableobjects/

    And now I'm completely stuck!

    I'm working on an inventory system and I would like to modularize what I have so far. I'm using scriptable objects already, but I think I could do much better. At the moment they are structured like this:

    ItemBaseData : ScriptableObject (base blueprint for inherited ItemData)
    This has lot of variables that EVERY item in my game should have, like:

    Code (CSharp):
    1.  
    2. [SerializeField] private string itemName;  
    3. [SerializeField] private string itemDescription;
    4. [SerializeField] private float itemWeight;
    5. [SerializeField] private int itemValue;
    6. [SerializeField] private Sprite itemIcon;
    7.  
    Etc. etc. Pretty self explanatory I think.


    Now this branches off into different categories like weapon, armor, consumables etc.

    ItemConsumableData : ItemBaseData (potions, food, beverages etc.)

    Naturally this has everything the ItemBaseData has. But now this should have two more variables like:

    Code (CSharp):
    1.  
    2. [SerializeField] private int itemSatisfyHunger;
    3. [SerializeField] private int itemSatisfyThirst;
    4.  
    5. [SerializeField] private int itemHeal;
    6. [SerializeField] private int itemDamage;
    And it has it's own UseItem() function which checks if itemHeal > 0 and if it is, it uses Heal() to heal who ever used the item. The same goes for damage (rotten food, or poisonous berries or what ever).

    ItemWeaponData has weaponDamage, ItemArmorData has armorDefense as extensions to the ItemBaseData.

    The ItemBaseData class has public getters for its properties. The problem with this is: How to access the new values of the inherited Data classes? For this I use functions in the ItemBaseData class. Not elegant in my opinion. So since I do not want anybody to change the values of the itemDatas they are all private. And once a GameObject like an inventory slot wants to have an ItemBaseData as input data we can't just say: display the satisfyHunger value on the UI. Because we can't access it from ItemBaseData.

    This means, that I need a function like this to get the default of 0 if the item is a weapon, for example:
    Code (CSharp):
    1.  
    2. public virtual float GetSatisfyHunger()
    3. {
    4.      return 0;
    5. }
    6.  
    I override this class in ItemConsumableData:
    Code (CSharp):
    1.  
    2. public virtual float GetSatisfyHunger()
    3. {
    4.      return itemSatisfyHunger;
    5. }
    6.  
    This is only one example of things that feel sooo wrong!
    What would be the right way to manage this?



    My dream scenario would be this:

    I have an Item class that can have a list or dictionary of properties. These properties are plug & play. And the display UI of the inventory just takes what ever it gets and displays it. You can have dreams ;-)

    How to achieve this?
    In order to achieve this I would have to use a extremely granular workflow like in the Game Architecture talk. I would have a SO blueprint for FloatValue and use it like this in the inspector: Create new ItemProperty > Name = BreadSatisfyHunger > Value = 10f;
    I would have to create a SO for EVERY item, for EVERY property this item has. I would end up with maybe 10 properties for every item. This feels wrong again. Then add it to the dictionary of properties. And so on.

    What ever I can think of, when I reach a point where I want to start running and implement it, it feels wrong again.



    How would you guys go about this?? Where am I thinking wrong?
     
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Instead of making each unique item type check anything, just declare something like an abstract .Use() method, which will be overriden in the child classes.

    Code (CSharp):
    1. public class Item : ScriptableObject {
    2.    ....
    3.    public abstract void Use(GameObject user, GameObject target);
    4. }
    5.  
    6. public class Medpack : Item {
    7.    [SerializeField]
    8.    private float _healValue = 10f;
    9.  
    10.    public override void Use(GameObject user, GameObject target) {
    11.       // Do healing etc
    12.       Health health = user.GetComponent<Health>(); // or target, depending on the game design
    13.       if (health != null) health.Add(_healValue);
    14.    }
    15. }
    Alternatively, use interfaces for parameters to get rid of gameobject -> component fetching.

    As to an actual display - make an abstract struct that contains data you need to display.

    E.g.
    Code (CSharp):
    1. public struct ItemCharacteristic {
    2.     public string Name;
    3.     public string Value;
    4. }
    Add another abstract method to fetch all your ItemCharacteristic, which can be just a List<ItemCharacteristic>.
    Then, display all of them one by one in the UI of your choice.

    If you need something to be done by item, feed the subjects into the .Use(...) method. And execute that logic inside that specific child class.


    Basically it all runs down to declaring abstract methods in the base class, and overring in the child class. And creating proper archetypes of those items. E.g. food, weapon, gear etc.
     
  3. SonicShade

    SonicShade

    Joined:
    Dec 29, 2015
    Posts:
    8
    Hey @xVergilx,

    thanks for your response!

    I do have one base class SO for items that other branched off classes can inherit from.

    Code (CSharp):
    1. ItemBase
    2.     - Consumable
    3.     - Equipable
    4.         - Weapon
    5.         - Armor
    6.  
    7.         etc.
    ItemBase has a
    Code (CSharp):
    1. public virtual void UseItem() {}
    function so the consumable class can implement its own behaviour like


    Code (CSharp):
    1. public class ItemConsumableData : ItemBaseData
    2. {
    3.     [SerializeField] private int itemHeal;
    4.     public int ItemHeal { get{return itemHeal;}}
    5.  
    6.     public override void UseItem()
    7.     {
    8.         PlayerHealth playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerHealth>();
    9.  
    10.         if (itemHeal > 0)
    11.         {  
    12.             playerHealth.Heal(itemHeal);
    13.         }
    14.     }

    I'm going to change that so it takes the User as a parameter instead of hard wiring it to the player, like you suggested. Then it does not matter who uses it.

    The problem with this is:
    If some other code wants to have an ItemBase class as input to do something with, because it's the base class, and you give a Consumable instead, you can't use the ItemHeal property for example. If I wanted to give the Consumable the possibility to satisfyHunger this would have to go into the use function too, right? Like the example with healing the user.

    The user would have to have a hunger or thirst component attached to it, so the item can grab it and call that instead, right? Like the healing where the user has the health component which contains the Heal(int amount) function.

    I would love to have an approach of coding the components so they can be tested individually. There are always problems when hard referencing stuff from a scene. Like in my current implementation of the Consumable, where it specifically searches for the player.

    Is there a book or good website out there for best practice dealing with encapsulation, modularity etc?
     
  4. SonicShade

    SonicShade

    Joined:
    Dec 29, 2015
    Posts:
    8
    The new Use() looks like this now:

    Code (CSharp):
    1. public override void UseItem(GameObject user, GameObject target)
    2. {
    3.     if (itemHeal > 0 && target.GetComponent<CharacterHealth>())
    4.     {
    5.         target.GetComponent<CharacterHealth>().Heal(itemHeal);
    6.     }
    7.  
    8.     if (itemDamage > 0 && target.GetComponent<CharacterHealth>())
    9.     {
    10.         target.GetComponent<CharacterHealth>().TakeDamage(itemDamage);
    11.     }
    12. }
     
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,322
    If you have base class type variable but need to access the properties of an extending type, you can do an explicit cast from the base class type to the extending class type. So you don't need to implement all the possible properties of extending classes in ItemBaseData.

    Code (CSharp):
    1. public class HealingPotion : Item
    2. {
    3.     public HealingPotionData Data
    4.     {
    5.         get
    6.         {
    7.             // explicitly cast from ItemBaseData to HealingPotionData
    8.             return (HealingPotionData)data;
    9.         }
    10.     }
    11.     public override void UseItem(GameObject user, GameObject target)
    12.     {
    13.         var healAmount = Data.healAmount;
    14.         if(target.GetComponent<CharacterHealth>())
    15.         {
    16.             target.GetComponent<CharacterHealth>().Heal(healAmount);
    17.         }
    18.     }
    19. }
    Generally speaking I think that it sounds like the best approach that the items just contain general purpose methods for using them, and the entities using them would not necessarily know or care about the details of this. So the item user doesn't need to know what the itemSatisfyHunger of an item is, it just uses the item, and the item handles increasing the stats it wants on the target. This kind of approach would make it much easier to add lots of items at least.

    If this doesn't seem to be enough in some situations, you could also considering adding more verbs, different ways to use an item. Use, drink, throw and whatever you need. Better to have five different methods than one method and 100 properties.

    You could also use interfaces to add a little bit more flexibility, like not requiring all consumables to inherit from the same base class and supporting multiple inheritance (item classes can implement multiple interfaces).

    Code (CSharp):
    1.  
    2. public interface IItem
    3. {
    4.     bool CanUse(IItemUser user, ITargetable target);
    5.     bool CanThrow(IItemThrower user, ITargetable target);
    6.     bool CanConsume(IItemConsumer user);
    7.  
    8.     void Use(IItemUser user, ITargetable target);
    9.     void Throw(IItemThrower user, ITargetable target);
    10.     void Consume(IItemConsumer user);
    11. }
    12.  
    13. public interface IConsumable : IItem
    14. {
    15.     int ItemSatisfyHunger { get; }
    16. }
    17.  
    18. public interface IThrowable : IItem
    19. {
    20.     int ThrowDistance { get; }
    21. }
    22.  
    23. public class FirePotionData : ItemBaseData, IConsumable, IThrowable // can be both consumed AND thrown
    24. {
    25.     [SerializeField]private int itemSatisfyHunger = 0;
    26.     [SerializeField]private int throwDistance = 5;
    27.  
    28.     public int ItemSatisfyHunger
    29.     {
    30.         get
    31.         {
    32.             return itemSatisfyHunger;
    33.         }
    34.     }
    35.  
    36.     public int ThrowDistance
    37.     {
    38.         get
    39.         {
    40.             return throwDistance;
    41.         }
    42.     }
    43.  
    44.     public override bool CanThrow(IItemThrower user, ITargetable target)
    45.     {
    46.         return Vector3.Distance(user.Position, target.Position) <= throwDistance;
    47.     }
    48.  
    49.     public override bool CanConsume(IItemConsumer user)
    50.     {
    51.         return true;
    52.     }
    53.  
    54.     public override Use(IActor user, ITargetable target)
    55.     {
    56.         target.Effectable.SetStatusEffect(StatusEffect.Inflammable, 120f);
    57.     }
    58.  
    59.     public override Throw(IActor user, ITargetable target)
    60.     {
    61.         target.Effectable.SetStatusEffect(StatusEffect.Inflammable, 120f);
    62.         target.Damageable.Damage(1f, DamageType.Bludgeoning);
    63.     }
    64.  
    65.     public override Consume(IActor user)
    66.     {
    67.         user.Effectable.SetStatusEffect(StatusEffect.GasPains, 300f);
    68.         user.Effectable.SetStatusEffect(StatusEffect.Diarrhea, 60f, 20f);
    69.     }
    70. }
    71.