Bestiary Management and Scriptable Objects

If you were creating a game like Final Fantasy you would need to manage and balance A LOT of data. The items in their shops (name, sprite, model, cost, etc) and the bestiary (name, hp, strength, experience award, etc) for example. The bestiary alone could easily have hundreds of entries.

Let’s suppose we were implementing the bestiary. A naive approach could be to directly create a prefab for each enemy, already configured to use the correct sprite and or model, and with components attached and stats typed into each one. Although it would technically work, it would be a nightmare to try to balance the data – there isn’t a good way to look at values across multiple enemies and make sure there is a good ramp in difficulty. It would be even worse if you ever decided to change architecture such as splitting a component into multiple smaller and reusable components, or even doing something as simple as renaming a variable – all of the data which had been serialized into the prefab will be lost.

A better approach would allow you to see your data in a spreadsheet. This way you can order your enemies however you like (for balance purposes), and at a glance compare how stats change over the course of the list.

I created a demo spreadsheet called “Enemies.csv” using some data from Final Fantasy 1 as an example. It shows the name of an enemy, the amount of hit points it has, the number of times it attacks, the damage it inflicts, its agility, the amount of experience you gain from killing it and the amount of gold you gain from killing it. There are more things we would actually need to track such as whether or not it is undead, what kinds of status ailments it can inflict when attacking, what it is immune to, etc. but this is good enough for a demo.

Name,HP,Hit Count,Damage,Agility,XP,Gold
IMP,8,1,4,4,6,6
GrIMP,16,1,8,6,18,18
WOLF,20,1,8,0,6,24
GrWOLF,72,1,14,0,93,22
WrWOLF,68,1,14,6,135,67
FrWOLF,92,1,25,0,402,200
ZOMBIE,20,1,10,0,24,12
GHOUL,48,3,8,6,93,50
GEIST,56,3,8,10,117,117
SPECTER,52,1,20,12,150,150
BONE,10,1,10,0,9,3
RBONE,144,1,26,12,378,378
SPIDER,28,1,10,0,30,8
ARACHNID,64,1,5,12,141,50
CREEP,56,1,17,8,63,15
CRAWL,84,8,1,8,186,200
GARGOYLE,80,4,12,8,132,80
RGOYLE,94,4,10,32,387,387
GARLAND,106,1,15,10,130,250
MADPONY,64,2,10,2,63,15
NITEMARE,200,3,30,24,1272,700
IGUANA,92,1,18,12,153,50
AGAMA,296,2,31,18,2472,1200
SAURIA,196,1,30,18,1977,658

The data here is very easy to balance, but it is not directly usable. You can get references to the TextAsset and parse it into data at runtime, but then you will need to hold the entire file in memory. Finding just the asset you need would require string parsing which is not efficient. You could try converting each entry into a data object and store it in a dictionary keyed by its name. That would allow quick lookup times, but caching the entire bestiary would result in an even larger memory footprint. On a mobile device it could be too much to handle.

The solution is found with the ScriptableObject and editor scripts. Here is a class that can hold an entry of our bestiary spreadsheet:

using UnityEngine;
using System;

public class EnemyData : ScriptableObject
{
	public int hp;
	public int hitCount;
	public int damage;
	public int agility;
	public int xp;
	public int gold;

	public void Load (string line)
	{
		string[] elements = line.Split(',');
		name = elements[0];
		hp = Convert.ToInt32( elements[1] );
		hitCount = Convert.ToInt32( elements[2] );
		damage = Convert.ToInt32( elements[3] );
		agility = Convert.ToInt32( elements[4] );
		xp = Convert.ToInt32( elements[5] );
		gold = Convert.ToInt32( elements[6] );
	}
}

Rather than manually copy the data from a spreadsheet to a scriptable object asset in the project, we will use Editor scripts to create them automatically for us. Even better, it is possible to “listen” to any time that the spread sheet changes and have the assets automatically update themselves to match!

Note that the following script needs to be placed in an “Editor” folder to work properly. Also, it expects the “Enemies.csv” to be placed in a “Settings” folder. The path was hardcoded but can be changed to suit your needs or to match your own project organization. The file path used to place the created assets puts them in a “Resources” folder which is necessary in order to load them using the “Resources.Load” method Unity provides.

