Creating a RPG Database in Unity

on unity3d

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.