Enemy States & Logging

I’ve been spending my time the past few weeks cleaning up various parts of the enemy code in Electric Noir. The feedback from the alpha made it clear that the enemies – though basically functional – needed a lot of improvement.

One of the biggest inconveniences of the codebase was the lack of a standardized way to handle enemy state. Enemies were just a collection of methods that would get called depending on which one the enemy code set into a pointer.

This worked, and probably would have worked well in a game that didn’t need a ton of shared enemy logic, but with so many enemies sharing similar logic it was obvious afterwards that this approach was going to cause me a lot of pain later on.

So I put together a pretty basic state machine where states are classes that can be created or inherited – to help share state code between enemies. It’s nothing spectacular, just enough to do what I need at the moment.

Each state implements the following interface.

public interface IState {   
    INoirLogger Log { get; set; }
    void BeginEnter(MonoBehaviour owner);
    void EndEnter();
    IEnumerator Execute();
    event EventHandler<StateBeginExitEventArgs> OnBeginExit;
    void EndExit();
    void OnDrawGizmos();
}

Each state has BeginEnter called when the state is being transitioned to, with EndEnter being called once the state is fully in control. After that Execute is called in a loop until it no longer yields or BeginExit is called with a new state. EndExit is called once the state is transitioned away from.

Having this simple standardized process around states lets me share more code across enemies – and other objects that can use the same states, like bosses, or companions. This will make creating new and interesting enemy archetypes much easier than before.

I’ll cover the OnDrawGizmos method in another post, but yep, each state can draw their own gizmos. The INoirLogger, as you’ve probably guessed is my custom logger, which I’ll talk about next.

I needed to start collecting logs so I could request them from folks that encountered issues. A lot of players reported issues where the dialogue system was broken for them, or a cutscene didn’t play right (I’ll cover more on those in another post).

Having some logs to look at would help a lot in tracking down those issues. Right now all I have to go on is whatever the player typed up in their feedback, which isn’t a lot.

Logging was a bit harder to get into the game, mostly because I wanted logging to be able to be added easily to any object that wanted it, and to have it be performant enough that it could handle being called in an update loop during development without crushing the game performance.

After a while I ended up with a logging system I’m pretty happy with, it supports logging levels, which can be overridden by component, and it’s initialized automatically at the start of the game and added to my base MonoBehaviour, so it can be used anywhere I need it.

I’ve included a small sample below showing the debug logs for an enemy switching from their EnemyIdle state to EnemyAttack. Another bonus of the logger is that it automatically includes the frame number in debug mode.

2019-09-29 00:14:55 DEBUG [EnemyIdle] frame=1868 ChangeStateImmediate newState=EN.Enemies.States.EnemyAttack transition=NoopTransition
2019-09-29 00:14:55 DEBUG [StateMachine] frame=1868 HandleStateBeginExit nextState=EnemyAttack transition=NoopTransition
2019-09-29 00:14:55 DEBUG [StateMachine] frame=1869 Execute done. Waiting for transition state=EnemyIdle
2019-09-29 00:14:55 DEBUG [StateMachine] frame=1869 Execute running transition Exit() state=EnemyIdle
2019-09-29 00:14:55 DEBUG [StateMachine] frame=1870 Execute switching to new state state=EnemyAttack prevState=EnemyIdle
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1870 choosing next attack
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1870 attackType=Quick
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1870 move=Quick Jab hDmg=120 wDmg=80
2019-09-29 00:14:55 DEBUG [StateMachine] frame=1870 Execute running transition Enter() state=EnemyAttack
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1871 SetupAnimation creating AnimatorOverrideController
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1871 SetupAnimation overrides=3
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1871 SetupAnimation Attack=TestEnemyQuickJab
2019-09-29 00:14:55 DEBUG [StateMachine] frame=1871 Execute running state state=EnemyAttack
2019-09-29 00:14:55 DEBUG [EnemyAttack] frame=1871 Execute playing animationState=Attack

The format of the log is {timestamp} {loglevel} [{component}] {debuginfo} {logmessage}. One of the more important parts of logging is that you want to keep like information lined up as much as possible. It makes scanning the log file a lot easier.

I’m definitely happy with these two new improvements to the game, I’m going to be sharing a few more code and internal system updates in the coming weeks as I work through the feedback from the alpha.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s