Hello again, Blogstalkers! Sorry for the lengthy delay between updates, but between the holidays and the numerous false starts on this particular topic, it took longer than I expected to actually settle on a simple, Unity-esque solution to the topic. And once I found one, I just kept focusing on the fun stuff; coding!
Anyhoo, as promised, today we’re going to look at defining Mob characteristics in a data file. Data-driven development, and all of the perks therein, should be obvious to anyone with half a clue – but if not, the primary reason I did it was to cut down on a lot of duplicate code in the various monster types. For example, let’s say you have a Kobold. When you spawn one, the object’s name needs to be set to “Kobold”, his height and weight need to be set, and his starting gear needs to be generated. Now, you can do this via inheritance in the monster’s constructor – and the original Java Android version of Dungeon Ho! did this – but it involved a lot of copying and pasting, and flipping between files to make sure I didn’t leave anything out. Wasn’t a big deal when I only had a few monster types. Much more of a deal when I had twenty. Much better to just pass a data file into the base Monster constructor and have it pull all the data from there. And, what better format to use for this than JSON? It’s human-readable, extensible, and most importantly, natively supported by Unity! Can’t beat that with a stick.
So first, let’s take a quick look at the current monsters.json file. This is, as you may have guessed, the JSON file that stores each monster’s default data. The beauty of it is that we can simply add new key-value pairs as we think of them, and it won’t break the existing loader; everything the C# code doesn’t recognize is simply ignored.
As you can see, each monster definition is labeled with an object name (in this example, “human”) and assigned a bunch of data. Since Unity handles text files directly, I simply dropped the .json file into a Data directory under my Assets folder in Unity, then dragged the file onto my Game object to create a reference to it. If you recall from our previous entry, the Game object is, well, a GameObject that manages the state of the entire game via the attached Game.cs script. So, by adding this line to the variable definition in that file…
public TextAsset monsterDefinitionsJSON;
We now have the ability to drag and drop a text file on there. So, we do.
Once that reference has been assigned, we need to parse the JSON. Merely assigning the asset causes Unity to automagically load the text file as a TextAsset, and we can then grab the text therein as a string. This’ll be necessary to let the JsonUtility class do its thing, since it needs a JSON-formatted string to pull the data from.
So, let’s talk about the object we’re using to store all of these monster definitions. It’s called, oddly enough, MonsterDefinitions.
Classes that can be parsed from JSON need to be tagged with [System.Serializable] – and note that we have a nested inner class to handle both the monster definition itself and the male/female pairings. The actual definition itself is mapped to the “human” local variable, which must match the monster’s object name from the JSON data.
Why didn’t we simply make an array of MonsterDefinitions, and then define it as an array of objects within the JSON? Because then the order of the definitions would matter, and I wanted to move away from hard-coding stuff like that. This way if I later decide to copy-paste a bunch of definitions and make a mistake somehow, I only screw up the ones I copy-pasted rather than all of them. Plus, and although this is a one-man project, let’s say a second person was tasked with inputting that data. You told them/wrote it down somewhere that the monster order matters, and it gets misplaced. Now you have an out of order data file. Yippee. Another drawback is having to possibly keep track of the number of monsters for the array allocation. And the fact that I’ve read rumblings that Unity’s JSON deserialization for arrays can be iffy and require a wrapper class anyway. Not worth the hassle.
Anyway. Moving on.
In the Game class, we take the passed-in reference to our JSON datafile and call the static method of the MonsterDefinitions class initDefinitionsFromJSON to parse the data and give us a MonsterDefinitions object, like so;
monsterDefinitions = MonsterDefinitions.initDefinitionsFromJSON(monsterDefinitionsJSON.text);
We can then use this object to retrieve the data from the ObjectSpawner, as the monster definitions variable is both public and static, alleviating the need for any obnoxious getters or reference passing. It’s totally okay since we’re only ever going to have one of these objects. And, although I don’t know if it’ll help, I null out the TextAsset reference afterward so the garbage collector can reclaim it, since we don’t need it anymore after that.
So how does the object spawner, use this data? Glad you asked.
The ObjectSpawner class uses similar logic for monsters that it did for the Player. Note that the basic GameObject creation has been abstracted into a separate generateEntity method. We do some basic housekeeping by setting a (temporary!) sprite to the SpriteRenderer so we can see it in game, as well as set the monster’s solid variable (we’ll cover that another time when we deep-dive further into the Monster and Item classes), but the most interesting thing is the assignMonsterComponent method. It takes a MonsterType enum, which is defined as an integer, and based on that, passes a particular MonsterDefinition from the Game’s MonsterDefinitions class into the Monster’s init method. That method is as follows;
Note the one-to-one mapping of variable names in the MonsterDefinition to the JSON data. Easy! We can grab the data fields as variable names and assign them to our own variables. Also note the slick data processing that checks whether or not we should format the Monster’s name with “A” or “An” depending on whether or not it starts with a vowel. Does this cause issues when localizing to other languages, you ask? Damn right it does! But I made the decision early on that such a text-heavy game would be basically impossible for me to localize anyway without a lot of work for very little gain. So if you were hoping to play Dungeon Ho!: Unity in German or Swahili, you’re out of luck.
Anyway, that’s all for today. Tune in next time when we lay the groundwork for items, including the Item class itself, extending the ObjectSpawner to handle them, and maybe even the Inventory system complete with picking stuff up that’s just lying around the dungeon. (Told you I’ve been busy!)