using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections.Generic;

public class SettingsAutoConverter : AssetPostprocessor
{
	static Dictionary<string, Action> parsers; 

	static SettingsAutoConverter ()
	{
		parsers = new Dictionary<string, Action>();
		parsers.Add("Enemies.csv", ParseEnemies);
	}

	static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
	{
		for (int i = 0; i < importedAssets.Length; i++)
		{
			string fileName = Path.GetFileName( importedAssets[i] );
			if (parsers.ContainsKey(fileName))
				parsers[fileName]();
		}
		AssetDatabase.SaveAssets ();
		AssetDatabase.Refresh();
	}

	static void ParseEnemies ()
	{
		string filePath = Application.dataPath + "/Settings/Enemies.csv";
		if (!File.Exists(filePath))
		{
			Debug.LogError("Missing Enemies Data: " + filePath);
			return;
		}

		string[] readText = File.ReadAllLines("Assets/Settings/Enemies.csv");
		filePath = "Assets/Settings/Resources/";
		for (int i = 1; i < readText.Length; ++i)
		{
			EnemyData enemyData = ScriptableObject.CreateInstance<EnemyData>();
			enemyData.Load(readText[i]);
			string fileName = string.Format("{0}{1}.asset", filePath, enemyData.name);
			AssetDatabase.CreateAsset(enemyData, fileName);
		}
	}
}

Here I used a “static constructor” (sort of like a normal constructor, but for class initialization) to link the names of spreadsheets to an Action (a delegate which performs a task). In the “OnPostprocessAllAssets” method, I loop through the changed files and if any of them are in my parser dictionary, I parse that spreadsheet with the associated Action. You may want to make a different data manager for each spreadsheet if your project gets too large, but keeping it all in one place simplifies the demo.

With the script already in your project, try making some changes to the data in the spreadsheet. Whenever you move the focus back to Unity, it will reimport the spreadsheet and that will trigger the assets to be recreated, using the new values within. Note that deleted entries in the spreadsheet won’t remove their corresponding assets, but you can easily enough delete all the objects in the folder and let it be regenerated anyway.

You won’t want to directly link to any of these assets (such as in a prefab or scene component) because the link will break anytime the asset is regenerated. There are easy ways to get references at runtime, and you only need to load whatever is actually relevant at that time. Furthermore, any object which uses the data, can share a reference to the data, rather than needing to clone it with each instance.

The following script shows how to load and unload an asset at runtime:

using UnityEngine;

public class Demo : MonoBehaviour
{
	void Start ()
	{
		EnemyData test = Resources.Load<EnemyData>("AGAMA");
		Debug.Log(test.hp);
		test = null;
		Resources.UnloadUnusedAssets();
	}
}

