Software Development

Using generics for type-safe and type-specific identifiers

After the slightly philosophical diversion of last week’s post, today I would like to present a few more technical ideas building on my post about accessing game objects by unique identifiers.

Last week I made an argument for simplicity, for keeping it simple. However, complexity does have its place and there are several good reasons to increase the complexity of a system. One such reason is, if we can make that complexity do work for us.

That is what we will do today.

We will expand our solution from last time using generics to be more powerful and flexible, while still being easy to use.

Recap: unique identifiers

In the previous post we created an Id type, which represented an identifier that we could use to uniquely identifier game objects – especially in networked multi-player scenarios.

The identifier was represented using an integer internally, and we used an IdManager to create unique identifiers for us, making sure we would not encounter doubles.

We would then keep a dictionary of our game objects, with their identifier as key, for fast and easy access. We use an event to notify us of the object’s deletion, so that we can remove it from the dictionary when appropriate.

Here is the essential code developed in that post. For the details, make sure to check the full post.

struct Id
{
    private readonly int value;
    public Id(int value)
    {
        this.value = value;
    }
}
class IdManager
{
    private int lastId = 0;
    public Id GetNext()
    {
        this.lastId++;
        return new Id(this.lastId);
    }
}
class GameState
{
    private readonly IdManager idManager = new IdManager();
    private readonly Dictionary<Id, GameObject> gameObjects =
        new Dictionary<Id, GameObject>();
    public Id GetNewId()
    {
        return this.idManager.GetNext();
    }
    public void Add(GameObject obj)
    {
        this.gameObjects.Add(obj.Id, obj);
        obj.Deleting += () => this.gameObjects.Remove(obj.Id)
    }
}
delegate void VoidEventHandler();
class GameObject
{
    public event VoidEventHandler Deleting;
    private readonly GameState game;
    private readonly Id id;
    public Id Id { get { return this.id; } }
    public bool Deleted { get; private set; }
    public GameObject(GameState game, Id id)
    {
        this.game = game;
        this.id = id;
        game.Add(this);
    }
    public void Delete()
    {
        if(this.Deleted)
            return;
        if (this.Deleting != null)
            this.Deleting();
        this.Deleted = true;
    }
}

Shortcomings of global identifiers

The solution above is great if we need to treat all of our game objects the same. In reality, this is not always true however.

For example, we may receive a network message to destroy a certain unit in our strategy game, containing the identifier of that unit. Or, we may receive a message to update the position of a projectile, or to change the build queue of a factory.

In each case, we the identifiers refer to different kinds of objects. What we can do of course is getting the respective game object, and casting them to the type of object we expect it to be, and in most cases this will work just fine.

However, what if we could encode the type of object into the identifier itself, and keep track of different objects by different identifiers based on their type?

Generic identifiers

We will use generics to do just that. Specifically, we will make our Id type generic as follows:

struct Id<T>
{
    private readonly int value;
    public Id(int value)
    {
        this.value = value;
    }
}

This does not seem to do much. However, it gives us one key property: Type-safety.

We already used a structure, instead of a simple integer, to make forging identifiers if not impossible, at least very obvious.

By using generics, we further constrain the semantic meaning of identifiers, by associating them with a type. In terms of our examples above, we could now have identifiers of units, projectiles and factories:

var unitId = new Id<Unit>(0);

This type-safety means that we cannot compare identifiers of different types by accident, and it also means that we can easily keep track of a game object by a number of different identifiers, one for each type we are interested in.

For example, a unit in our game could be a factory as well, and we could have it be addressable as both an Id<Unit> and an Id<Factory>.

Generating generic identifiers

Generating these generic identifiers is almost as easy as it was before – assuming we are fine with using the same integer counter for all our different identifier types.

We simply have to change our identifier generating method to be generic as follows.

class IdManager
{
    private int lastId = 0;
    public Id<T> GetNext<T>()
    {
        this.lastId++;
        return new Id<T>(this.lastId);
    }
}

This can now be used as follows.

var idMan = new IdManager();
var unitId = idMan.GetNext<Unit>();

We can also keep separate counters for the different types, which I consider more elegant. It also helps with debugging, and improves the hashing behaviour and therefore performance of our dictionaries.

What we have to do is keep a dictionary from all known identifier types to their counter. When we generate a new identifier, we check if we already know the specified type, and otherwise start a new counter, making sure to properly track the values in our dictionary.

class IdManager
{
    private readonly Dictionary<Type, int> lastIds =
        new Dictionary<Type, int>();
    public Id<T> GetNext<T>()
    {
        int lastId;
        var type = typeof (T);
        this.lastIds.TryGetValue(type, out lastId);
        lastId++;
        this.lastIds[type] = lastId;
        return new Id<T>(lastId);
    }
}

This is slightly more verbose and somewhat slower than the simple increase of an integer. However, we should not be generating identifiers nearly as often for this to cause a measurably difference. The advantages of easier debugging, and later lookup performance clearly win out here.

Keeping track of generic identifiers

Keeping track of these generic identifiers is the most complicated part of this post, and we will build the required system step by step.

The most important thing for this system is that is should be easy to use. As such, ideally we would like to keep track of our objects with as little code as possible.

We will aim to make it essentially as easy as this:

class UnitFactory : GameObject
{
    public UnitFactory(GameState game, Id<Unit> unitId, Id<Factory> factoryId)
        : base(game)
    {
        this.idAs(unitId);
        this.idAs(factoryId);
    }
}

Assumptions and interfaces

First, we will start with the assumption that each object that has an identifier of a type is of that same type, and probably needs to display this identifier publicly.

The second assumption is not much to ask. After all, it stands to reason that at various times we may be interested in checking or comparing an object’s identifier – to make sure we have the correct one, or to read it for sending to a different system.

The first is a bit more involved however and the technical reasons for the constraint will become clearer in a bit. However, note that if our object has both identifiers as unit and as factory, we should be able to use it as both a unit and a factory.

To make that possible, we can introduce IUnit and IFactory interfaces, both of which are object can implement. Since these interfaces are the types that we will want to have identifiers of, we can make both of them implement another interface, IIdable, which contains our generic identifier as follows.

interface IIdable<TId>
    where TId : IIdable<TId>
{
    Id<TId> Id { get; }
}
interface IUnit : IIdable<IUnit>
{
}
interface IFactory : IIdable<IFactory>
{
}

Note the self-referential generic constraint used in the IIdable interface. This constraint ensures that the given type is in fact implementing the same interface. This gives a strong hint to use it as above and prevents nonsense like interface ITest : IIdable<RandomType>.

Our code from above will now look more like this:

class UnitFactory : GameObject, IUnit, IFactory
{
    private readonly Id<IUnit> unitId;
    private readonly Id<IFactory> factoryId;
    Id<IUnit> IIdable<IUnit>.Id { get { return this.unitId; } }
    Id<IFactory> IIdable<IFactory>.Id { get { return this.factoryId; } }
    public UnitFactory(GameState game, Id<Unit> unitId, Id<Factory> factoryId)
        : base(game)
    {
        this.unitId = unitId;
        this.factoryId = factoryId;
        this.idAs(unitId);
        this.idAs(factoryId);
    }
}

Dictionaries of generic identifiers

With the above interfaces in place, we can now easily add our objects to the correct dictionaries.

Let us first implement the GameObject.idAs() method we used above:

protected void idAs<TId>()
    where TId : IIdable<TId>
{
    var asTId = (TId)this;
    var dictionary = this.game.GetIdDictionary<TId>();
    dictionary.Add(asTId.Id, asTId);
}

Note that since we are in the GameObject class, we have to cast ourselves to the generic TId type, to make sure we can access the identifier, and add it to the dictionary.

Still missing now are the implementation of GameState.GetIdDictionary<TId>(), as well as how we will handle deletion, now that the situation has become somewhat more complicated.

However, these aspects can be solved analogously to the above and I leave them as exercise for the reader.

Conclusion

In this post we saw some of the ways in which we can expand on our previous identifier solution using generics.

I hope this has been interesting, and of course feel free to share any thoughts or questions you may have in the comments below.

Until next time,

Enjoy the pixels!

Paul Scharf

Paul is a self-publishing game developer. He believes that C# will play an ever growing role in the future of his industry. Next to working on a variety of projects he writes weekly technical blog posts on C#, graphics, and game development in general.

Related Articles

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button