Generating Character Names in Unity from External Files (Part I)

Check out the (free!) Unity asset, complete with readme and a basic usage guide, on GitHub!

Like many devs, we have lots of prototypes and work-in-progress titles lying around. Some are far along or in a pre-alpha stage, and some are the barest inklings of ideas that may or may not ever see the light of day. Wherever possible, though, we like to reuse and repurpose code if possible to save time and energy. So when a new project (you’ll be hearing about this one more as time goes on!) comes along and needs the ability to randomly build character names from external files, we dug into the archives and repurposed this gem.

What’s in a name?

The first step was determining what exactly we needed. First, we needed to be able to read the individual name components from external files. One of our guiding principles is to make all our games as moddable as possible—we believe that adds value to the final product and helps to build a thriving community. So using Unity’s default Resources class was not going to work, nor would asset bundles or other methods that would require the player to learn and understand Unity’s systems. Therefore, we decided to use the simplest file type we could—the humble .csv—and put the files somewhere the player can easily add, remove, or modify them. For this we decided the StreamingAssets folder would work best.

Next, we didn’t want to generate names from scratch with some sort of algorithm; that would distract from the goal of the project and the results are often unsatisfactory for the veneer of realism we wanted. So instead we resolved that the .csv files should hold lists of individual name components, like surnames, forenames (often called first or Christian names), and room for custom titles or suffixes. We also decided that we should be able to choose the forenames by gender.

We also wanted a way to separate the names by culture. In the modern world, globalization means that it’s reasonable to see a mixture of names from different ethnic or national backgrounds in a single place, but our game (spoiler!) is a long, long time ago. Therefore, we wanted to be able to separate the cultures into different files for convenience. This also means it’s important to be able to dynamically read those cultures into the code and use them somehow, as the game doesn’t know when it’s compiled how many files will be in the StreamingAssets folder.

It is also important to have the ability to prevent duplicate names. While it wouldn’t cause any collisions in our design, some people might use the names as the unique identifier for a character, which could cause major issues if duplicated. For our purposes, we just wanted to prevent player confusion if there were two different characters with identical names.

So our design goals were, in summary:

  1. read the name components from moddable external files
  2. these external files should contain lists of names: forenames, surnames, etc
  3. the names should allow for different genders
  4. the names should be able to be separated into cultures
  5. duplicate names should be prevented

CSV parsing

A .csv is beautifully simple to read, if you can trust the input. Unlike a commercial application where we might have to scrub an incoming file and carefully handle the various ways one can be screwed up, we figured that for modders it is sufficient to throw errors if the files are misconfigured. It’s possible in the future we could make a validator program to ship with the final product as an aid to modders to make sure their files are correctly designed.

For now, though, we settled on a single hardcoded delimiter, the “pipe” or “|” character. Since this character isn’t traditionally used in names or titles (unlike, say, an apostrophe “’” or even a comma “,”), we felt it a good choice.

Our CSV files are very straightforward:

surnames
male forenames
female forenames
titles
suffixes

All the names are in a single row separated by the delimiter, with a newline (\n) serving as a line delimiter. Therefore, given a known layout, without needing to bother with row lengths or all that jazz, the .csv parsing is straightforward using Linq:

public static string[][] ParseCSV(string lines)
{
    return lines.Replace("\r","").Split(lineDelimiter)
        .Select(line => line.Split(delimiter))
        .ToArray();
}

We read into a jagged array, where the first dimension points to the part of the name—called NameKind in the code—and the second dimension indexes each of the individual names.

Culture

Our next major problem was dealing with a variable number of .csv files in the StreamingAssets folder. The solution we came up with was using a dictionary: the file name of each .csv serves as the key, while we store all the jagged arrays as the values. We do these operations in two separate methods: LoadDirectoryToDictionary grabs a given directory within StreamingAssets (taking path as a parameter, so you could give it any directory path you want), and reading the file into a Dictionary<string,string>. A second method takes this first dictionary and parses the values into the jagged array described above.

One thing that bit us in the rear before we realized it was that we forgot you can’t use the Resources class in Unity for anything outside of the Assets/Resources “magic folder.” We’ll do a post on the magic folder system at some point soon to flesh out this idea further, but suffice it to say certain folders or methods in Unity have effects that may not be immediately apparent. For instance, when your game builds, everything in Resources is serialized and bundled into non-modifiable files used by your game. StreamingAssets, on the other hand, is located at an absolute file path depending on your build (Windows, iOS, Android, etc) and the files are not serialized, and so therefore the player can modify them to their heart’s content.

The problem is that Resources only deals in assets, and anything in StreamingAssets is not, ironically, an asset. A .csv file in Resources is a TextAsset type, and so with a simple call to myFile.text you can manipulate the file as a string. A .csv in StreamingAssets is a .csv. The class to manipulate these files, therefore, is actually Unity’s WWW class, even if you’re only working with strictly local files. The documentation asks for a URL as a parameter, but it can be the local machine’s files, as well. Here, for instance, is LoadDirectorytoDictionary:

/// <summary>
/// Loads the files in a local directory into a dictionary using
/// the filename as a key
/// </summary>
public static Dictionary<string, string> LoadDirectoryToDictionary( string path,
                                                                    string fileExtension)
{
    Dictionary<string, string> dict = new Dictionary<string, string>();

    if(Directory.Exists(path))
    {
        DirectoryInfo dir = new DirectoryInfo(path);
        FileInfo[] files = dir.GetFiles();
        int count = dir.GetFiles().Length;
        for(int i = 0; i < count; i++)
        {
            WWW file = new WWW(files[i].FullName);
            string loadedText = "";

            //curse you, .meta files
            if( files[i].Extension == fileExtension &&
                !files[i].Extension.Contains(".meta"))
            {
                loadedText = file.text;

                if (loadedText == "")
                {
                    Debug.Log("The requested " + fileExtension + " file at " +
                                path + " could not be found.");
                }
                else
                    Debug.Log("Loaded " + files[i].Name + " from " + path);
            }

            dict.Add(Path.GetFileNameWithoutExtension(files[i].Name), loadedText);
        }
    }

    return dict;
}

With the filename read into the code whenever this method is called, it works great. You can even modify the files during runtime: as long as you call this method after the file is added (perhaps on a menu activation, etc), it will give you a filename to use as a key. If you then populate, say, a dropdown with these filenames, like…

Dropdown myDropdown.options = new List<string>(myDictionary.Keys);

…you can avoid all instances of “stringly-typed” method calls. Or, if you have no other choice, you can call a particular file like so (spoiler for the finished product!):

NameBuilder.RandomUniqueName(Gender.FEMALE, "EarlyRoman")

However, if your “EarlyRoman” file doesn’t exist, or you spell it wrong, it’ll just return a blank name; we use TryGetValue on the dictionary to avoid throwing any errors on null references, but obviously a blank name is not ideal behavior. Your best bet is to never assume that there will be a certain file in a folder if your player can modify that folder.

Conclusions, for now

We’ve not yet finished going over how CulturalNames works; for next time, we’ll address preventing duplicate names, grabbing random values from arrays, and why we chose to use a struct to hold the final product. Contact us if you have any questions about our assets or if you have a suggestion for a different method of doing this, and we’ll see you next time.