Diablo Simulator: The Great Refactoring

Over-scoping and over-engineering were two things I wanted to avoid with this project. With the lack of a strict deadline, the scope is still manageable, given that I’m not having to worry about things like physics and (for the most part) graphics. That said, I definitely underestimated the amount of organization I needed on the back-end to support future additions.

In this post, my aim is to show the evolution of the various systems in the game. This will be done primarily through examining the GameManager class, for reasons which should become obvious shortly.

First, to provide some context, here’s a GIF that shows nearly all of the user-facing features present in the game since before the engine was refactored:

Present in the pre-refactor build are the following features:

  • Character creation, multiple character classes
  • Randomly chosen monsters
  • Randomly chosen exploration events
  • Basic attacks for player characters and monsters
  • Equippable items, potions
  • Modifiable stats for player characters, monsters, and items
  • Experience and leveling up for player characters
  • Level affects stats for player characters, monsters, items
  • Saving and loading of player character data

Now it’s time to take a look at the primary reason for refactoring the engine. I say “engine” now because by this point I’ve had to write a significant number of systems (including an event manager) that I wouldn’t normally need to write if I was working in something like Unity or Unreal.

Here’s the pre-refactor version of the GameManager class:

//---------------------------------------------------------
//
// File Name:	GameManager.cs
// Author(s):	Jeremy Kings
// Project:     DiabloSimulator
// Date:        3 December 2019
//---------------------------------------------------------

using DiabloSimulator.Game.Factories;
using DiabloSimulator.Game.World;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;

namespace DiabloSimulator.Game
{
  //---------------------------------------------------------
  // Public Structures:
  //---------------------------------------------------------

  public class GameManager
  {
    //---------------------------------------------------------
    // Public Functions:
    //---------------------------------------------------------

    #region constructors

    public GameManager();

    #endregion

    #region gameFunctions

    public PlayerActionResult GetActionResult(PlayerAction action);
    public Hero Hero;
    public Monster Monster;
    public bool InCombat;
    public int Turns;

    #endregion

    #region saveLoad

    public void SaveState();
    public void LoadState(string saveFileName);
    public List<string> SavedCharacters;
    public bool CanLoadState;

    #endregion

    //---------------------------------------------------------
    // Private Structures
    //---------------------------------------------------------

    private delegate void ActionFunction(List<string> args);

    //---------------------------------------------------------
    // Private Functions:
    //---------------------------------------------------------

    #region playerActions

    private void Look(List<string> args);
    private void Explore(List<string> args);
    private void Attack(List<string> args);
    private void Defend(List<string> args);
    private void Rest(List<string> args);
    private void Flee(List<string> args);
    private void TownPortal(List<string> args);
    private void Proceed(List<string> args);
    private void Back(List<string> args);

    #endregion

    #region heroFunctions

    public void CreateHero();
    private void HeroLifeRegen();

    #endregion

    #region monsterFunctions

    public void DestroyMonster();
    public string DamageMonster(float amount);

    #endregion

    #region zoneFunctions

    private void ProcessWorldEvent(WorldEvent worldEvent);
    private void SetZone(string name);

    #endregion

    #region gameStateFunctions

    private void AdvanceTime();
    private void GameOver();

    #endregion

    //---------------------------------------------------------
    // Public Variables:
    //---------------------------------------------------------

    // Pseudo-constants
    public static PlayerChoiceText exploreChoiceText;
    public static PlayerChoiceText combatChoiceText;
    public static PlayerChoiceText discoverChoiceText;
    public static PlayerChoiceText yesNoChoiceText;

    //---------------------------------------------------------
    // Private Variables:
    //---------------------------------------------------------

    // Game state
    private bool inCombat;
    private int turns;
    private StringWriter nextEvent;
    private List<string> savedCharacters;
    private string saveLocation;
    private PlayerChoiceText currentChoiceText;

    // Actors
    private Monster monster;
    private Hero hero;
    private WorldZone zone;
    private string nextZoneName;

