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. Dismiss Notice

Understanding ScriptableObjects - Some questions.

Discussion in 'Scripting' started by dreamtocode77, Apr 12, 2020.

  1. dreamtocode77

    dreamtocode77

    Joined:
    Nov 10, 2019
    Posts:
    46
    First I want to make sure I am doing this effectively. Lets assume that I was going to create 100 enemies. Here is my setup:

    1. Create the base SO

    Code (CSharp):
    1. [CreateAssetMenu(fileName = "Hero", menuName = "Type")]
    2. public class Tester : ScriptableObject
    3. {
    4.     public string Name;
    5.     public int Level;
    6. }

    2. The I would begin to create my enemies SO through the create asset menu and give them fill in those stats. I create one.


    3. I need another script to do something with them right?

    Code (CSharp):
    1. public class BaseClass : MonoBehaviour
    2. {
    3.     public Tester tester;
    4.  
    5.     [Header("Stats")]
    6.     public string Name;
    7.     public int Level;
    8.    
    9.  
    10.     // Start is called before the first frame update
    11.     void Start()
    12.     {
    13.         Name = tester.Name;
    14.         Level = tester.Level;
    15.         sprite = tester.sprite;
    16.     }
    17. }
    4. Create a Game object and attach the BaseClass script to it, drag the one of the enemies SO's into the inspector than save that as the prefab correct?

    Assuming I did that correctly:

    1. What is the difference between this and just making a single BaseStats script that contains all the data needed to explain characters/enemies, attaching it to all your characters/enemies and just tailoring it to each specific one? This just feels like more steps? What is the major benefit?

    2. Is it possible to store sprite data(and to extension sprite animation)inside the SO. Attach it to a empty gameobject and then instantiate that sprite on play? Or just have the sprite be the gameobject and attach the script to that?

    3. Would functions like attacking/healing be stored in the SO or the MonoBehaviour part?
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,120
    There are a couple of advantages to using SOs instead of just putting all of the data directly on the prefab object itself.
    • With SOs, it's easier to override behavior using pre-defined templates. Let's say you have a melee enemy who uses certain attacks. I define those attacks in SOs, so that I can easily change which attacks a given enemy will use. I could put this directly on the prefab, but then I'd need to have prefab variants for every possible attack style, which is messier than just having distinct Attack Configs, stores as SOs.
    • If I'm referencing a large asset, like an audio file, I always put those references in SOs. Let's say you spawn 100 enemies that all make the same kinds of noises when they attack/die/jump/etc. If you put the references to the audio files on the prefabs, I believe that causes those audio clips to be instantiated once per prefab. But if those clips are in a SO, and all the prefabs reference the SO, there should only be one instance of the clip. Same should go for sprite data.
    • I have virtually no functions/behavior in my SOs.You can do that, but I generally treat SOs only as data, not as behavior. It's up to some other script to actually do something with the data. The only time I violate this is for convenience methods that do some trivial operation on the data to return something. But those functions won't have side-effects.
     
    april_4_short and dreamtocode77 like this.
  3. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    ScriptableObjects are just basically handy bags of data that are easy to edit in the UnityEditor.

    Since they are full classes they can actually contain additional logic, which can also be useful.

    One example might be a weapon that has a bunch of fields:

    - magic type
    - base damage range

    That's neat and all, but you might want to centralize additional logic such as function that returns a single package of damage from that weapon, and it could optionally take in parameters such as the stats of who is using the weapon. Obviously you can also put that sort of logic elsewhere; it's just an architecture question.

    Your example above copies the contents of the Tester instance out to local variables. This is important because when you're in the editor, modifying that instance of the SO will actually change it on disk, and this can lead to lots of confusion. This is just one of the things that lets you write really powerful editor scripts in Unity.

    Another approach you can do is to actually Instantiate<Tester>() the SO in your Start function.

    This will clone/copy the SO (not writing it to disk though) and then you can modify it to your hearts content, and it will still be inside that SO. With an advanced editor extension such as Odin Inspector or Advanced Inspector, you can actually edit those transient SO instances while your game is running, such as by pressing PAUSE and twiddling the numbers, which is HUGELY helpful for debugging.

    And of course Monobehaviors are basically instances of components that exist in the scene and interoperate closely with the GameObjects in your scene. Plugging SOs into MBs is exactly the right approach here.

    In fact, you can even plug SO instances into other SO instances! For example:

    PlayerDefinition : ScriptableObject
    --> string Description
    --> WeaponDefinition : ScriptableObject
    --> ArmorDefinition : ScriptableObject

    etc.

    Mess with it, get your feet wet, get a feel for how amazingly powerful this stuff is, how it can multiply and magnify your ability to create game content and keep it all organized.
     
  4. dreamtocode77

    dreamtocode77

    Joined:
    Nov 10, 2019
    Posts:
    46
    1. Did I have the idea about how to set SO's right? BaseSO -> create enemy SO's -> MonoBehavioyr script to use SO's -> attach to gameobject -> prefab?
    2.. I was going to create every skill in the game as a SO then have every enemy SO have a array of skill SO's stored in them(they would have like 3-6 skills). Is that a good idea?
    3.. So I SHOULD store things like sprites/animation/sounds in the SO?
    4.. Ok. Understood.
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    1. yes, totally valid way to go, but how complex and how deep your base -> actual -> etc. gets should be tailored to the current complexity of your game. Don't overdo it early on!!!!

    2. again, yes, one SO instance per skill is fine... don't do more than THREE (3) to start with because I guarantee you will fundamentally change the "shape" of them as you go forward. Redoing three of them is easier than redoing 300 of them.

    3. you certainly MAY put anything into a SO instance you like, and the Unity editor will allow you to edit all the things that it currently lets you edit on a Monobehavior instance. All the same serialization / editing rules apply. You can easily make custom editors for your SOs as well, which is extremely commonplace, such as making a weapon editor.
     
    dreamtocode77 likes this.
  6. dreamtocode77

    dreamtocode77

    Joined:
    Nov 10, 2019
    Posts:
    46
    Lets talk about this because I don't quite understand it.

    Your example above copies the contents of the Tester instance out to local variables. This is important because when you're in the editor, modifying that instance of the SO will actually change it on disk, and this can lead to lots of confusion. This is just one of the things that lets you write really powerful editor scripts in Unity.

    Another approach you can do is to actually Instantiate<Tester>() the SO in your Start function.


    The original SO will contain all the data about the enemy and that info will be the same for every enemy across the board(No enemy will get a special stat or anything). It only serves as a base for my enemy SO's to be created from it. In the BaseClass script I made the reference to it public and in the inspector I don't drag the Original SO but one of the many enemy SO's.. so I assumed that the SO(Tester) would never be touched. Any reason I need a clone of the original SO? Isn't the SO's created from create asset menu already a clone essentially?

    Another questions: If I store the sprite that enemy will be using inside the SO...put me on the right track instantiating it.

    EDIT: If it store the sprite in a game object this works. Do we generally child a sprite to a empty gameobject?

    Code (CSharp):
    1. public class BaseClass : MonoBehaviour
    2. {
    3.     public Tester tester;
    4.  
    5.     [Header("Stats")]
    6.     public string Name;
    7.     public int Level;
    8.     public GameObject sprite;
    9.  
    10.     // Start is called before the first frame update
    11.     void Start()
    12.     {
    13.         Name = tester.Name;
    14.         Level = tester.Level;
    15.         sprite = tester.sprite;
    16.         swawn();
    17.     }
    18.  
    19.       public void swawn()
    20.     {
    21.         Instantiate(sprite, new Vector3(0, 0, -10), Quaternion.identity);
    22.     }
     
    Last edited: Apr 12, 2020
  7. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,120
    Regarding the sprite, it's more typical to keep the sprite as a Sprite object, and assign it to the appropriate field of an Image component. I probably wouldn't be instantiating new copies of a sprite. I don't think there's any need to do that. The sprite is just an object which other objects can reference.

    Also, I rarely instantiate new SOs at runtime. The only time I do that is if I have a one-off case where an object has completely different settings.

    Here's an example of me using some SOs on a robot enemy:

    Code (CSharp):
    1. public abstract class RoboticSoldierControllerBase : GraviaControllerBase
    2. {
    3.     public RoboticSoldierSettings RoboticSoldierSettings;
    4.     public RoboticSoldierCombatSettings RoboticSoldierCombatSettings;
    5.     public RoboticSoldierAudioSettings RoboticSoldierAudioSettings;  
    6.     public Animator Animator;
    7.     public Collider MainCollider;
    8.     public Collider BodyCollider;
    9.     public Transform LookOrigin;
    10.     public DynamicBodyPart[] StandardBodyParts;
    11.     public DynamicBodyPart[] ArmorBodyParts;
    12.     public NoiseOnCollisionSettings BodyPartNoiseOnCollisionSettings;
    13.    
    14.     // Snip...
    15. }
    Those first three public fields are all references to Scriptable Objects. Here's what my prefab looks like:

    upload_2020-4-12_17-49-27.png

    When I instantiate a robot, I just instantiate the prefab, no need to do anything specific with the SOs. In this screenshot you'll see that on this prefab, I've overridden the Combat Settings SO to use a different config than my standard robot.

    So, for the most part, I don't instantiate new SOs at runtime, but your game could be different. Even still, unless you want some particularly random behavior, I'd think it would be easier to just create lots of SO instances within your project, and assign them to the newly instantiated prefab to change its behavior.
     
  8. dreamtocode77

    dreamtocode77

    Joined:
    Nov 10, 2019
    Posts:
    46
    @dgoyette - I was going to do the image thing but the image has to part of the canvas(?) and the only thing I want in my canvas are my UI elements. That is why I have always taken my sprites and childed them to an empty gameobject or just used them directly (drag in hierarchy).

    So what does the actual SO script for the Combat Settings SO look like if its not to much trouble.
     
  9. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,323
    The difference is that it is easy to create multiple enemies with the same stat block. Without external object, you'd need to copy-paste by hand and hunt down specific objects.

    Also statblock can be edited inside inspector, if you simply click on the asset.

    Of course, if you don't need it, you don't have to implement it.

    You can reference all serializable types from scriptable objects, that includes references to sprites.
    https://docs.unity3d.com/Manual/script-Serialization.html

    They would be stored in monobehavior.

    ScriptableObjects are useful when:

    1. You need configurable data.
    2. You want that data editable from unity (no editing json, or, cthulhu forbid, xml by hand).
    3. The data should not be embedded inside of any object, and it should be possible to have multiple objects refer to the same data block.

    The point of scriptable objects is that they can be used as data blocks, which can be referenced form objects, and can have their custom editors.

    Actual functions that are related to enemy behaviors and the like should likely go to the MonoBehavior.

    Basically, functions should be implemented where it makes most sense.
    For scriptable object it is reasonable to have function like "calculateDerivedStat()" or "calculateWeaponDamage()", or "calculateWalkingSpeed()". Those would return data based datablock's state.

    "dealDamage()' feels like it should be responsbility of a component on GameObject, and not ScriptableObject's job.
     
    april_4_short likes this.
  10. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,120
    Here are the SOs. Most of the settings are in the HumanoidAIAttackSettings objects. This AI cycles through several attacks in order, from weaker to stronger, so each entry in "MeleeAttackSettings" represents a single attack. This includes the sounds that get played, the attack's range, and how much damage it does, among other things.

    Code (CSharp):
    1.  
    2.     [CreateAssetMenu(menuName = "Game Settings/Characters/Robotic Soldier Combat")]
    3.     public class RoboticSoldierCombatSettings : GameSettingsBase
    4.     {
    5.         [DefaultValue(2500f), Range(0, 10000), Tooltip("The smallest collision force in Newtons that will cause damage to the player.")]
    6.         public float HarmfulCollisionForceInNewtons;
    7.  
    8.         [DefaultValue(2f), Tooltip("Collision force is scaled using the Log function. This is the base used to scale damage.")]
    9.         public float CollisionDamageScalingFactor;
    10.  
    11.         [DefaultValue(1f), Tooltip("After scaling the collision force, we apply this many points of damage per scaled amount.")]
    12.         public float CollisionDamagePerScaledNewton;
    13.  
    14.         public HumanoidAIAttackSettings[] MeleeAttackSettings;
    15.         public HumanoidAIAttackSettings FragileBreakAttackSettings;
    16.     }
    17.  
    18.  
    19.     [Serializable]
    20.     public class HumanoidAIAttackSettings
    21.     {
    22.         [Tooltip("Played when an attack is initiated")]
    23.         public AudioClipDefinition[] AttackStartedSoundSettings;
    24.  
    25.         [Tooltip("Played when an attack strikes its target")]
    26.         public AudioClipDefinition[] AttackLandedSoundSettings;
    27.  
    28.         [Tooltip("Played when an attack misses")]
    29.         public AudioClipDefinition[] AttackMissedSoundSettings;
    30.  
    31.         [DefaultValue(2f), Tooltip("How close does the agent need to get to initially be considered in range for the attack?")]
    32.         public float MinEnterRange;
    33.  
    34.         [DefaultValue(3f), Tooltip("After entering melee range, how far away can they get from the player to no longer be in range?")]
    35.         public float MaxExitRange;
    36.  
    37.         [DefaultValue(0f), Tooltip("How much damage does the attack do?")]
    38.         public float Damage;
    39.  
    40.         [DefaultValue(1f), Tooltip("How long after the attack do we have to wait?")]
    41.         public float Delay;
    42.  
    43.         [DefaultValue(2f), Tooltip("How long does the attack stun the player?")]
    44.         public float StunDuration;
    45.  
    46.         [DefaultValue(1000f), Tooltip("How much force does the attack apply to the target?")]
    47.         public float ImpactForce;
    48.  
    49.         [DefaultValue(50f), Tooltip("Moves the Attacker forward by a certain amount as the attack is initiated.")]
    50.         public float LungeForce;
    51.  
    52.         [Tooltip("Used to indicate which animator bone connects on the attack, and where the audio should play for the attack.")]
    53.         public HumanBodyBones ImpactBone;
    54.  
    55.         [DefaultValue(0f), Tooltip("Some moved might have a cooldown which can only be executed after a certain length of time has passed.")]
    56.         public float Cooldown;
    57.  
    58.         [DefaultValue(false), Tooltip("If true, a Bomber soldier will self-destruct after this step.")]
    59.         public bool PerformBomberDetonationAfterAttack;
    60.     }
    Different robots have different SO for the attack settings to make them relatively stronger or weaker. Here's a screenshot of one of the SOs:

    upload_2020-4-12_19-23-34.png
     
    april_4_short likes this.
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,756
    @dgoyette you are very wise with your tooltips being so explanatory! BRAVO!! It sounds like working in your codebase would be very enjoyable.
     
    april_4_short and PraetorBlue like this.
  12. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,120
    It's because I so often forget why the heck I added something. :)
     
    Kurt-Dekker likes this.