39 thoughts on “Bestiary Management and Scriptable Objects

  1. this looks very useful!
    i haven’t tried it yet but i don’t understand what the results of it.
    so i get some scripted object, but how do i use it with my prefabs which already configured with scripts they need for doing their job?

    1. It is ultimately up to you. An architecture in this way can encourage patterns a bit closer to a traditional MVC (model view controller). In this example, your model is the scriptable object, the view could be the prefab of an enemy that displays this data in some way, and it would be connected and driven by a controller – perhaps the battle manager that would load an enemy and assign the data to it.

      Hopefully I didn’t confuse the issue by saying not to directly link to the assets in the project. I only meant not to try to reference them in the project (such as saved to a prefab) but you would definitely get a reference at runtime and link to that. If you had an “Enemy” script that normally would have had all of this same data directly entered into it, now you have a property for “EnemyData” instead. So instead of a line of code like “player.hp -= wolf.damage” you would have “player.hp -= wolf.data.damage”.

      I would also imagine an architecture where you create a factory class which creates your game objects based on this data. There are plenty of options- you could keep sprite or model names etc in the spreadsheet and have the factory create and configure game objects at runtime, or you could have parallel prefabs in your project already configured for display with the same name, but not having their “data” assigned. Then when a “name” is passed to the factory, it knows exactly what model to load and what data to assign it.

    1. Yes, but instead of being a single object holding all the data, you have lots of individual objects (one for each line of data), each of which can be loaded and unloaded directly by name.

    1. Scriptable objects beat XML and JSON because you don’t have to load the XML or JSON file itself and then parse the string contents and convert entries to data types etc. In short, it should consume less memory, and load faster since it has no need to convert anything and you can grab exactly what you want and ignore everything else.

  2. Design question – would you set up abilities as enums?

    I’m making a prototype where each unit can have exactly one ability, so I figure it’s not bad design to just list them on the spreadsheet in addition to other unit stats.

    I figure I’d parse the ABILITY field as an enum, but I’m curious how you’d approach it. I figure abilities would be defined in an abilities.cs script that has a list of enums. Mainly I just want something robust I can scale up later. How would you design it?

    1. If the game is simple enough that there is no variation in any of the abilities and there are only a handful of them, then it would be fine with me to do them as enums.

      Most games I can think of require some sort of individual parameters to go along with the abilities though. For example if the ability is to apply damage, then the parameter would be how much damage to apply. In this sort of case the enum architecture wouldn’t be sufficient. I would create the ability as its own simple object instead.

      1. Ah, I didn’t think this through – most of the abilities will require a target of some kind, so it’s probably better if I parse a string with the ability name and link it to a prefab/object.

        Would you recommend making each ability a prefab and parse them separately, similar to the units?

        1. Separate prefabs would be fine, then you could instantiate one per object that needed to use it which would make them each be able to have unique parameters. Of course, I created a bunch of Abilities you could learn from on the Tactics project too 😉

  3. I used these scripts as a base and made a few changes:
    – Changed filepath from ‘Settings’ to ‘ExternalData’
    – Changed filename ‘Enemies’ to ‘ProgramData’ (since units in my game are called programs) and changed the hardcoded references in the autoparser.

    I see no errors in the console but I also see no assets created. Any ideas?

    1. Make sure that your project panel has folders listed with the same names and hierarchies as you are using in your code. Also check the console for errors that might occur after you expect the script to run.

      1. I double-checked and everything seems to be correct.

        I see nothing in the console, even after hitting Play. Still no assets created in the ExternalData/Resources folder though.

        The parser script is in a folder named ‘Editor’ within the ‘Scripts/Programs’ folder.

  4. Debug logs seem to pinpoint the issue. ParseEnemies (function renamed to ParsePrograms) is not being called.

    static ExternalDataAutoConverter()
    {
    Debug.Log(“Static constructor called.”);
    parsers = new Dictionary();
    parsers.Add(“ProgramData.csv”, ParsePrograms);
    }

    I do see “Static constructor called.” in the console, but that’s it.

    1. My code looks exactly the same as yours with names and paths changed for my project. I’m not sure why ParsePrograms doesn’t get called (equivalent to your ParseEnemies).

      My debug statement in ParsePrograms is right at the beginning of the function call and it’s not registering.

      1. On further inspection, I found a conflict with the local variable ‘name’ in EnemiesData (renamed to ProgramData in my project) and with a standard library’s local variable ‘name’. I ended up changing it to programName.

        I see in the debug logs now that OnPostProcessAllAssets() is now called, but I’m still not seeing the debug statement that should be printed when ParseEnemies (renamed to ParsePrograms) is called.

        1. Just to be sure, do you understand that the script won’t automatically parse your .csv file simply by fixing and or making changes to the script itself? The “trigger” for your .csv to be parsed is that the importer detects that the .csv has changed since the last time it checked. If you are not making changes to the file then nothing will happen. In order to get it to run the first time it can be handy to manually trigger it using a ‘MenuItem’ tag. You can see an example of this in the ‘JobParser’ of the Tactics RPG Job post.

  5. Ah, yeah I needed to actually call the function. Sorry for my noobness, I misinterpreted the ‘automatically parse updates’ as also parsing it initially.

    How would I use the loaded asset? It’s just a set of fields, so I’m curious how to get the int/string contents of it.

    1. No worries, everyone has to start somewhere 🙂

      See the end of the post. If the asset is in a Resources folder you can load it whenever you want from wherever you want. I would probably load it via a Factory class to help construct a unit based on the info in the asset. I’m working on another project that shows more of working with these. It will take awhile though before I start sharing 😉

      1. I mean, after I load it, how do I read the fields like the name and whatnot? I’m not familiar with how to use it after I load it or what actually happens when it’s loaded.

        1. There is nothing special about a ScriptableObject – it is just like any other ‘object’ in programming terms. It is an instance of whatever class you defined it as.

          If it helps you, you can think of it having some similarities to a prefab. For example, when you use Resources.Load to get a reference to one of your assets, you are referencing the actual project asset and should not change any of the field values or you will be changing it for the whole project. Instead, you can use ‘Instantiate’ to clone it and then you are free to change the values of the cloned instance.

          Suppose you had a reference to a scriptable object which you named ‘stats’, you would “read” it through dot notation just as you would with a reference to any script. If you wanted its name you would just use ‘stats.name’. Does that help?

  6. Ok, back to architecture again. I’m thinking I would load all the unit assets at the start of the game and create prefabs from them in the resources folder. Is this how you’d approach it too?

    More in-depth, I’m thinking what I would do is delete the prefabs folder in Resources at the start of the game to remove any outdated prefabs and then recreate the folder and initialize each of the prefabs.

    What do you think?

    1. Nope. You’re missing some important things here. First of all, the whole point of having assets in a ‘Resources’ folder is so that you only load them when you need them. If you need them all at once, then you can just add them to an array field of one of your scripts in the scene. Then all the memory management (loading and unloading of objects) would be automatic as you load and change scenes, whereas any resources you load from the Resources folder you are also responsible for unloading when you are done with them.

      Second, if you are always going to create prefabs in exactly the same way, then you can do that in Pre-production with an Editor script too – no need to have a Scriptable object in the first place.

      Third, you would never want to delete project resources in-game. This should only happen in edit mode via an editor script, or from direct interaction in the editor itself. If you programmatically create objects in-game, then they will automatically be deleted when you exit the scene or exit play mode.

      1. Makes sense. So instead I should be doing:

        – Load asset file.
        – Create new prefab based on asset.
        – Instantiate the prefab.

        But now I’m concerned what happens if I change the asset and create a different prefab with the same name. Wouldn’t I then have two prefabs with the same name or something? Should I delete the previous prefab if it exists, just before creating the new prefab?

        1. You’re closer, but still not quite there. Creating a prefab is an editor-time process, not a game-time process. If you look at the ‘PrefabUtility’ class documentation you will see that it is in the ‘UnityEditor’ namespace.

          What you can do in game-time is Instantiate existing prefabs and then load settings on them based on your asset. You can also create new GameObjects and add components and children game objects etc to make whatever it is you wanted. If you made your own object (kind of like you would have made the prefab) then you don’t need to ‘Instantiate’ it, you can simply start using it. The only reason you would ‘Instantiate’ it is if you needed multiple copies of the object.

          When creating or cloning objects at game-time you don’t have to worry about name conflicts. These assets are not stored in the project and modifying their script’s fields won’t have any effect on the assets in the project that were used to Instantiate and or configure them.

          Again, let me reiterate, you would never want to delete or edit ‘project’ resources in-game.

          1. Ah, that clears things up more. So I should make prefabs for the units I want in my game, and then when it comes around to instantiating them in-game, I instantiate the prefab and load the settings based on the associated asset file.

  7. Alright, I got the asset loading and unloading to work, but now something else bothers me. I didn’t think it would be an issue until I saw my spawn UI appear after the game is supposed to have transitioned out of the spawn objects state.

    Right now the game starts in a spawn state where the user clicks on white tiles and spawns an object. Once there is an object on every spawn tile, the game transitions into the select target state.

    I checked the Debug Inspector and the battlecontroller is not in transition. There are simply two states active (this image shows the inspector in normal mode):

    http://i.imgur.com/yKRYVpJ.png

    The transition from select spawn state to select target state takes place in a coroutine after a yield return null command.

    The select spawn state is based on your select target state, which is also based on your select target state. I’m curious why the select spawn state might not be disengaged after a changestate command (located in the coroutine) ?

  8. Thanks for doing these posts. Very helpful. I’m having an issue with this and am not sure what is going on. I was able to follow the steps, modify the enemies class into a class I made and create the .asset files in Resources. To test it I ran the script at the bottom of the post (modified with the ItemData for my class instead of EnemyData) in a scene. It works fine the first time I do it, showing the ItemData name and itemId. Issue is that the second time I run the scene, the name turns to an empty string and the itemId becomes 0. Any other ItemData resources I try to load show the same thing.

    If I update the .csv file, the script regenerates the resources. If I run the scene again it works; the ItemData object is loaded and the Debug.Log shows the correct item data name and id. However the 2nd time I run it, I get the same issue the item name is blank and the item id is 0.

    1. Nevermind, figured it out. Had the fields in my class that replaced EnemyData labelled as private. Changed them to public and commented out the getters and things worked correctly. Thanks again for these blog posts.

  9. I see how you load and unload the assets at runtime, but what happens if I want my prefabs (not just .asset files) to be updated dynamically also? I figure that code goes in the OnPostprocessAllAssets() function inside the for-loop, but I’m a bit unsure on its implementation. Should I load the asset inside that loop, create the prefab, and then unload it, all inside that loop so it goes through each item?

    1. Take another look at the Jobs post. It demonstrates the ability to create prefabs, as well as how to load and modify existing ones. You are correct that you can do this inside of the loop, but working with them in the Editor is a little different than working with them in-game. You don’t have to concern yourself with unloading them.

  10. Hi,

    I have an error as follows:

    FormatException: Input string was not in the correct format

    System.Int32.Parse (System.String s) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System/Int32.cs:629)
    System.Convert.ToInt32 (System.String value) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System/Convert.cs:1270)
    EnemyData.Load (System.String line) (at Assets/Scripts/EnemyData.cs:21)
    SettingsAutoConverter.ParseEnemies () (at Assets/Scripts/Editor/SettingsAutoConverter.cs:43)
    SettingsAutoConverter.OnPostprocessAllAssets (System.String[] importedAssets, System.String[] deletedAssets, System.String[] movedAssets, System.String[] movedFromAssetPaths) (at Assets/Scripts/Editor/SettingsAutoConverter.cs:23)
    System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:222)
    Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
    System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:232)
    System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Reflection/MethodBase.cs:115)
    UnityEditor.AssetPostprocessingInternal.PostprocessAllAssets (System.String[] importedAssets, System.String[] addedAssets, System.String[] deletedAssets, System.String[] movedAssets, System.String[] movedFromPathAssets) (at C:/buildslave/unity/build/Editor/Mono/AssetPostprocessor.cs:27)

    I have not made any changes, and tried to follow your instructions explicity.

    My folder structure is:
    Assets folder, then a Scripts Folder wherein is the EnemyData.cs file,
    Editor folder within Assets, and within Editor is the SettingsAutoConverter.cs file
    Assets/Settings/Enemies.csv
    Assets/Settings/Resources

    No idea what I’ve managed to do wrong, any help is much appreciated.

    I am hoping to make my way through many of your posts here – veru useful for someone looking to get into Unity, C# and making games in general – thank you!

    1. Somehow there seems to be a disconnect between what appears in the Enemies.csv and the code that is trying to parse it into usable values. In particular, it is trying to turn some portion of a string into an Int32 and it is failing. I would recommend that you set a breakpoint and/or use “Debug.Log” statements just before the line that fails so you can see what the values are at the point you are trying to parse. It could be due to a misplaced comma, mis-ordered data, bad data, an extra empty line at the end of the csv, etc. For example, it will fail to parse the string “hello” into an integer. Good luck!

      1. I figured it out, there were double quotes around each set of data e.g. “IMP,8,1,4,4,6,5” instead of just IMP,8,1,4,4,6,5. Cannot see this in excel, but notepad++ showed the issue clearly.

        So the whole thing was trying to parse as a string…

        Also, I didn’t take a month to figure that – just came back to this as it was bugging me!

Leave a Reply

Your email address will not be published. Required fields are marked *