D20 RPG – Events

In this lesson we will examine how our dependencies can observe and invoke events.

Overview

Sometimes a dependency will need to interact with other dependencies. In many cases this will be an explicit interaction such as when one system needs to directly manage the interplay of actions and reactions with their various exceptions to the rules. In other cases, the dependency will merely need to know that its state, or an action it takes, should be “observable”. The observation should be one without care of who the observers are, what they do, or in what order they respond. This second case will be the focus of this lesson.

There are many ways to implement the observer pattern, though for this project I have decided to stick with a standard C# event. If you have followed along with my other projects and wonder why I am not using my NotificationCenter, it isn’t because I feel that the old pattern was “bad”. I do have a minor complaint in that I don’t like having to pass the “sender” and “info” arguments as Object, and would rather they kept their type information intact.

If I add an event to one of my interface-injected systems, and want to observe the event from another interface-injected system, then I either would have to make sure they were injected in a particular order (I don’t like this option), or I need a way to handle their life cycle. For a basic idea of what I mean, Unity handles the life cycle of its MonoBehaviour scripts with calls like “Awake” and “Start”. When a scene first loads, you can have confidence that all scripts have already had “Awake” called before any of them have “Start” called, so you have a convenient way to make your components work together without worrying about initialization order.

This lesson will provide some structure so that our dependencies have a lifecycle pattern, and then will introduce our first event – one for when an Entity is Destroyed. We will use this as an opportunity to cleanup unused game data after our encounter ends, because we will no longer need the monsters we had instantiated for the combat.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Dependency

Let’s get started by expanding our IDependency interface. Open its script and add the following default interface methods:

public void SetUp()
{
    //Debug.Log("SetUp " + this.GetType().Name);
}

public void TearDown()
{
    //Debug.Log("TearDown " + this.GetType().Name);
}

You can read more about default interface methods here. For a quick idea of what they are in my own words, you can think of them as a way to provide a “default” interface conformance for anything that doesn’t want or need to implement that part of the interface. It could be because you think those methods are “optional” or maybe because you would want to use the same implementation in most if not all cases. If a class that implements the interface also defines the same conforming method, then it will use the class version rather than the default version.

I have added two lifecycle methods with naming based on some of the lifecycle methods you’ve seen in our unit tests. They are “SetUp” and “TearDown” – and I chose them because I didn’t want any conflicts with other options like “Start” which would appear in a MonoBehaviour based dependency. The “SetUp” method is intended to be invoked after our Injector has completed injecting dependencies. In other words, you should expect that all needed dependencies already exist. The “TearDown” method would be called when the dependency is no longer needed. In this case, that would be when the app is being destroyed.

In the snippets above, I left some commented-out logs that will print the name of the dependency that is having a life cycle method invoked. You may wish to enable those lines if you want to verify your work, or to test how default implementation vs more specific implementations work.

Injector

I decided to add extra methods to all of the various injector classes, one for “SetUp” and one for “TearDown”. The idea is that for any dependency the injector is responsible for registering, that it will also be responsible for calling its life cycle methods.

Open the Injector class and add the following:

public static void SetUp()
{
    ActionInjector.SetUp();
    AssetManagerInjector.SetUp();
    CombatInjector.SetUp();
    ComponentInjector.SetUp();
    DataInjector.SetUp();
    DiceRollInjector.SetUp();
    EntityInjector.SetUp();
    FlowInjector.SetUp();
    IGameSystem.Resolve().SetUp();
    IInputSystem.Resolve().SetUp();
    SoloAdventureInjector.SetUp();
}

public static void TearDown()
{
    ActionInjector.TearDown();
    AssetManagerInjector.TearDown();
    CombatInjector.TearDown();
    ComponentInjector.TearDown();
    DataInjector.TearDown();
    DiceRollInjector.TearDown();
    EntityInjector.TearDown();
    FlowInjector.TearDown();
    IGameSystem.Resolve().TearDown();
    IInputSystem.Resolve().TearDown();
    SoloAdventureInjector.TearDown();
}

