Showing posts with label Scriptable Objects. Show all posts
Showing posts with label Scriptable Objects. Show all posts

Sunday, September 6, 2020

A Look At Scriptable Objects Part 2

In my last post, I introduced my recent journey into Unity's Scriptable Objects.  Since then I have been working to convert the tree objects and the tile engine to use them in place of Singleton classes that store tree and tile data in static lists.  I have now completed parts of this refactoring and am quite impressed with the new workflow, the added benefits, and the surprising performance boost I have seen moving to scriptable objects.  From here onward, I will abbreviate the term Scriptable Object to simply (SO) for brevity.

The Refactoring Process

I decided to refactor my tree game objects first since the tree code was less work than the tile engine. I started by creating an abstract base class which derived from (SO).  All my other (SO) classes derive from my base class.  I used this base class to store properties and fields that were common to all the (SO) classes I would eventually create.  The class needed to be abstract because I had no intention of instantiating this base (SO) class by itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    public abstract class STBaseObject : ScriptableObject
    {
        public string Id { get; set; }
        public string Name;
        public string FriendlyName;
        public STBaseType BaseType;        
        public GameObject DefaultPrefab;
        public GameObject InventoryPrefab;
        public float AdjustedXPos;
        public float AdjustedYPos;
        public int HighestMapLevel;
        public int LowestMapLevel;
        [TextArea(1, 20)]
        public string Description;        
    }    

