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...
No comments:
Post a Comment
Feel free to leave a comment. All comments are moderated.