thoughts, stories and ideas

Here at quasiStudio, we focus on clean, testable and reusable code when developing our indie mobile games.

We are using micro-system design for our game code. Game logic is separated into numerous micro-systems that do only one thing, but do it very well.

While Unity (legacy Unity3D) is working on its new Entity Component System (ECS) implementation, we have decided to go with Entitas - a super fast ECS Framework specifically made for C# and Unity.

Coupled code issues

When we first started prototyping Aero Attack, we used the traditional MonoBehaviours approach.

It was great at the beginning, but soon things started breaking left and right. We would implement one thing, and 10 others would break.

At that point we made a decision to make a switch from MonoBehaviour architecture to a micro-system architecture.

That move helped us considerably reduce the amount of bugs, improve overall stability and finish the project with a much cleaner code that we could easily test.

Every day our test coverage grew and we felt more secure modifying the code. We didn't have to worry about things breaking anymore.

How does it work?

You start by defining contexts of your ECS. We have separated our ECS into Game, Input, Audio, and many more contexts.

This will allow you to namespace your components to be assignable only to entities from a specific context.

That's important because you will only have to search through one or more much smaller lists, and your entities will be more organized this way.

[Audio, Unique]
public sealed class MusicComponent : IComponent {
    public string Filename;
}

This is a Component, and its only job is to hold data. In this case, it's the filename of the audio clip that is currently playing.

Audio attribute means it can only be assigned to an entity in Audio context. Unique attribute prevents this component from being assigned more than once.

As soon as you attach this component to AudioEntity, PlayMusicSystem will start playing a track with filename defined in MusicComponent.

public sealed class PlayMusicSystem : ReactiveSystem<AudioEntity> {
    ...
}

This is our PlayMusicSystem. It contains only 3 methods:

protected override ICollector<AudioEntity> GetTrigger(IContext<AudioEntity> context) {
    return context.CreateCollector(AudioMatcher.Music);
}

In GetTrigger we create a Collector for PlayMusicSystem.

Collectors job is to find all entities that have the Music component.

protected override bool Filter(AudioEntity entity) {
    return entity.hasMusic;
}

Filters job is to filter out entities found by Collector. Here you can fine tune when your system gets triggered.

protected override void Execute(List<AudioEntity> entities) {
    entities.ForEach(entity => {
        MusicPlayer musicPlayer = entity.view.gameObject.GetComponent<MusicPlayer>();
        if (musicPlayer == null) {
            musicPlayer = entity.view.gameObject.AddComponent<MusicPlayer>();
        }
        musicPlayer.PlayTrack(entity.music.Filename);
    });
}

In Execute you will write your game logic, but make sure to write your tests first...

PlayMusicSystem goes through all the collected and filtered entities. Afterwards it fetches simple MonoBehaviour from the entity, or adds a new one.

At the end, it calls PlayTrack method. PlayTrack method loads AudioClip with filename parameter and assigns it to Unity's AudioSource component.

Using this approach we have created a modular audio system that we can reuse in our games. The system can play music and sound effects, all while having a full volume control over both music and sound FX.