    // Factories
    private MonsterFactory monsterFactory;
    private HeroFactory heroFactory;
    private ItemFactory itemFactory;
    private WorldZoneFactory zoneFactory;

    // Internal data
    private Dictionary<PlayerActionType, ActionFunction> actionFunctions;
    private Random random;
  }
}

For the sake of brevity, I’ve omitted the definitions of the properties and methods. Even without them, however, it should be apparent that there is way too much stuff going on in this class. Or rather, more importantly, it has too many disparate responsibilities. In this iteration, it was managing:

  1. The Hero (player class)
  2. Monsters
  3. The current zone
  4. Player actions sent from the UI
  5. Available player choices in the exploration UI control
  6. World event text
  7. Game state such as whether the player is in combat
  8. Time/turn advancement

While these things are related in the sense that they interact with each other in the game, they definitely shouldn’t all be managed by the same system. Having realized this, I knew I was going to have to reorganize most of the game’s back-end. After all, splitting this system up into multiple smaller systems requires that the new systems have a robust way of communicating with each other.

Prior to the re-factor, there was no system for managing nor retrieving other systems. To facilitate this, I created a static EngineCore class, which handles just that. Once EngineCore was in place, breaking the GameManager up into several smaller subsystems was relatively straight-forward.

Here’s the second version of GameManager, post-EngineCore implementation:

//---------------------------------------------------------
//
// File Name:	GameManager.cs
// Author(s):	Jeremy Kings
// Project:     DiabloSimulator
// Date:        6 December 2019
//
//---------------------------------------------------------

using DiabloSimulator.Engine;
using DiabloSimulator.Game.World;
using System;
using System.Collections.Generic;
using System.Windows;

namespace DiabloSimulator.Game
{
  //---------------------------------------------------------
  // Public Structures:
  //---------------------------------------------------------

  public class GameManager : IModule
  {
    //---------------------------------------------------------
    // Public Functions:
    //---------------------------------------------------------

    #region constructors

    public GameManager();
    public void Inintialize();

    #endregion

    #region gameFunctions

    public PlayerActionResult GetActionResult(PlayerAction action);
    public bool InCombat;
    public int Turns;
    public PlayerChoiceText CurrentChoiceText;

    #endregion

    //---------------------------------------------------------
    // Private Structures
    //---------------------------------------------------------

    private delegate void ActionFunction(List<string> args);

    //---------------------------------------------------------
    // Private Functions:
    //---------------------------------------------------------

    #region playerActions

    private void Look(List<string> args);
    private void Explore(List<string> args);
    private void Attack(List<string> args);
    private void Defend(List<string> args);
    private void Rest(List<string> args);
    private void Flee(List<string> args);
    private void TownPortal(List<string> args);
    private void Proceed(List<string> args);
    private void Back(List<string> args);

    #endregion

    #region gameStateFunctions

    private void AdvanceTime();
    private void GameOver();
	 
    #endregion

    //---------------------------------------------------------
    // Public Variables:
    //---------------------------------------------------------

    // Pseudo-constants
    public static PlayerChoiceText exploreChoiceText;
    public static PlayerChoiceText combatChoiceText;
    public static PlayerChoiceText discoverChoiceText;
    public static PlayerChoiceText yesNoChoiceText;

    //---------------------------------------------------------
    // Private Variables:
    //---------------------------------------------------------

    // Game state
    private bool inCombat;

    // Internal data
    private Dictionary<PlayerActionType, ActionFunction> actionFunctions;
    private Random random;

    // Module references
    WorldEventManager eventManager;
    MonsterManager monsterManager;
    HeroManager heroManager;
    AudioManager audioManager;
    ZoneManager zoneManager;
  }
}

The purpose of the GameManager class is much more coherent in this version – handling available player choices and actions, and game state – though it could still use some more refining.

