Project Description

Published On: February 8th, 20230 Comments

For our final project in Rapid Game Development, we had to take two of the game genres we had previously made games in (pong, maze, racing, platformer, rpg combat, memory match, rhythm) and mash them together into a cohesive game. Seeking to atone for my less-than-stellar showing in the maze game category, I chose to combine that with the rpg combat genre to create an endless dungeon crawler in the vein of Dungeon Encounters or Darkest Dungeon. I’m quite proud of this project, and think it would make a neat mobile game with a bit more refinement.

Programming Notes

  • All combat actions such as Attack, Heal, Fire Magic, etc. inherit from a base CombatAction ScriptableObject class:
public abstract class CombatAction : ScriptableObject
{
    public string Name;
    public string ButtonName;
    public string CombatLogText;
    public string Description;
    [SerializeField] protected GameEvents gameEvents;
    
    public virtual IEnumerator DoAction(Unit origin, Unit target)
    {
        Debug.Log("Combat Action: " + this);
        
        gameEvents.UIUpdateEvent.Raise();
        yield return new WaitForSeconds(1f);
        
        gameEvents.EndTurnEvent.Raise();
        yield return new WaitForSeconds(0.25f);
    }

    protected virtual void DoPreAction(Unit origin, Unit target)
    {
        gameEvents.CombatMessageEvent.Raise(origin.Name + " " + CombatLogText + "!", 2f);
    }
}
  • These CombatActions can be overridden to create new behaviors:
[CreateAssetMenu (fileName = "CombatMagicAttack", menuName = "Combat Actions/Magic (Attack)", order = 51)]
public class CombatMagicAttack : CombatAction
{
    public int ManaCost;
    [SerializeField] private AudioClip audioHeal;
    [SerializeField] private GameObject castEffect;
    [SerializeField] private GameObject targetEffect;
    
    public override IEnumerator DoAction(Unit origin, Unit target)
    {
        DoPreAction(origin, target);
        origin.SetCast();
        if(castEffect) origin.PlayEffect(castEffect, false);
        
        // roll to determine heal amount
        var roll = Random.Range(2, 6) + Random.Range(2, 6) + Random.Range(2, 6) + Random.Range(2, 6);
        var damage = Mathf.FloorToInt(origin.Attack.Value / 4f) + roll;

        yield return new WaitForSeconds(.5f);
        
        if(audioHeal) gameEvents.PlaySoundEvent.Raise(audioHeal);
        if(targetEffect) target.PlayEffect(targetEffect, true);
        yield return new WaitForSeconds(.75f);
        
        origin.CurrentMana -= ManaCost;
        
        target.SetHurt();
        target.TakeDamage(damage);
        gameEvents.CombatMessageEvent.Raise(origin.Name + "'s " + Name + " does " + damage + " damage!", 1f);

        // call the end turn event
        yield return base.DoAction(origin, target);
    }
}
  • By using ScriptableObjects for these CombatActions, new abilities can be created through the Inspector:
  • GameEvents itself is a ScriptableObject class defining each of the ScriptableObject events, so I don’t need to assign them more than once in the Inspector:
[CreateAssetMenu (fileName = "GameEvents", menuName = "Game Events", order = 51)]
public class GameEvents : ScriptableObject
{
    [Header("Game Events")]
    [SerializeField] public GameEvent GameStartEvent;
    [SerializeField] public GameEvent MazeReadyEvent;
    [SerializeField] public GameEvent NextLevelEvent;
    [SerializeField] public GameEvent GameOverEvent;
    
    [Header("Combat Events")]
    [SerializeField] public GameEvent CombatEndEvent;
    [SerializeField] public GameEvent CombatStartEvent;
    [SerializeField] public GameEvent CombatSpawnEvent;
    [SerializeField] public GameEventInt PostCombatEvent;
    [SerializeField] public GameEvent TurnEndEvent;
    [SerializeField] public GameEvent PlayerTurnEvent;
    [SerializeField] public GameEvent EnemyTurnEvent;
    
    [Header("UI Events")]
    [SerializeField] public GameEvent UpdateUIEvent;
    [SerializeField] public GameEventStringFloat MazeMessageEvent;
    [SerializeField] public GameEventStringFloat CombatMessageEvent;
    [SerializeField] public GameEventAudio PlaySoundEvent;
    
    [Header("Stat Events")]
    [SerializeField] public GameEvent BoonPickupEvent;
    [SerializeField] public GameEventInt BattleWonEvent;
    [SerializeField] public GameEventInt TileExcavatedEvent;
}
  • Similar ScriptableObject TypeObjects are used to setup & spawn Units (player and enemy combatants) and Boons; in addition, the Unit has a sub-type for its AI, which defines which CombatActions are available. (ScriptableObjects are so, so great.) In later portfolio pieces, I’ll show how this sort of architecture lends itself well to Factory and Object Pooling strategies too. Here, this allows the enemy’s Character prefab (visual model) and base Unit prefab (logic) to be separated, letting me quickly set up around a half dozen different enemies in the final version:
  • The transition between Excavation mode and Combat is all done within the same Unity Scene by utilizing Sprite Sorting Layers and a simple combat background Sprite Renderer that lerps its transparency at the start and end of combat (plus locking/unlocking the player character’s movement).

Pitch Deck

DwarfDigger_PitchDeck

Instruction Booklet

DwarfDigger_Instructions

Download/Play

Project Images

Memory Melee
Random Survival

Leave a Reply

Share This: