I spend… some time gaming. And a lot more time writing code that relates to gaming.

I have a hard time playing a game, when I see that it lacks feature ‘x’ – and I fully know that I could go right now and mod it in.

That’s why I joined the team at ADOH, to help add features to their server.

Through this process, I spent a good deal of time using the game’s proprietary scripting language, and eventually took it to the next level with modding. For modding, I had two options:

  • Use C++ and grind through development of custom plugins.
  • Use C# with .NET Core and use a higher-level layer to write my plugins.

While I knew in the long run #1 would probably be the better option, I really wanted to dig into .NET Core and see what I could do with it. For learning purposes, of course.

It started out well, but part-way through the process I had the “brilliant” idea of writing a custom component system on top of the APIs provided by the modding layer. I had a few reasons for this:

  • Unity uses a component system, and I absolutely love it.
  • I wanted to see what happened if I made my own component system. (learn)
  • I wanted to encapsulate my code better. This seemed like a very clear way to do it.

I learned a lot! But I also learned that I should have just used the APIs provided by the modding layer. I’ll explain why.

What is a component system?

I am referring specifically to the type of component system used in Unity, not to be confused with the newer ECS (Entity Component System) that Unity is building with DOTS (Data Oriented Technology Stack).

The basic idea of a component system in this form is that you have 3 types of objects:

  • Entities
  • Components
  • Magic Things (things that exist outside the standard lifecycle)

Entities represent an “object” in its purest form. The entity has no special capabilities, besides the fact that: 1) It can exist. 2) It can cease to exist. 3) It can have components attached to it.

Components, on the other hand, add functionality to the Entities. Components are nearly limitless in the things they can do, here are some examples:

  • A component can add a health parameter to an entity.
  • A component can spam your log with information every time an entity is destroyed.
  • A component can add a “position” parameter to an entity – this giving the entity a physical existence in your virtual game world.

The best component systems use static typing, generics, and dependencies for some really cool development experiences. For example:

  • A healthbar component that requires the entity to have a health component. It simply grabs the health component and displays the health.
  • A renderer component that requires the entity to have a position component. It uses the position to determine how to render the entity.
  • A gun component that requires any one IInput component (interface). Multiple components can implement IInput, such as an AI controller or a player controller.

Component systems and C#

Generally speaking, C# likes its allocations to be handled by the garbage collector (GC). This is a good thing, because it means that you don’t have to worry about as much about memory management. Interestingly enough though, this kind of fights against the idea of a component system. In a component system, components are usually destroyed explicitly, and intentionally.

For example, lets make our own little component system in pseudo-code:

class Entity
{
  List<Component> components = new List<Component>();

  public void addComponent(Component component) => components.add(component);
  public void RemoveComponent(Component component) => components.remove(component);
}

class ManagerOfThings
{
  List<Entity> entities = new List<Entity>();

  public Entity CreateEntity() => entities.add(new Entity());
  public void DestroyEntity(Entity entity) => entities.remove(entity);
}

This is a very bare-bones system that depends on the GC to destroy our objects. When we call DestroyEntity we are not actually destroying the entity, we are just removing it from the list of entities. The GC will eventually come around and realize that nothing is referencing the entity anymore, and it will destroy it.

This could work, until we start referencing components from other components. For example:

class FollowOtherComponent
{
  Entity toFollow;

  public FollowOtherComponent(Entity entity)
  {
    toFollow = entity;
  }
}

Yikes! GC no longer has our back, because the component is retained by another. I very quickly ran into this issue with my component system, and the (naive) solution is very simple:

class FollowOtherComponent
{
  WeakReference<Entity> toFollow;
  // ...
}

Works right? Yes it ‘works’ but we’ve just introduced an undocumented, unintuitive, and potentially dangerous requirement for our component system. Now, every component that wants to reference another component must use a WeakReference. This is not a good solution.

Before I move on, I want to point one of the ways Unity handled this, which is more-or-less a gotcha that nails a lot of new Unity developers. This is a very simplified example, but it looks something like this:

class Entity {
  private CppEntity cppEntity; // backreference to C++ entity which is NOT garbage collected
  // lightweight code
}

class FollowOtherComponent {
  Entity toFollow;
  void Update()
  {
    if (toFollow != null) {
      // do stuff
    }
  }
}

The idea here is that entities are actually lightweight wrappers that point to the true entity, which is managed by the C++ side of Unity. The idea is that it doesn’t matter if you cause toFollow to be retained. Because it’s not the real object, it’s just a wrapper – and the real object may be destroyed for all you know! The sneaky thing Unity does here, is that it overrides the == operator for the Entity class, so that instead of checking if toFollow` is null (it can’t be!) it checks if the cppEntity is null. This is a very clever solution, but it once again fights the C# language. Additionally, it prevents you from using ?? operators, which is a very common pattern in C#.

Doing things over time

For any game, you will need to do things over time. The API for the modding layer I was using encouraged using Tasks for this, and they had many extension methods that facilitated this. For example:

class ReadySetGoComponent : PlayerComponent
{
  NwPlayer Entity;

  void StartRace()
  {
    Task.Run(() => ReadySetGo());
  }

  async Task ReadySetGo()
  {
    await NwTask.Delay(TimeSpan.FromSeconds(1));
    Entity.SendServerMessage("Ready!");
    await NwTask.Delay(TimeSpan.FromSeconds(1));
    Entity.SendServerMessage("Set!");
    await NwTask.Delay(TimeSpan.FromSeconds(1));
    Entity.SendServerMessage("Go!");
  }
}

This did not work. To start, the task can continue running after the component is destroyed. The Entity var type is a wrapper around the underlying C++ entity, and it is not garbage collected. This means that the task will continue running, and it will try to access the entity – which is now destroyed. In addition to this, the task causes the component to be retained, which means that the component won’t be destroyed until the task is finished.

To get around the first issue, I suppose I double-check the Entity and make sure it’s valid, using Entity.IsValid, but then I have do do this after every single await. Ugly.

Onward though! The component system automatically destroys components that are attached to an entity, when the entity is destroyed by the engine. I might be able to leverage this to go ahead and cancel the task when the entity is destroyed. To prevent myself from having to stick a CancellationToken in every single component, I decided to make a base component class that would handle this for me. I ended up creating a complex Component class that basically operates like so: (pseudo-code)

class Component
{
  protected class DelayUtility
  {
    private CancellationTokenSource tokenSource;
    public DelayUtility(CancellationTokenSource tokenSource)
    {
      this.tokenSource = tokenSource;
    }

    public Task Delay(TimeSpan timeSpan)
    {
      await NwTask.Delay(timeSpan);
      if (tokenSource.IsCancellationRequested) throw new CustomException();
    }
  }

  protected async void StartTask(Func<DelayUtility, Task> taskBuilder)
  {
    try
    {
      await taskBuilder(ourDelayUtility);
    }
    catch (CustomException)
    {
      // do nothing
    }
  }
}

The idea was simple:

  • Start the task in the superclass, providing delays to use instead of the engine ones.
  • Throw a custom exception if the entity is destroyed.
  • Catch the custom exception in the superclass, and do nothing.

It was at this point that I realized a striking similarity between this and the Unity API. Unity has a concept of Coroutines, which are basically Tasks that are managed by the engine. They are started and stopped when the entity is destroyed, GameObject is disabled, etc. I actually begun to start understanding why Unity coroutines worked the way they did, not just how they worked. I also began to understand why it’s such a pain to mingle Unity’s coroutines with Tasks!


Conclusion (for now)

This project has been a very large learning experience for me. I now am starting to understand a lot of the mind-bogglingly confusedness of Unity’s API. But I also re-taught myself the very important lesson of choosing the right language for the task. C# is a great language, for many things! Just maybe not so much for a Unity-esque component system? I spent more time fighting the language than I did writing gameplay code. But now I need to go back and learn more about C# Tasks, and how to use them properly.

Code makes us wiser.

Code keeps us humble.