top of page

Embers of Steel - Dev Diary 1: The Alias System

Updated: Mar 15, 2022

Overview

This is an ongoing project. Using everything I've learned, I decided to build a TRPG in Unity from scratch. There are many complex systems at play here, but one system that I am very proud of is the Alias System. In coordination with the Visual Novel System, I was able to create cinematic moments in my game.


Tech Summary

Genre

Tactical Role-Playing Game (TRPG)

Team Size

1 Developer

Engine

Unity 2021

Platform

PC

Development Time

Ongoing (3 Months)


 

Content

Intro

As a designer, I wanted fine-tune control over the gameplay so that I could convey important details to the player. In terms of features, I wanted to:

  • Block/Unblock Player Input on specific units.

  • Move the camera to look at specific objects.

  • Highlight specific objects or units.

  • Activate/Deactivate AI for specific situations.

And all these functions needed to integrate with the Visual Novel System, meaning I had to, as the designer, be able to call commands in the dialogue text files that corresponded to actual C# code.


Architecture

I was inspired by "Creation Kit" inside of Fallout 4. Since the same objects or characters in the world could be referenced by multiple quests, the quest used aliases, quest-specific variables to manipulate those entities independently of other quests.

Alias Window from Creation Kit

I realized that a simplified version of this could work inside of Unity. An alias could represent a generic Unity GameObject with a corresponding string ID. With this key-value pair, a Dictionary was the obvious choice for a data structure. This was the resulting system:

UML Diagram of basic Alias System

Implementation Notes

If you'd like to see the full AliasSystem code, you can see it here on GitHub. Note that this code uses Odin Inspector, a Unity Asset Story plugin for Editor GUI formatting, so this code will only compile if you have Unity installed with Odin Inspector or you remove those dependencies.


Alias ID

The first step was the AliasID. I could have made this a string variable, but instead I made a wrapper class that surrounds the actual string _id. This accomplishes two things:

  • Ensures the value cannot be changed at runtime. I can only access the value in editor.

  • Encourages type safety.

This ensures that I will not confuse AliasID for any old string value, and even if I did, the compiler will catch that error for me. And in practice, I don't really care what the AliasID contains. I only care that the database has a match.

using System;
using UnityEngine;

namespace EmbersOfSteel.AliasSystem.Data
{
    [Serializable]
    public class AliasID : IEquatable<AliasID>
    {
        [SerializeField]
        private string _id = "";

        public bool Equals(AliasID other)
        {
            return _id == other._id;
        }

        /// Allows AliasID to be implicitly cast as a string without
        /// needing to access _id directly or having to override 
        /// and call ToString().
        /// i.e: Console.Log(aliasID);
        public static implicit operator string(AliasID aliasID) => aliasID._id;
    }
}

Alias Data

Through testing, I realized that I might want to do operations on groups of GameObjects under a single AliasID, so the AliasData evolved from a GameObject to a List of GameObjects.

using System.Collections.Generic;
using UnityEngine;
using Sirenix.OdinInspector;

namespace EmbersOfSteel.AliasSystem.Data
{
    [System.Serializable]
    public class AliasData
    {
        [SerializeField, HideLabel]
        private AliasID _id = new AliasID();
        public AliasID id => _id;

        [SerializeField]
        private List<GameObject> _references = new List<GameObject>();

        public List<GameObject> references => _references;
    }
}

Alias Database

Unity does not natively support serializing Dictionaries, meaning I cannot edit values in the Dictionary from the editor. I can, however, edit Lists from the Editor, so the simple workaround was to have a List of AliasData for Editor and a Dictionary that would be populated by that List at runtime.

...

namespace EmbersOfSteel.AliasSystem.Core
{
    [System.Serializable]
    public class AliasDatabase : MonoBehaviour
    {
        ...
        
        [SerializeField, TabGroup("Aliases")]
        private List<AliasData> _aliases = new List<AliasData>();

