Let’s improve our Hack’n’slash data organisation!
This article is also available on Medium.
In the last episodes, we worked on our hero/enemy interactions. In particular, we set up a basic multi-hits combo system so that our player can hit the enemy and eventually kill it.
But, for now, we’ve directly initialised arbitrary values for the attack range, the healthpoints, etc. in our manager scripts… and we’re beginning to have quite a lot of components here and there on our game objects (plus some nested hierarchies). The problem is that although all of this is quick to implement, it’s not very reliable.
For example, as we start to add more parameters (like an attack rate, the attack damage…), it will start to crowd our manager scripts with lots of variables. And if we want to have several enemy types with different amounts of healthpoints, it will soon become pretty hard to configure. And if we create another “Brute” enemy, we’ll have to make sure to re-create the proper hierarchy and link everything up.
That’s definitely not robust enough for our game!
So: time to re-organise all this and take advantage of two other really cool features of Unity: the Scriptable Objects and the Prefabs 🙂
Storing “abstract” data thanks to Scriptable Objects
To begin with, let’s re-work our data organisation and create some data-related C# classes. To do this, we’ll use Scriptable Objects.
What are Scriptable Objects?
If we take a look at the Unity docs, we see that Scriptable Objects are a way of “[saving] large amounts of data, independent of class instances”. More precisely, they allow us to:
- save and store data during an editor session
- save data as an asset in our project to use at runtime
Scriptable Objects are classes of pure data that inherit from the
ScriptableObject class and that can be instantiated to create new assets saved in the project’s folder.
A nice thing is that because each created asset can be referenced in various scripts (without being re-instantiated), we avoid useless memory copies for shared data. And since Scriptable Objects are serialised by default, they are easily readable and tweakable in the Inspector.
Finally, whenever you modify a Scriptable Object at runtime, if you’re in the Unity Editor, then the modifications will remain after you exit the preview…
Note: for more details on the power and utility of Scriptable Objects, you can check out this talk at Unite Austin 2017 by R. Hipple on how his game company started to use Scriptable Objects as a core feature of their game architecture and design.
Storing data for our hero
First, let’s create a Scriptable Object class to describe the info related to our hero. This class will obviously need to be expanded throughout the series – we’ll add more and more variables as we implement new systems in our game.
For now, we’ll start with a very basic
PlayerData class where we re-inject all the current parameters of our hero for movement or attack:
This class simply inherits from
ScriptableObject and has a bunch of public variables that are serialised and can be edited in the Inspector. I’ve also added a
[CreateAssetMenu] attribute so that I can easily instantiate this class as a new asset.
So now, I can go back to the editor and right-click in the assets window to create a new instance of this class:
If I select my new asset, I can use the Inspector to set its variables – I’ll simply re-take the same values as what I currently have in my managers.
Then, I can add a reference to this new asset in my
PlayerAttackManager, and I use it in place of my previous variables:
Of course, don’t forget to drag the asset into the scripts
Data slot 🙂
Storing data for the enemies
We can then do a similar thing for the enemies. Again, we can extract the healthpoints parameter from our
EnemyManager into a new Scriptable Object class,
What is really nice is that now, we can directly create various instances of this Scriptable Object for each enemy type (to define the data specific to this enemy type).
But something important is that this class contains more “abstract” data that will be shared between all enemy instances; so
EnemyData cannot contain instance-specific info. This means that, here, the
healthpoints variable represents the maximum (and initial) amount of healthpoints for all the enemies that are of this type. And each enemy will then have its own
_healthpoints variable that is decreased when it takes a hit for example – but this variable is of course independent from one enemy to another (because we don’t want all enemies to lose healthpoints if the guard hits the one closest to him!).
For now, let’s re-create the data for our “Brute” enemy:
This data will then be re-used again and again by each instance of the “Brute” enemy to easily share parameters.
Just like before, we can finally update the
EnemyManager to use the data in an
EnemyData instance to initialise the
Pre-configuring scene pieces thanks to Prefabs
The second issue we need to solve is the question of game objects reproducibility and easy hierarchy duplication. Indeed, parallel to sharing pure data, we’d also like to share some configurations, component setups and object hierarchies.
This can be done fairly easily using one of Unity’s core tool: the Prefabs.
Roughly put, Prefabs are re-usable assets that store a game object with all its components, children, tags, layers… They are crucial to “copying” the same asset in multiple scenes because they keep all the copies in sync, and they are also a cool way of instantiating a game object with all of this setup at runtime.
To create a Prefab, all we have to do is drag a game object from our scene into our project’s assets folder – so, for example, we can create a new Prefab for our Player by dragging it to the assets:
Note: by the way, Unity helps you spot Prefab instances in your Scene hierarchy by showing them in blue 🙂
Then, if you select this brand new asset, you’ll see that the Prefab has kept all the components, tags, etc. Also, note that the Inspector shows you just the top-level of the Prefab hierarchy but you can “open the prefab” to explore it further.
The important part is that:
- some references, such as the Animator, are relative: for example, if you happen to instantiate this Prefab two times, each copy will reference its own Animator on its own sub-child object
- other references, on the other hand, are absolute: for example, all instances of this Prefab will automatically use the same
PlayerDataScriptable Object asset
We can of course do the same for our “Brute” enemy and therefore store all the various enemy game objects (for each enemy type) as its own Prefab.
This tutorial was a bit shorter than usual but it was essential to re-organise our assets and codebase before the game started to grow too much and got out of hands 😉
Now that we have a more robust way of defining enemy data, in the upcoming episodes, we will focus more on these enemies and see how we can make a simple AI using finite state machines…