Then follow the pattern by adding the relevant methods to our other injectors. This may be a bit tedious – it really highlights just how many systems I have created so far, but I think it is worth doing per system even if we don’t “yet” have a specific need for the life cycle methods. I would rather it be standard practice to just include them, because we may add special handling in the future, or even swap out the registered dependency, and you won’t know if the new dependency needs those calls to be made. You can always grab the completed project at the end of this lesson to help speed things along. If you want to skip, but also read the important bits, then scroll down to the heading for “App Flow”.

AbilityScoreInjector

public static void SetUp()
{
    IAbilityScoreSystem.Resolve().SetUp();
    ICharismaSystem.Resolve().SetUp();
    IConstitutionSystem.Resolve().SetUp();
    IDexteritySystem.Resolve().SetUp();
    IIntelligenceSystem.Resolve().SetUp();
    IStrengthSystem.Resolve().SetUp();
    IWisdomSystem.Resolve().SetUp();
}

public static void TearDown()
{
    IAbilityScoreSystem.Resolve().TearDown();
    ICharismaSystem.Resolve().TearDown();
    IConstitutionSystem.Resolve().TearDown();
    IDexteritySystem.Resolve().TearDown();
    IIntelligenceSystem.Resolve().TearDown();
    IStrengthSystem.Resolve().TearDown();
    IWisdomSystem.Resolve().TearDown();
}

ActionInjector

public static void SetUp()
{
    ICheckSystem.Resolve().SetUp();
}

public static void TearDown()
{
    ICheckSystem.Resolve().TearDown();
}

AssetManagerInjector

public static void SetUp()
{
    ICombatActionAssetSystem.Resolve().SetUp();
    IEncounterAssetSystem.Resolve().SetUp();
    IEntryAssetSystem.Resolve().SetUp();
}

public static void TearDown()
{
    ICombatActionAssetSystem.Resolve().TearDown();
    IEncounterAssetSystem.Resolve().TearDown();
    IEntryAssetSystem.Resolve().TearDown();
}

CombatActionsInjector

public static void SetUp()
{
    IAttackRollSystem.Resolve().SetUp();
    IStrideSystem.Resolve().SetUp();
}

public static void TearDown()
{
    IAttackRollSystem.Resolve().TearDown();
    IStrideSystem.Resolve().TearDown();
}

CombatInjector

public static void SetUp()
{
    CombatActionsInjector.SetUp();
    ICombatantSystem.Resolve().SetUp();
    ICombatResultSystem.Resolve().SetUp();
    DamageInjector.SetUp();
    IRoundSystem.Resolve().SetUp();
    ITurnSystem.Resolve().SetUp();
}

public static void TearDown()
{
    CombatActionsInjector.TearDown();
    ICombatantSystem.Resolve().TearDown();
    ICombatResultSystem.Resolve().TearDown();
    DamageInjector.TearDown();
    IRoundSystem.Resolve().TearDown();
    ITurnSystem.Resolve().TearDown();
}

ComponentInjector

public static void SetUp()
{
    AbilityScoreInjector.SetUp();
    IAdventureItemSystem.Resolve().SetUp();
    IArmorClassSystem.Resolve().SetUp();
    ICombatantSystem.Resolve().SetUp();
    IDyingSystem.Resolve().SetUp();
    HealthInjector.SetUp();
    ILevelSystem.Resolve().SetUp();
    INameSystem.Resolve().SetUp();
    IPartySystem.Resolve().SetUp();
    IPositionSystem.Resolve().SetUp();
    SkillsInjector.SetUp();
}

public static void TearDown()
{
    AbilityScoreInjector.TearDown();
    IAdventureItemSystem.Resolve().TearDown();
    IArmorClassSystem.Resolve().TearDown();
    ICombatantSystem.Resolve().TearDown();
    IDyingSystem.Resolve().TearDown();
    HealthInjector.TearDown();
    ILevelSystem.Resolve().TearDown();
    INameSystem.Resolve().TearDown();
    IPartySystem.Resolve().TearDown();
    IPositionSystem.Resolve().TearDown();
    SkillsInjector.TearDown();
}

DamageInjector

public static void SetUp()
{
    IDamageSystem.Resolve().SetUp();
    IDamageImmunitySystem.Resolve().SetUp();
    IDamageWeaknessSystem.Resolve().SetUp();
    IDamageResistanceSystem.Resolve().SetUp();
    IDamageResistanceExceptionSystem.Resolve().SetUp();
}

public static void TearDown()
{
    IDamageSystem.Resolve().TearDown();
    IDamageImmunitySystem.Resolve().TearDown();
    IDamageWeaknessSystem.Resolve().TearDown();
    IDamageResistanceSystem.Resolve().TearDown();
    IDamageResistanceExceptionSystem.Resolve().TearDown();
}

DataInjector

public static void SetUp()
{
    IDataSerializer.Resolve().SetUp();
    IDataStore.Resolve().SetUp();
    IDataSystem.Resolve().SetUp();
}

public static void TearDown()
{
    IDataSerializer.Resolve().TearDown();
    IDataStore.Resolve().TearDown();
    IDataSystem.Resolve().TearDown();
}

DiceRollInjector

public static void SetUp()
{
    IDiceRollSystem.Resolve().SetUp();
    IRandomNumberGenerator.Resolve().SetUp();
}

public static void TearDown()
{
    IDiceRollSystem.Resolve().TearDown();
    IRandomNumberGenerator.Resolve().TearDown();
}

EntityInjector

public static void SetUp()
{
    IEntitySystem.Resolve().SetUp();
    IEntityRecipeSystem.Resolve().SetUp();
}

public static void TearDown()
{
    IEntitySystem.Resolve().TearDown();
    IEntityRecipeSystem.Resolve().TearDown();
}

FlowInjector

public static void SetUp()
{
    ICombatFlow.Resolve().SetUp();
    IEncounterFlow.Resolve().SetUp();
    IEntryFlow.Resolve().SetUp();
    IGameFlow.Resolve().SetUp();
    IHeroActionFlow.Resolve().SetUp();
    IMainMenuFlow.Resolve().SetUp();
    IMonsterActionFlow.Resolve().SetUp();
    IRoundFlow.Resolve().SetUp();
    ITurnFlow.Resolve().SetUp();
}

public static void TearDown()
{
    ICombatFlow.Resolve().TearDown();
    IEncounterFlow.Resolve().TearDown();
    IEntryFlow.Resolve().TearDown();
    IGameFlow.Resolve().TearDown();
    IHeroActionFlow.Resolve().TearDown();
    IMainMenuFlow.Resolve().TearDown();
    IMonsterActionFlow.Resolve().TearDown();
    IRoundFlow.Resolve().TearDown();
    ITurnFlow.Resolve().TearDown();
}

HealthInjector

public static void SetUp()
{
    IHealthSystem.Resolve().SetUp();
    IHitPointSystem.Resolve().SetUp();
    IMaxHitPointSystem.Resolve().SetUp();
}

public static void TearDown()
{
    IHealthSystem.Resolve().TearDown();
    IHitPointSystem.Resolve().TearDown();
    IMaxHitPointSystem.Resolve().TearDown();
}

SkillsInjector

public static void SetUp()
{
    IAcrobaticsProficiencySystem.Resolve().SetUp();
    IAcrobaticsSystem.Resolve().SetUp();
    IArcanaProficiencySystem.Resolve().SetUp();
    IArcanaSystem.Resolve().SetUp();
    IAthleticsProficiencySystem.Resolve().SetUp();
    IAthleticsSystem.Resolve().SetUp();
    ICraftingProficiencySystem.Resolve().SetUp();
    ICraftingSystem.Resolve().SetUp();
    IDeceptionProficiencySystem.Resolve().SetUp();
    IDeceptionSystem.Resolve().SetUp();
    IDiplomacyProficiencySystem.Resolve().SetUp();
    IDiplomacySystem.Resolve().SetUp();
    IIntimidationProficiencySystem.Resolve().SetUp();
    IIntimidationSystem.Resolve().SetUp();
    ILoreProficiencySystem.Resolve().SetUp();
    ILoreSystem.Resolve().SetUp();
    IMedicineProficiencySystem.Resolve().SetUp();
    IMedicineSystem.Resolve().SetUp();
    INatureProficiencySystem.Resolve().SetUp();
    INatureSystem.Resolve().SetUp();
    IOccultismProficiencySystem.Resolve().SetUp();
    IOccultismSystem.Resolve().SetUp();
    IPerformanceProficiencySystem.Resolve().SetUp();
    IPerformanceSystem.Resolve().SetUp();
    IProficiencySystem.Resolve().SetUp();
    IReligionProficiencySystem.Resolve().SetUp();
    IReligionSystem.Resolve().SetUp();
    ISkillSystem.Resolve().SetUp();
    ISocietyProficiencySystem.Resolve().SetUp();
    ISocietySystem.Resolve().SetUp();
    IStealthProficiencySystem.Resolve().SetUp();
    IStealthSystem.Resolve().SetUp();
    ISurvivalProficiencySystem.Resolve().SetUp();
    ISurvivalSystem.Resolve().SetUp();
    IThieveryProficiencySystem.Resolve().SetUp();
    IThieverySystem.Resolve().SetUp();
}

public static void TearDown()
{
    IAcrobaticsProficiencySystem.Resolve().TearDown();
    IAcrobaticsSystem.Resolve().TearDown();
    IArcanaProficiencySystem.Resolve().TearDown();
    IArcanaSystem.Resolve().TearDown();
    IAthleticsProficiencySystem.Resolve().TearDown();
    IAthleticsSystem.Resolve().TearDown();
    ICraftingProficiencySystem.Resolve().TearDown();
    ICraftingSystem.Resolve().TearDown();
    IDeceptionProficiencySystem.Resolve().TearDown();
    IDeceptionSystem.Resolve().TearDown();
    IDiplomacyProficiencySystem.Resolve().TearDown();
    IDiplomacySystem.Resolve().TearDown();
    IIntimidationProficiencySystem.Resolve().TearDown();
    IIntimidationSystem.Resolve().TearDown();
    ILoreProficiencySystem.Resolve().TearDown();
    ILoreSystem.Resolve().TearDown();
    IMedicineProficiencySystem.Resolve().TearDown();
    IMedicineSystem.Resolve().TearDown();
    INatureProficiencySystem.Resolve().TearDown();
    INatureSystem.Resolve().TearDown();
    IOccultismProficiencySystem.Resolve().TearDown();
    IOccultismSystem.Resolve().TearDown();
    IPerformanceProficiencySystem.Resolve().TearDown();
    IPerformanceSystem.Resolve().TearDown();
    IProficiencySystem.Resolve().TearDown();
    IReligionProficiencySystem.Resolve().TearDown();
    IReligionSystem.Resolve().TearDown();
    ISkillSystem.Resolve().TearDown();
    ISocietyProficiencySystem.Resolve().TearDown();
    ISocietySystem.Resolve().TearDown();
    IStealthProficiencySystem.Resolve().TearDown();
    IStealthSystem.Resolve().TearDown();
    ISurvivalProficiencySystem.Resolve().TearDown();
    ISurvivalSystem.Resolve().TearDown();
    IThieveryProficiencySystem.Resolve().TearDown();
    IThieverySystem.Resolve().TearDown();
}

SoloAdventureInjector

public static void SetUp()
{
    ICombatantAssetSystem.Resolve().SetUp();
    ICombatantViewSystem.Resolve().SetUp();
    IEncounterActionsSystem.Resolve().SetUp();
    IEncounterSystem.Resolve().SetUp();
    IEntrySystem.Resolve().SetUp();
    IPhysicsSystem.Resolve().SetUp();
    IPositionSelectionSystem.Resolve().SetUp();
    ISoloHeroSystem.Resolve().SetUp();
}