The elephant in the room in this version is all of GameManager‘s strong dependencies. (See the “module references” section of the private variables at the bottom.) We split things up, but this class still depends on all of he pieces we took out of it. It would be much better if those particular modules could handle their corresponding parts of the player actions without having to give the GameManager direct access to so many of their internals (or to the systems themselves in the first place).

To address this issue, I implemented an event/messaging system. This required a little more work than I was hoping, as C#’s events aren’t really designed to propagate across a chain of objects (with the exception of UI events in WPF), but nothing I haven’t already done before.

Here’s the current version of the GameManager class (as of the date of this post), written after the implementation of the event system. I’ve included some code from its Initialize function to show how registering event handlers and raising events works with my event system:

//---------------------------------------------------------
//
// File Name:	GameManager.cs
// Author(s):	Jeremy Kings
// Project:     DiabloSimulator
// Date:        8 December 2019
//
//---------------------------------------------------------

using System;
using DiabloSimulator.Engine;

namespace DiabloSimulator.Game
{
  //---------------------------------------------------------
  // Public Structures:
  //---------------------------------------------------------

  public class GameManager : IModule
  {
    //---------------------------------------------------------
    // Public Functions:
    //---------------------------------------------------------

    #region constructors

    public override void Inintialize()
    {
      // Register for player actions
      AddEventHandler(GameEvents.PlayerProceed, OnPlayerProceed);
      AddEventHandler(GameEvents.PlayerBack, OnPlayerBack);

      // Register for monster/hero actions
      AddEventHandler(GameEvents.MonsterDead, OnMonsterDead);
      AddEventHandler(GameEvents.HeroDead, OnHeroDead);
      AddEventHandler(GameEvents.AdvanceTime, OnAdvanceTime);

      // Register for world events
      AddEventHandler(GameEvents.WorldMonster, OnWorldMonster);
      AddEventHandler(GameEvents.WorldZoneDiscovery, OnWorldZoneDiscovery);

      // Load audio shtuff
      RaiseGameEvent(GameEvents.LoadAudioBank, this, "Master.strings");
      RaiseGameEvent(GameEvents.LoadAudioBank, this, "Master");
      RaiseGameEvent(GameEvents.LoadAudioBank, this, "Music");
    }

    #endregion

    #region gameFunctions

    public bool InCombat;
    public int Turns;
    public PlayerChoiceText CurrentChoiceText;

    #endregion

    //---------------------------------------------------------
    // Private Functions:
    //---------------------------------------------------------

    #region playerActions

    private void OnPlayerProceed(object sender, GameEventArgs e);
    private void OnPlayerBack(object sender, GameEventArgs e);

    #endregion

    #region monsterAndHeroActions

    private void OnMonsterDead(object sender, GameEventArgs e);
    private void OnHeroDead(object sender, GameEventArgs e);
    private void OnWorldMonster(object sender, GameEventArgs e);

    #endregion

    #region gameStateFunctions

    private void OnAdvanceTime(object sender, GameEventArgs e);
    private void OnWorldZoneDiscovery(object sender, GameEventArgs e);

    #endregion

    //---------------------------------------------------------
    // Public Variables:
    //---------------------------------------------------------

    // Pseudo-constants
    public static PlayerChoiceText exploreChoiceText;
    public static PlayerChoiceText combatChoiceText;
    public static PlayerChoiceText discoverChoiceText;
    public static PlayerChoiceText yesNoChoiceText;

    //---------------------------------------------------------
    // Private Variables:
    //---------------------------------------------------------

    // Game state
    private bool inCombat;
  }
}

In this version of the GameManager, dependencies on other systems have been more or less eliminated. It receives many events from other places, but rarely if ever needs a handle to any of those senders to do its job. This means systems can be added or removed from the engine at will without needing to add or remove references to those systems in other files, making for a much less brittle codebase overall.

With the major refactoring out of the way, the way has been paved for extending existing gameplay features and adding new ones. Expect another post once I’ve made tangible progress on that front!

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 )

Facebook photo

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

Connecting to %s