Frog Quota

Creating a Tag System in C#

A large part of a game programmer’s job is to design systems that efficiently use computer resources and are easy for the designers to use. In this post I’ll discuss an example of this, how I made an efficient tag system for our game.
Our game, broadly, is about fantasy adventurers exploring a dungeon the player has built and populated with monsters and loot. When the adventurer leaves the dungeon, they leave a review based on categories like how challenging the dungeon was, how much loot they found, whether it was in a style they like, and the variety of enemies they fought. Your goal as a player is to maximize these review scores.

It quickly became clear to us that we needed some way for adventurers to perceive attributes of things in the dungeon if they were to have opinions on them. Scoring loot is easy enough since the gold they received is just a number, but how do you measure style? How do you know if one enemy is similar to another? We settled on a tag system similar to what you might seen on Instagram. Many things in the game have tags that adventurers can perceive – a skeleton warrior for example has the tags “Skeleton” and “Warrior”. A golden treasure chest might have the tags “Loot Container” and “Fancy”. These tags are also handy for easily making complex AI behavior as well – an adventurer who is afraid of spiders can spot an enemy with the “Spider” tag and run for the hills.

The problem is that just looking at text tags as-is is quite slow since it involves comparing the text tags to various bits of code. When text is stored on a computer, every letter is encoded as a number. A word is then a list of numbers, one for each letter. For a computer, checking if one word is the same as another involves going through each word letter by letter (number by number, for the computer) and checking if they match. With enough adventurers and objects in the dungeon, there could potentially be hundreds of these tag comparisions every few seconds, so this slowness is unacceptable. What can be done? Well, while comparing text is slow for a computer, doing just one number comparison is very fast. So if we could replace the text tag with just a number, we could speed up this process dramatically.

The simplest solution is to just replace the list of tags with a list of numbers. The designers will keep track of what ID number corresponds to what tag in a file, and when designing say, a zombie warrior, they would just look up the IDs for “zombie” and “warrior” and add those to the new enemy. This is much faster in the game, but it’s also a real pain for the designers. We can do better.

What if instead the software had a file that kept track of the tags and their IDs? The designer would set up the enemy with text tags, and then when the game launches each object will read the file and look up the IDs it needs for their tags, and adding them to a list that it keeps.
This is better, but it still has a few problems. For one thing, if I want to add a brand new tag to an enemy, I need to remember to update the file that keeps track of the tags and their number. Another bigger problem is related to creating mods for our game. We want players to be able to make their own content that other players can download and use in their game. In order to add new tags to objects in their mods, players would have to modify this file. If two mods both added a tag with the same ID, there is no easy way to resolve the conflict – at best the game would confuse the two tags, at worst it would just crash. Back to the drawing board.

What we ended up settling on was a sort of dynamic tag database, the code of which you can find below If you find it useful, feel free to use it in your own project, though credit would be appreciated! 

We created a TagDatabase script, which is the only script allowed to access the collection keeping track of the tags and their corresponding ID number. When the game starts, this collection is empty. As before, the designer will define tags as a list of text words when creating objects. Then when the game starts, instead of looking up the ID for these tags themselves the objects ask the TagDatabase for the ID of their tags (through the GetID() function). The tag database then looks in its collection. If it finds the tag in there, it just replies with the ID of the tag. If the tag doesn’t exist, the database assigns it an unused ID, adds it to the list, and returns this new ID. This way the designer gets to write the enemies in a more readable way, the modders can add all the tags they want without any fear of overlap (unless they use the same words) and game performance is a lot better. Everyone wins!

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Keeps track of all the tags that exist in the game.
/// </summary>
public class TagDatabase : MonoBehaviour
{
    //two-way dictionary with tag IDs and tag names
    private Dictionary<string, int> _tags = new Dictionary<string, int>();
    private Dictionary<int, string> _names = new Dictionary<int, string>();
    private int _nextAvailableID = 0; //next available ID
    public static TagDatabase Instance;

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            Debug.LogError("More than one TagDatabase!");
        }
    }

    /// <summary>
    /// Looks for the ID of argument tag. If it can't find it, it assigns it an ID and returns that.
    /// </summary>
    /// <param name="tag"></param>
    /// <returns></returns>
    public int GetID(string tag)
    {
        string queryTag = tag.ToUpper();
        int tagID;
        //if the key exists
        if (_tags.ContainsKey(queryTag))
        {
            //great we found the thing
            tagID = _tags[queryTag];
        }
        else
        {
            //Add the tag to the dictionary with the next available ID. Save that ID. 
            tagID = _nextAvailableID;
            _tags.Add(queryTag, _nextAvailableID);
            _names.Add(tagID, queryTag);
            //increment the next available ID
            _nextAvailableID++;
        }
        return tagID;
    }
    /// <summary>
    /// Looks for the name of argument id.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public string GetName(int id)
    {
        return _names[id];
    }
}

Leave a Comment

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