        private Dictionary<string, List<GameObject>> _aliasDatabase = null;
        ...
}

And so, this code populates the entire dictionary database with entries from the list.

private void InitializeDatabase()
{
    _aliasDatabase = new Dictionary<string, List<GameObject>>();
    foreach (AliasData alias in _aliases)
    {
    if (_aliasDatabase.ContainsKey(alias.id))
    {
        Debug.LogError("Duplicate id detected for key: " + 
        alias.id + ". Alias entry rejected.");
        return;
    }
    if(alias.references == null)
    {
        Debug.LogError("GameObject is missing for alias: " + 
        alias.id + ". Alias entry rejected.");
        return;
    }
    _aliasDatabase[alias.id] = alias.references;
    }
}

However, we do not want people touching the Dictionary directly, so these two public functions act as the interface from which other classes can access the GameObject or GameObjects that they are looking for.

/// <summary>
/// Returns the first element of the Alias group.
/// </summary>
public GameObject GetFirstAlias(string id)
{
    return GetAllAliases(id)[0];
}

public List<GameObject> GetAllAliases(string id)
{
    if (_aliasDatabase.ContainsKey(id)) return _aliasDatabase[id];
    Debug.LogWarning("Key '" + id + "' does not exist.");
    return null;
}

And so, a typical request from the AliasDatabase might look something like this:

GameObject unit = _aliasDatabase.GetFirstAlias("Pikeman1");

This is what the end product looks like in the Unity Editor:



With this setup, a designer can quickly grab any game object in scene and mark it under any aliases for all kinds of scripted sequences like the one you saw in the intro video. Much of that system makes full use of the Alias System.


The Dynamic Alias Problem

There was one major problem with this setup. This only works for objects that already exist within scene, and unfortunately, units in my game spawn at runtime. They spawn using UnitMarkers. At runtime, the UnitSpawner looks for UnitMarker GameObjects and spawns the in-game units at those locations with whatever initialization data was on the UnitMarker.

My solution was two-fold. I created an AliasEntity component that I attached to the UnitMarker. The idea was that since the UnitMarker is already there in editor, acting as a placeholder for the actual GameObject(s), it can hold the data that the spawner would need to submit the actual alias data to the database.

using UnityEngine;
using EmbersOfSteel.AliasSystem.Data;
using Sirenix.OdinInspector;

namespace EmbersOfSteel.AliasSystem.Core
{
    public class AliasEntity : MonoBehaviour
    {
        [SerializeField, HideLabel]
        private AliasID _id = new AliasID();
        public AliasID id => _id;

        [SerializeField]
        private bool _isActiveAlias = false;
        public bool isActiveAlias => _isActiveAlias;
    }
}

I then made two additional classes, a SpawnerBase that processes the spawn data into an alias and a SpawnEventArgs that the SpawnerBase packages all the data into. The idea was that whenever a unit is spawned in, the SpawnEvent will be invoked, sending in the SpawnEventArgs that contain everything the AliasDatabase would need to register this new alias.

using System;
using System.Linq;
using UnityEngine;
using EmbersOfSteel.AliasSystem.Core;

namespace EmbersOfSteel.AliasSystem.Spawners
{
    public abstract class SpawnerBase : MonoBehaviour
    {
        public event EventHandler<SpawnEventArgs> SpawnEvent;

        /// <summary>
        /// Called to register a gameObject as an alias.
        /// </summary>
        /// <param name="aliasEntity">The alias containing the ID to assign to the reference.</param>
        /// <param name="reference">The target gameObject to store in the alias system.</param>
        protected void InvokeSpawnEvent(GameObject aliasEntity, GameObject reference)
        {
            SpawnEventArgs spawnEventArgs = new SpawnEventArgs(aliasEntity.GetComponents<AliasEntity>().ToList(), reference);
            SpawnEvent?.Invoke(this, spawnEventArgs);
        }
    }
}

InvokeSpawnEvent above will provide a List of AliasEntities and a GameObject. This is all packaged neatly into the SpawnEventArgs.

using System;
using System.Collections.Generic;
using UnityEngine;
using EmbersOfSteel.AliasSystem.Core;

namespace EmbersOfSteel.AliasSystem.Spawners
{
    public class SpawnEventArgs : EventArgs
    {
        public readonly List<AliasEntity> aliasEntities;
        public readonly GameObject reference;

        public SpawnEventArgs(List<AliasEntity> aliasEntities, GameObject reference)
        {
            this.aliasEntities = aliasEntities;
            this.reference = reference;
        }
    }
}

Any Spawner can inherit from this class, and all they need to do to register their aliases is to call the SpawnerBase.InvokeSpawnEvent function. In my case, that would be the UnitSpawner.


public class UnitSpawner : SpawnerBase, IUnitGenerator, ISaveable
    {
        ...

        public List<UnitEntity> SpawnUnits(List<Cell> cells)
        {
            List<UnitEntity> units = new List<UnitEntity>();
            
            foreach (UnitMarker unitMarker in _unitMarkers.GetComponentsInChildren<UnitMarker>())
            {
                if(!_hasGameStarted)
                {
                    UnitEntity unit = SpawnUnitFromMarker(unitMarker);
                    RegisterUnitToNearestCell(unit, cells);
                    units.Add(unit);
                    InvokeSpawnEvent(unitMarker.gameObject, unit.gameObject);
                }
                unitMarker.gameObject.SetActive(false);
            }
            _hasGameStarted = true;

            return units;
        }
    }

The UnitSpawner supplies the UnitMarker which contains the AliasEntity and the GameObject that will be registered to the AliasDatabase. With this, I can now track units being spawned on the map.


Further Applications

I discovered that I could also use the Alias System to check specific conditions in gameplay to trigger even more dialogue/gameplay sequences.

For instance, I could check the alias of units and tiles to see if a specific unit is occupying the space. Or even check if a unit has been killed or is alive. This file you are seeing houses all the dialogue trigger data for Map 1 of my game.

If I wanted to, I could take this further and make it part of the win conditions for a level and not just to trigger dialogue.


206 views0 comments

Recent Posts

See All

Comments


bottom of page