public static void TearDown()
{
    ICombatantAssetSystem.Resolve().TearDown();
    ICombatantViewSystem.Resolve().TearDown();
    IEncounterActionsSystem.Resolve().TearDown();
    IEncounterSystem.Resolve().TearDown();
    IEntrySystem.Resolve().TearDown();
    IPhysicsSystem.Resolve().TearDown();
    IPositionSelectionSystem.Resolve().TearDown();
    ISoloHeroSystem.Resolve().TearDown();
}

App Flow

Now that our injectors manage the life cycle events of our dependencies, we need to actually invoke those methods. We will use the AppFlow for that purpose. Go ahead and open that script and then immediately after having the Injector run its Inject, we will have it run its SetUp:

Injector.SetUp();

Because the dependencies I have injected at the AppFlow level will all live for the lifetime of the app, the TearDown won’t really make a difference. But, for the sake of a thorough example you could add something like this (since our AppFlow is a MonoBehaviour):

private void OnDestroy()
{
    Injector.TearDown();
}

Encounter Cleanup

When an encounter begins, we dynamically create a new Entity or Entities (the monsters in the combat) and each Entity will have plenty of associated data such as ability scores, hit points, etc. When the “battle” ends you may still want those entities around so you can determine things like how much experience to award. By the time we are exiting the encounter flow completely, there is no longer any need to keep those entities or their data around. If we don’t delete them, then over time our Game Data save file will grow to undesirable sizes.

The Entity System is responsible for destroying an Entity, but I don’t want it to explicitly talk to EVERY other system that holds data related to an Entity so that those systems can perform their cleanup. For example, there is no reason at all that the Entity System needs to know that a Hit Point System exists, and there is no reason at all that it would care if an Entities Ability Scores were deleted before or after its Hit Points. This is an ideal setup for an event. The Entity System is doing something that it knows other systems will need to know about, and so it can invoke the event, and then as far as it is concerned, its job is complete.

Entity System

Let’s start the cleanup process within the EntitySystem script. Go ahead and open that script and add the following event definition to the IEntitySystem interface:

event Action<Entity> EntityDestroyed;

Note that for the use of the Action above we will need another using statement:

using System;

To maintain the interface conformance we will also need to add an event to our EntitySystem class:

public event Action<Entity> EntityDestroyed;

Finally, add the following to the end of the EntitySystem‘s Destroy method:

EntityDestroyed?.Invoke(entity);

Now, anytime we use the system to destroy an Entity, we invoke an observable event that it has occurred. Any number of other systems can be watching for this to happen and act based on the new information.

Entity Table System

Most of our systems that manage an Entity’s data are a subclass of the EntityTableSystem. Go ahead and open that file and add the following:

public virtual void SetUp()
{
    IEntitySystem.Resolve().EntityDestroyed += OnEntityDestroyed;
}

public virtual void TearDown()
{
    IEntitySystem.Resolve().EntityDestroyed -= OnEntityDestroyed;
}

protected virtual void OnEntityDestroyed(Entity entity)
{
    Remove(entity);
}

I start by adding custom implementation of the dependency lifecycle methods “SetUp” and “TearDown”. This means that for this class and any of its subclasses that it will now use these methods rather than the default ones specified in the interface. Here I use it as an opportunity to handle adding/removing an observation handler to/from the entity system’s event. The handler merely needs to call “Remove” using the entity which was passed in as a parameter.

For the “observation handler” I decided to create a protected virtual method. This means that any outside class won’t know this method exists and can’t call it directly. However, a subclass will know about the method and can choose to override it, if needed, to do things beyond just deleting its own data. For example, I might have some sort of equipment system that maps from one entity to another, such as a Goblin holding a sword. If I delete the Goblin, then I may also wish to delete the sword, and not merely “disconnect” the two.

Entity Set System

Now open the EntitySetSystem and give it the same treatment as our EntityTableSystem:

public virtual void SetUp()
{
    IEntitySystem.Resolve().EntityDestroyed += OnEntityDestroyed;
}

public virtual void TearDown()
{
    IEntitySystem.Resolve().EntityDestroyed -= OnEntityDestroyed;
}

protected virtual void OnEntityDestroyed(Entity entity)
{
    Remove(entity);
}

Encounter Flow

Now it’s time to trigger the encounter cleanup. We know we want it to happen as we are leaving the encounter, so we will use the appropriate flow. Open the EncounterFlow script and add the following:

using System.Collections.Generic;

We need that using statement to work with a generic List in the next snippet:

void DeleteMonsters()
{
    var system = IEntitySystem.Resolve();
    var table = new List<Entity>(ICombatantSystem.Resolve().Table);
    foreach (var entity in table)
        if (entity.Party == Party.Monster)
            system.Destroy(entity);
}

This method starts by grabbing a reference to the registered EntitySystem, which it will use to delete monsters that are no longer needed. Next, it creates a copy of the CombatantSystem‘s Table. This is necessary because the CombatantSystem is a subclass of the EntitySetSystem, and so as we start deleting monster’s its own Table will be modified – you can’t modify a collection while enumerating it. Finally we enumerate the copied table, and for any entity that has a Party type of Monster, we will call Destroy.

We will invoke this new method from within the Exit method, just before the last line that awaits a completed task:

DeleteMonsters();

Demo

At this point I would recommend that you validate that everything is working. You can do that with breakpoints, logs, etc, whatever you are most comfortable with.

For an example of what I did (and keep in mind that this is temp code that should be deleted after your test) was to open the DataSerializer script and change the Serialize method to look like this:

public string Serialize(Data data)
{
    var result = JsonUtility.ToJson(data);
    Debug.Log(result);
    return result;
}

This will cause the JSON representation of your game data to be displayed in the Console every time your Game saves its data. Next I will copy the data (just the json, not the other info) and paste it into a resource like jsonlint. Just paste the data there, click the “Validate JSON” button, and enjoy a much easier to read version of the data – you can even use the interface to collapse groups of data that you aren’t interested in. It’s a handy tool!

Summary

In this lesson we started making use of the observer pattern with our first event. In order to facilitate our dependencies working together without worrying about an initialization order, we also setup a structure for some dependency life cycle events. This allows us to do “SetUp” of our systems at a time where we know all other systems have already been registered.

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

3 thoughts on “D20 RPG – Events

  1. Hi!

    I’ve been reading through this project (not quite following along because I’m still in the throes of another but I really do love reading these and seeing your different approaches) but I was curious about how you might handle the sort of bubbling of events or interceptions required in a project like this?

    In your notification center projects previously you would have something like a:

    WillPerformAction event where Exceptions could be passed and then a DidPerformAction for updating listeners afterwards. Will you be following a similar principal with these events here or will it be different?

    1. Hi, I’m glad you are enjoying these. I don’t have anything set in stone yet – I thought my previous projects had a lot to think about, but the scope of this one is even bigger. I know that the architecture around actions will need to evolve over time, and I will need to get a better feel for the direction as I start implementing more of them. There is probably a good chance it will look like what you are describing, though I may opt for generic events over notifications so there is less to cast.

      1. Got’cha, well very excited to see. Was also curious to see if you’d be adapting the Feature paradigm from the Tactics project? I’m unsure how exactly that would function in ECS (Maybe a FeatureSystem that tracks what Features are on what entities?) I liked that paradigm a lot and have adapted it in a few other projects.

        I was hoping after I finish this project I’m working on some day I’d take another whack at a Tactical RPG as that’s my dream genre but was unfortunately beyond my skill level for a long time. I like the architecture of this project a lot, especially the dependency injection paradigm and was thinking about adapting this for a Tactical RPG down the line that’s why I was wondering about the event bubbling/interceptions and the “Feature” architecture as they were used heavily in your previous project.

Leave a Reply

Your email address will not be published. Required fields are marked *