When creating a role-playing game you’ll quickly be faced with the daunting problem of storing possibly hundreds of items, spells, abilities, classes, npcs and other data about the game.
What is the best way of storing this information? Here is my solution.
I Use YAML
yaml – a human friendly text file format. Yaml is easy to read, change and extend. It even supports something similar to variables. It is far superior to JSON and XML, at least in this use case.
A yaml file is created for each type of data, Spells.yaml
, Abilities.yaml
,
Classes.yaml
, etc. And is put inside the Assets/Resources
directory, so it
can be loaded at runtime.
How to Load Yaml
Next a parser is needed, I personally found great success with yamldotnet.
Download it and put it in your Assets
directory.
Example Yaml File
So in my game, there are many classes with each having it’s own stats – health, speed, attack – and each having it’s own abilities. Take a look.
classes:
- name: Grenadier
description: Medium range heavy unit with support abilities.
stats:
health: 80
supply: 8
sightRange: 10
speed: 2
shootRange: 8
attack: 8
cooldown: 8
armored: true
abilities:
- name: Grenade Launcher
- name: Smoke Launcher
- name: Rifleman
description: Throws frags.
stats:
health: 100
supply: 8
sightRange: 10
speed: 2
shootRange: 7
attack: 10
cooldown: 8
abilities:
- name: Frag
I called this file Classes.yaml
and put it inside the Assets/Resources
.
Note: Quotes are optional in yaml and arrays are created with a simple -
.
Also ensure you do not mix tabs and spaces or your yaml file will have trouble
loading. It’s best to display tabs in your editor to catch the problem as it
happens.
Loading The Yaml File
I created a Datastore
class that acts as a central place to load all yaml files.
Once loaded the data is stored in a method called AddItems()
, that will be
explained in a minute.
using UnityEngine;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
public class Database {
public class ClassList {
public List<Class> classes { get; set; }
}
public Database() {
}
public void Load() {
LoadClasses();
}
private void LoadClasses() {
TextAsset classesYaml = Resources.Load("Classes") as TextAsset;
StringReader input = new StringReader(classesYaml.text);
Deserializer deserializer = new Deserializer(namingConvention: new CamelCaseNamingConvention());
ClassList classList = deserializer.Deserialize<ClassList>(input);
Class.AddItems(classList.classes);
}
}
The ClassList
class defines the structure of the yaml file.
The parser will actually create Class
instances with the yaml data and store them in the array, no messy string names to deal with.
This is all a Yamldotnet feature, once again great library.
How to use it
First load the yaml data in a Unity MonoComponent
.
Datastore datastore = new Datastore();
datastore.Load();
Then whenever you like in your game, grab some data.
Class klass = Class.Find(Classes.Rifleman);
Debug.Log(klass.health);
foreach(Ability ability in klass.abilities) {
Debug.Log(ability.name);
}
Warning: A class can be found from anywhere in the code base, it’s best practice to not modify the classes and to make them read-only. Subtle bugs can be introduced if you change this static data.
How Does This Work?
Let’s dive into the Class
and Ability
classses.
public enum Classes {
None,
Rifleman,
Grenadier,
SpecOps
}
public class Class : DataStore<Classes, Class> {
public List<Ability> abilities { get; set; }
public Stats stats { get; set; }
public Class() {
stats = new Stats();
abilities = new List<Ability>();
}
}
public enum Abilities {
}
public class Ability : DataStore<Abilities, Ability> {
public string name { get; set; }
public string description { get; set; }
public string animation { get; set; }
public string target { get; set; }
public int cost { get; set; }
public int attack { get; set; }
public int range { get; set; }
public bool patrol { get; set; }
}
Each class has a corresponding enum
which is like an index to find all items in
the game. Usually a plain string or number is used to, but that only works for so long
until items get renamed or removed and subtle bugs are created. I enums are much
cleaner.
Each class also extends from Datastore
, this class adds instance variables id
, name
and
description
but more importantly it adds static methods – AddItems()
,
Clear()
– for finding and
registering global data.
Note: When loading yaml data, ensure each property on the class has an
attribute accessor– { get; set; }
– or else an error will occur.
The Datastore Class
using UnityEngine;
using System.Collections.Generic;
using System.Text.RegularExpressions;
public abstract class DataStore<R, T> where T : DataStore<R, T> {
private static Dictionary<R, T> _items = new Dictionary<R, T>();
public static T Find(R id) {
return _items[id];
}
public static void AddItems(List<T> items) {
foreach(T item in items) {
AddItem(item);
}
}
public static void AddItem(T item) {
string idName = Regex.Replace(item.name, @"\s+", "");
R id = (R)System.Enum.Parse(typeof(R), idName, true);
_items[id] = item;
item.id = id;
}
public static void Clear() {
_items.Clear();
}
public R id { get; set; }
public string name { get; set; }
public string description { get; set; }
public DataStore() {
}
}
This is a template class, which means it appends these methods on to child
classes. Because it’s defined as a abstract
class it cannot be instantiated,
only derived classes can be.
This is the magic line.
string idName = Regex.Replace(item.name, @"\s+", "");
R id = (R)System.Enum.Parse(typeof(R), idName, true);
Using the loaded item name from yaml it will figure
out the correct enum, so it can be used in the Find()
method later.
It will also remove spaces so a class name as Wizard Master
will have an enum
of WizardMaster
.
In Conclusion
Yaml is a great file format. Using the Datastore
and Database
classes you
can load an unlimited number of data files for your game. This data can then be
accessed anywhere for reading.
Thanks for reading my blog. Post an questions below.