The next (SO) class was my STTreeObject class.  This class would hold fields and properties that were unique to my trees.  Up to this point, all my tree data was stored in a public class that held the properties for my trees.  This class was accessed by another class which existed to simply to instantiate the list of trees at runtime.  That class was instantiated once as a public property on one of my Singleton classes, which also was only instantiated once.  What I am describing is a Singleton data storage mechanism.  Getting back to the (SO) alternative, have a look at the top portion of the new class showing the CreateAssetMenu attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[CreateAssetMenu(fileName = "TreeObject", menuName = "SurviveAndThrive/Flora/TreeObject", order = 1)]
    public class STTreeObject : STBaseObject
    {
        public int AssetId;
        public GameObject Sapling;
        public GameObject Ripe;
        public string FellingSound;
        public float FellSoundTimer;
        public STFloraAssetType AssetType;
        public STFloraAssetDistributionType DistributionMethod;
        public STFloraAssetAgeType MinimumAge;
        public STFloraAssetAgeType MaximumAge;
        public STFloraAssetAgeType MaturityAge;
        public STFloraAssetGrowthRateType GrowthRate;
        public STFloraAssetGrowthRateType FlowerRate;
        public STFloraAssetGrowthRateType SeedRate;

The class attribute is important because it allowed me to create a (SO) file in the Unity Editor by creating a menu item for it under Unity's Assets => Create menu.  Notice the fileName is the name of the (SO).  The menuName is the location within the menu structure where the (SO) will appear. 

Scriptable Object Menu

At first, I thought my tree class and (SO) tree class would be basically the same.  However, once I started refactoring, I realized some of the string values I was using to locate my tree objects were no longer needed because I could directly reference them as a GameObject on my (SO) class.  Here is what a new TreeObject looks like from the Inspector.
A New TreeObject

The New Workflow

There is much to be appreciated in the screenshot above.  For one, it is a lot easier to keep track of my tree data using the Project and Inspector tabs in Unity as opposed to scrolling down 1600+ lines of code in the class I was using to store this data before.  In a glance, I can see how many trees I have and I can compare the property settings of each one to the others.  Plus, by selecting a tree (SO) and locking the inspector, I can navigate to my prefabs and drag-and-drop them onto the (SO).  This means I no longer have to load those prefabs from the disk each time I need to instantiate a new tree in the game.  Yes, it is tedious to transfer all the information from the class to the (SO) files, but it is no less work than using the classes.  

The Added Benefits

This brings me to the benefits I have noticed using (SO) files instead of static classes.  Like I mentioned earlier, I was able to switch from storing and using strings in the tree class which held paths and filenames to using hard references in the (SO) file for things like the DefaultPrefab, InventoryPrefab, and Sapling and Ripe variants.  Another amazing, and I do mean AMAZING benefit over static class storage is the fact that all of the tree data can be altered at runtime.  For example, if I need to increase the number of logs that are produced from chopping down a particular type of tree, all I have to do is change the value in the inspector.  The next time I chop down that type of tree, it will produce the altered count of logs automatically.  This is perhaps the best overall benefit I have seen to using (SO) over other methods of storage of static data.

The Performance Boost

Once I had converted to using (SO) for the trees, I proceeded to build out the tile engine to use (SO) as well.  There is a nice method in my MapManager class which creates the preview map in the game.  I chose that as the first step to refactoring the tile engine.  What follows are comparisons between the pre-scriptable-object version of the game and the current using the new (SO) system.  This test involved loading an existing map template and rendering the preview of it.  The map is 1 MILLION tiles, being 1000 x 1000 tiles in size, rendered on a single Unity tilemap.  Notice in the Console the Start and Stop times.  These times captured how long it took to generate the preview map using the old and new systems.

Pre-SO-Test
35 Seconds

I didn't think 35 seconds was all that bad considering the map engine had to generate a million value noise map, distribute the tile types based on the user's chosen weight for each type, and then loop through to actually create the tilemap tiles using a custom rule-tile method.  But I was not really prepared for the results of the (SO) run.

SO Test
23 Seconds

The same template using (SO) files took only 23 seconds to create and render.  Doing the math, the pre-SO test processed 28,571 tiles per second.  The SO test processed 43,478 tiles per second.  That is a whopping 35% increase in performance! Now to be fair, there is a critical optimization that I used for the (SO) refactoring.  With the old classes, I had to build out a file path and filename to the required tile and then instantiate it with a call to Resources.Load<Tile>().  This was necessary simply because of the huge amount of tiles to manage.  But with the (SO) solution, I chose instead to reference all 15 tiles of each terrain type in their respective (SO) files.  This enabled me to reference all of the tile variations in a clean and organized manner. I believe the majority of the performance boost was due to this optimization as loading from a file system is always more costly than from memory.  The Task Manager didn't show significant differences, but here they are. 

Pre-SO-Test

SO Test

I am very happy that I decided to learn about (SO) and how to use them for static game data storage.  There are many other uses for them that I am sure I will discover.  And, I still have quite a bit of refactoring left to do.  The preview map is just a small part of the map engine.  I will likely follow-up this post with a part three to summarize any future findings.  But one thing is sure.  I will definitely be using Scriptable Objects as much as I can.  That real-time tweaking feature is just fantastic!  

Until next time...

[The Next Day UPDATE!]

I have spent pretty much all day working on refactoring the entire map engine to use (SO).  After some debugging, I ran some tests and I want to update this post to show the dramatic results of moving from Singleton class stores with resource loading to (SO) managed data.  I created a new test map and placed a stopwatch on the initial chunk loading, which loads all 9 chunks around the player in the initial load.  Here are the results.

Pre-SO Chunk Load Times

Post-SO Chunk Load Times

The above comparison shows before the (SO) refactoring, my chunk engine loaded the 9 chunks around the player in 2.91 seconds.  After implementing the (SO) solution, the 9 chunks loaded in only .73 seconds or 730 milliseconds!  I noticed also that the (SO) solution was extremely consistent across almost all the chunks after the initial chunk loaded.  Whereas the pre-SO load times are wildly different, ranging from 870 milliseconds down to 15 milliseconds.  Needless to say, I am incredibly impressed with the results of this work.  The (SO) path has been very rewarding thus far.  I will definitely be looking for every opportunity to use Scriptable Objects in as much of my game as possible.  I hope this has been as interesting to you as it has been rewarding for me.  

Until next time...

Saturday, August 29, 2020

A Look At Scriptable Objects Part 1

The Unity Game Object

Recently, I have been taking a look at Scriptable Objects, a relatively new topic in my Unity learning experience. I wanted to understand them to determine if I should be using them in my game or not.  If you are not familiar with Unity, the engine is built on the concept of the UnityEngine.GameObject, the base class for every entity in a game scene.  By themselves, you can think of them as empty containers.  They can be filled with Components which can be dragged and dropped onto or otherwise attached to game objects to add specific features and functionality, such as animation, sound, physics, and everything else a game object may need.  The most basic game object comes with a single transform component which is responsible for holding the position, rotation, and scale information of the game object in the scene.  Without additional components, the game object is pretty useless except as a sorting item in the scenes object hierarchy.  In fact, it is common to use empty game objects for the sole purpose of organizing all the objects in the scene into parent-child relationships which make managing them easier.  But if they are to be used in the game, they require more components to make them visible and integral until they gain the form and function to become what they are meant to be, whether it be a player, a piece of terrain, an enemy, a building, or a weapon.  When a developer needs to add functionality to a game object that isn't provided in Unity's base component selection list, or if the developer needs to add custom coding to a game object, a C# script can be created and attached as a component.  While scripts can hold any fields, properties, methods, or events the developer decides to code into them, they all have one requirement if they are to serve as a component of a game object.  They must derive from Unity's UnityEngine.MonoBehaviour base class.

Scripts attached to game objects operate within Unity's game cycle, which is one big continuous game loop that runs at the speed of the game's frame-rate.  By operate, I mean each script deriving from MonoBehaviour has base functionality to access events that fire within the game loop.  The most familiar of these are Awake(), Start(), and Update().  The Unity Manual provides a very nice chart which shows the order of execution in the game life cycle.  It's worth studying.

Static Verses State Data

Every game needs to store defining information about each game object.  A sword might have a strength property, a length, a hand requirement (single or double-handed) and a minimal damage value.  A shield might have a block or glance rate and a size value compared to other shields.  A piece of food might provide a specific nutritional or healing effect on the player.  This kind of information that defines and differentiates one game object from another is called Static Game Data.  It is different from information like where the sword is stored or how much damage the sword has or who owns the sword.  Any information that is subject to change from its original creation point in the game is known as State Data.  State data is typically saved in a file when the player saves the game so that it can be reloaded with the same state values when game play continues.  But what about static data, that information that defines game objects and never changes?  Where does that get stored?

Storage Methods For Static Data

Game developers utilize several methods for storing static game data.  One method is to simply embed the game object with every property value that defines it.  For example, lets say you have an NPC enemy in your game.  The script that is attached to your game object might have the following properties:

private int enemyId;

private float maxHealth;

public float health;

public float attackValue;

public float attackBonus;

public float defenseValue;

public float defenseBonus;

Let's say health, attackBonus and defenseBonus are state data that gets saved and loaded in the game's save file for each enemy NPC.  Everything else is static game data.  Having the other static properties on the game object's script is not necessarily wrong.  But is it the best approach?  What if there were 10 enemies, or 100, or 10,000 with each one hauling around their static game data properties?  That would seem wasteful.  Instead of holding that information on every game object, what if we held it in a single location but made it accessible for any enemy of the same type?

The Singleton Pattern

One solution for offloading the extra weight of static game data is to store that data in a class that is only instantiated one time when the game loads.  This methodology is known as the Singleton pattern.  To make a class a Singleton, it is constructed in this manner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class MyNPCManager : MonoBehaviour
{
        public static MyNPCManager Instance { get; private set; }

        private void Awake()
        {
            if (Instance == null)
            {
                Instance = this;
                DontDestroyOnLoad(gameObject);
            }
            else
            {
                Destroy(gameObject);
            }
        }

        //lets store some game data here
        public float MaxHealth = 100f;
}

To use this pattern, create an empty game object in your scene and attach the script to it.  Then, move the game object up towards the top of your object hierarchy above any game objects that would reference it.  This class can then have a single set of properties defined for the enemy NPC which could be accessed from any game object script.  Supposing we now have the property MaxHealth in the Singleton class and we needed to make sure a heal did not go over that value.  We could check this by accessing the instance of the Singleton.

if(myNPC.Health + HealAmount > MyNPCManager.Instance.MaxHealth)

This allows the MaxHealth to be stored once so that every instance of the NPC does not have to tote that value around.

I am currently using the Singleton pattern in several places in my game.  For example, all of the map tile static properties are held in a list of tiles generated in a Singleton class.  The definitions for my trees are also similarly held in a Singleton class as a list of flora.  While each tree game object isn't holding all the information about itself, the Singleton is still holding all the information for every single type of tree in the game.  But what if I had a map with only 3 types of trees in it?  The extra tree information would still be held in the Singleton, taking up memory.  Also, each time I access information about a single tree, I have to query the list of trees, locate the one I am searching for, and return the information I need from it.  I will mention another side-note about storing my data in a Singleton.  I cannot change the data at runtime.  If I want a tree to spawn 5 logs instead of 4 when it is chopped down, I have to edit the script manually.  Is there yet a better way to manage static data?  Unity says, "Yes there is."

Scriptable Objects

I guess that was a lot of rambling to get to today's topic, but I felt I needed to present you with a valid reason for talking about this somewhat-new-to-me Unity feature.  So, what is a Scriptable Object and how are they different from game objects?  Well, the better question to ask is how are they different from UnityEngine.MonoBehaviour?  "Scriptable" kind of gives it away that scripts are involved.  And I already mentioned that all scripts added as components to game objects MUST be derived from MonoBehaviour.  Therefore, scriptable objects are not script components of game objects.  They are in fact objects unto themselves that may be accessed in a scene but by attaching them to a game object component.

I am definitely not yet an expert on the subject as I have only just recently started learning about them.  But my immediate take-away thoughts were these.  First, as I mentioned, you cannot add a scriptable object as a component of a game object.  This totally threw me when I first read about that.  I thought, Where in the world do they go if they do not go on a game object?  Everything in Unity revolves around the game object and its components, right?  That was my thinking.  But then when I started thinking of scriptable objects as scripts (i.e. c# classes) that operate outside of game objects (i.e. I don't have to attach them to anything to use them), the smoke began to clear.

Here is where things get interesting.  Scriptable Objects basically derive from the same base as MonoBehaviours!  The main difference is that you MUST attach MonoBehaviours to game objects to use them, while you MUST NOT attach Scriptable Objects to game objects to use them.  One relies on game objects; the other does not.  But since they both have a common ancestry, they are both equally recognizable in the Unity engine space.  Here's another interesting fact.  Since scriptable objects are not attached to game objects, they DO NOT participate in the game cycle the way MonoBehaviours do and therefore do not consume frame time.  

Now let's pause and think about this, because an obvious question might start to rise up in your mind when you think about these things called Scriptable Objects.  How are they different from plain old C# classes?  I am glad you asked.  Here is the answer as I understand it.  Plain C# classes are not part of the Unity "object" universe.  They don't appear in the Inspector.  You cannot tweak their property values at runtime.  But scriptable objects are part of the Unity universe.  They do appear in the inspector when you select them, and they can be tweaked at runtime as well as design time.  You can use them as plain c# classes (derived from ScriptableObject of course).  But you can also work with them and modify values.  Those modifications are maintained when you exit play mode.  This is fantastic when you think that you can test values at runtime and the values will be retained when you exit play mode as opposed to manually editing script properties after leaving play mode.  Also, because they do not participate in the game loop, they never take up overhead until you need them.  This makes them an ideal location for static Game Data.

Sound interesting?  Yes, it does to me as well which is why I am preparing to replace much of my Singleton data stores with scriptable objects.  I have run long with this post, so I will have to bring you a part two sometime soon.  Meanwhile, I will be working to implement scriptable objects into my game and will certainly have some things to say about the refactoring that lies ahead of me and what I have learned from it.  

Until next time...