Started in fall semester 2018
Code by Trevor Thacker
Art by Caethan Raley

Geldamin’s Vault is a work in progress game about item-based dungeon crawling and puzzle solving with 3 currently planned different characters and their stories based around a similar quest.
I have been using it as a learning experience and plan to refactor much of the code as I learn better and more efficient code and development skills.
The code and examples shown here show the progress I have made in learning C#, Unity, and general game design practices overall.

Overview Video of Geldamin’s Vault

Tool – Custom Sprite Animation Controller

Geldamin’s Vault is a 2D game. While Unity’s default animation controller is fantastic for 3D animations and smooth transitions, it is overly complex and has too much excess when needing to do simple sprite animations. It is also nearly impossible to change certain aspects of the animation, like exact animation length.

Because what I needed was not too complex, I decided to make my own animation controller for 2D sprite animations. It is robust enough for actual use, efficient, and makes use of state machines as well. It does lack a fancy UI to show off the state machines, but usually there aren’t enough animations to warrant that feature.

Inspector of an animator script
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Animator2D : MonoBehaviour
{
    public List<Animation2D> animations;
    private SpriteRenderer sp;
    private Image uiImage;
    private IEnumerator coroutine;
    public Animation2D playing;
    public bool stop;
    public int frameNo;
    private void Awake()
    {
        for(int i = 0; i < animations.Capacity; i++)
        {
            animations[i] = Instantiate(animations[i]);//Sets all the animations to copies
        }
        if(gameObject.GetComponent<SpriteRenderer>()) sp = gameObject.GetComponent<SpriteRenderer>();
        if (gameObject.GetComponent<Image>()) uiImage = gameObject.GetComponent<Image>();
        Play(animations[0]);//Plays the first animation at start
    }
    public Animation2D FindAni(string name)
    {
        return animations.Find((Animation2D a) => { return a.name == name; });
    }
    public bool Play(string name)
    {
        Animation2D ani = animations.Find((Animation2D a) => { return a.name == name; });
        if (playing == null || playing != ani && ((ani.priority > playing.priority && playing.uninterruptable) || (ani.priority >= playing.priority && !playing.uninterruptable)))
        {
            if (coroutine != null) StopCoroutine(coroutine);
            if (sp) coroutine = PlayAnimation(ani);
            if (uiImage) coroutine = PlayAnimationUI(ani);
            StartCoroutine(coroutine);
            return true;
        }
        return false;
    }
    public bool Play(Animation2D ani)
    {
        if (playing == null || playing != ani && ((ani.priority > playing.priority && playing.uninterruptable) || (ani.priority >= playing.priority && !playing.uninterruptable)))
        {
            if (coroutine != null) StopCoroutine(coroutine);
            if (sp) coroutine = PlayAnimation(ani);
            if (uiImage) coroutine = PlayAnimationUI(ani);
            StartCoroutine(coroutine);
            return true;
        }
        return false;
    }
    private IEnumerator PlayAnimation(Animation2D animation)
    {
        if (stop) stop = false;
        playing = animation;
        for (int i = 0; i < 1 || (animation.loop && !stop); i++)
        {
            frameNo = 0;
            for (int f = 0; f < animation.animation.Length; f++)
            {
                if(animation.frameFunction != null) animation.frameFunction(f);
                sp.sprite = animation.animation[f];
                frameNo++;
                yield return new WaitForSeconds(animation.length / animation.animation.Length);
            }
        }
        if (stop) stop = false;
        if (playing.next != "")
        {
            Animation2D ani = animations.Find((Animation2D a) => { return a.name == playing.next; });
            if (sp) coroutine = PlayAnimation(ani);
            if (uiImage) coroutine = PlayAnimationUI(ani);
            StartCoroutine(coroutine);
        }
    }
    private IEnumerator PlayAnimationUI(Animation2D animation)
    {
        if (stop) stop = false;
        playing = animation;
        for (int i = 0; i < 1 || (animation.loop && !stop); i++)
        {
        frameNo = 0;
            foreach (Sprite frame in animation.animation)
            {
                uiImage.sprite = frame;
                frameNo++;
                yield return new WaitForSeconds(animation.length / animation.animation.Length);
            }
        }
        if (stop) stop = false;
        if (playing.next != "")
        {
            Animation2D ani = animations.Find((Animation2D a) => { return a.name == playing.next; });
            if (sp) coroutine = PlayAnimation(ani);
            if (uiImage) coroutine = PlayAnimationUI(ani);
            StartCoroutine(coroutine);
        }
    }
}

This Scriptable Object is used to store the frames of a sprite animation, which can be easily done with sliced tiled images with a single drag and drop. Other animation information such as how long it takes to loop through and if it should loop can be set here and easily changed when the animation is being referenced to in other scripts if need be.

Inspector of an animation object
using System.Collections;
using UnityEngine;
[CreateAssetMenu(fileName = "Animation2D", menuName = "Animation2D", order = 1)]
public class Animation2D : ScriptableObject
{
    public Sprite[] animation;//Frames of the animation
    public float length = 1;//Time it takes for the animation to play
    public new string name;
    public bool loop;//Repeat animation?
    public int priority;//Animations with a higher priority can cancel ones below them
    public bool uninterruptable;//Is the animation uninturruptable by the same level of priority?
    public string next;//The animation that will play once this animation finishes if it's not looping
    public delegate void FrameFunction(int f);
    public FrameFunction frameFunction;
}

Modular Items

The game is based around being able to carry up to 3 active and 3 passive items of your choosing in order to solve puzzles and fight bosses. The sheer number of unique items we wanted to implement warranted a modular development system for them.

Inspector of an item
public class ItemBase : MonoBehaviour{
    public float cooldownValue;//Cooldown for the item
    public float cooldown;//Current cooldown
    public GameObject icon;//Object to show in the ItemHUD
    public bool passive;//Is the item a passive item? If false it is an active item
    public static int activeItemNo;
    public static int passiveItemNo;
    public int itemNo;
    private void Start()
    {
        Rect screen = Player.instance.HUD.GetComponent<RectTransform>().rect;
        float activeDim = screen.width / 24;
        float passiveDim = screen.width / 30;
        if (passive)
        {
            Player.instance.enablePassives += Passive;
            itemNo = passiveItemNo;
            Player.instance.passiveItems[itemNo] = gameObject;
            Passive();
            //Instantiate the HUD icon
            Instantiate(icon, transform.position, transform.rotation, Player.instance.HUD.transform).GetComponent<RectTransform>().anchoredPosition
                = new Vector2((passiveDim / screen.width) + screen.width + (passiveDim / 2) - (screen.width / 25) * (passiveItemNo + 1), activeDim + screen.height / 20);
            passiveItemNo++;
        }
        else
        {
            Player.instance.useActive += Activate;
            itemNo = activeItemNo;
            Player.instance.activeItems[itemNo] = gameObject;
            //Instantiate the HUD icon
            Instantiate(icon, transform.position, transform.rotation, Player.instance.HUD.transform).GetComponent<RectTransform>().anchoredPosition
                = new Vector2((activeDim / screen.width) + screen.width + (activeDim / 2) - (screen.width / 20) * (activeItemNo + 1), screen.height / 20);
            activeItemNo++;
        }
        Player.instance.unequipItems += Unequip;
    }
    protected void StartCooldown()//Call in the item to start the cooldown
    {
        if (Player.instance.activeCooldownBoxes[itemNo].GetComponent<Animator2D>() != null)
        {
            Player.instance.activeCooldownBoxes[itemNo].GetComponent<Animator2D>().FindAni("Cooldown").length = cooldownValue;//Cooldown animation must be the 3rd animation
            Player.instance.activeCooldownBoxes[itemNo].GetComponent<Animator2D>().Play("Cooldown");
        }
        cooldown = cooldownValue;
    }
    private void Update()//Lowers cooldown timers
    {
        if (cooldown > 0)
        {
            cooldown -= Time.deltaTime;
            if (cooldown < 0) cooldown = 0;
            
        }
    }
    private void Activate(int iNo)//Checks if this is the correct item and it is off cooldown
    {
        if (iNo == itemNo && cooldown == 0) Active();
    }
    public virtual void Active() { }
    protected virtual void Passive() { }
    protected virtual void Unequip() { }
}
public class Repulsor : ItemBase {
    public int speed;
    public override void Active()
    {
        StartCooldown();
        StartCoroutine(Push());
    }
    private IEnumerator Push()
    {
        GetComponent<BoxCollider2D>().enabled = true;
        yield return new WaitForSeconds(.5f);
        GetComponent<BoxCollider2D>().enabled = false;
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Block")
        {
            Vector2 force;
            if (collision.gameObject.GetComponent<Health>()) collision.gameObject.GetComponent<Health>().fallen = true;
            force = collision.transform.position - transform.position;
            if(Mathf.Abs(force.x) > Mathf.Abs(force.y))
            {
                if (force.x > 0) force = new Vector2(1,0);
                else force = new Vector2(-1, 0);
            }
            else
            {
                if(force.y > 0) force = new Vector2(0, 1);
                else force = new Vector2(0, -1);
            }
            collision.gameObject.GetComponent<Rigidbody2D>().velocity = force * speed;
            StartCoroutine(Force(collision, force * speed));
        }
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        collision.gameObject.GetComponent<Rigidbody2D>().velocity = new Vector2(0,0);
    }
    private IEnumerator Force(Collider2D collision, Vector2 force)//Constantly set velocity until the block hits something and stops
    {
        while(!collision.IsTouchingLayers(0) && !collision.IsTouchingLayers(10))//FIX!!!--------------------------------------------------------------------
        {
            print(collision.IsTouchingLayers(-1));
            collision.gameObject.GetComponent<Rigidbody2D>().velocity = force;
            yield return new WaitForEndOfFrame();
        }
        if (collision.gameObject.GetComponent<Health>()) collision.gameObject.GetComponent<Health>().fallen = false;
    }
}

Modular Enemies

Like items, we planned on having many different types of enemies with varying AIs and attack patterns. For this, I used inheritance to make an enemy superclass with all of their shared attributes.

public class EnemyBase : Health {
    
    public float moveSpeed;
    public float xsign;
    public float ysign;
    public Vector2 relative;
    protected Rigidbody2D rb2d;
    protected Animator2D animator;
    public bool AIEnabled = true;
    public bool triggered;
    private void Awake()
    {
        animator = GetComponent<Animator2D>();
        health = maxHealth;
        if(GetComponent<Rigidbody2D>()) rb2d = GetComponent<Rigidbody2D>();
    }
    protected void Postioning()
    {
        relative = Player.instance.transform.position - new Vector3(0, Player.instance.GetComponent<SpriteRenderer>().bounds.extents.y*2, 0) - transform.position + new Vector3(0, GetComponent<SpriteRenderer>().bounds.extents.y,0);
        xsign = ((Player.instance.transform.position.x - transform.position.x) / Mathf.Abs(Player.instance.transform.position.x - transform.position.x));
        ysign = ((Player.instance.transform.position.y - transform.position.y) / Mathf.Abs(Player.instance.transform.position.y - transform.position.y));
    }
    private new void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "PlayerDamage" && !triggered) Trigger();
        base.OnCollisionEnter2D(collision);
    }
    public virtual void Trigger() { }//Set what happens when an enemy is triggered in their AI 
}
public class GolemAI : EnemyBase
{
    void Update()
    {
        if (animator.playing.name == "Walk")
        {
            if (xsign > 0) gameObject.transform.localScale = new Vector3(-1, 1, 1);
            if (xsign < 0) gameObject.transform.localScale = new Vector3(1, 1, 1);
            Postioning();
            Move();
        }
    }
    protected new void Postioning()
    {
        BoxCollider2D slamBox = transform.Find("SlamTrigger").GetComponent<BoxCollider2D>();
        Vector2 slamCenter = slamBox.offset + (Vector2)gameObject.transform.position - new Vector2(0, slamBox.size.y / 2);
        relative = ((Vector2)Player.instance.transform.position - new Vector2(0, Player.instance.GetComponent<SpriteRenderer>().bounds.extents.y * 2)) - slamCenter;
        xsign = ((Player.instance.transform.position.x - transform.position.x) / Mathf.Abs(Player.instance.transform.position.x - transform.position.x));
        ysign = ((Player.instance.transform.position.y - transform.position.y) / Mathf.Abs(Player.instance.transform.position.y - transform.position.y));
    }
    private void Move()
    {
        rb2d.velocity = relative.normalized * moveSpeed;
    }
    protected override IEnumerator Death()
    {
        AIEnabled = false;
        animator.Play("Unanimate");
        yield return new WaitUntil(() => animator.playing.name == "Inanimate");
    }
    public override void Trigger()
    {
        animator.Play("Animate");
    }
}
Inspector of an enemy AI

Modular Enemy Attacks

Enemy attacks were also made to be as modular as possible, so that they could be created in a blank space and put onto enemies at will. These are monobehaviours rather than superior scriptable objects simply because they were created while I was learning.

public class AttackBase : MonoBehaviour {
    public string attackAni;
    public bool inRange;
    public float cooldownValue;
    public float cooldown;
    
    private void Start()
    {
        transform.parent.GetComponent<Animator2D>().FindAni(attackAni).frameFunction += Attack;
    }
    protected void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.tag == "Player" && inRange && cooldown == 0 && transform.parent.GetComponent<Animator2D>().playing.name != attackAni)
            if(transform.parent.GetComponent<Animator2D>().Play(attackAni)) cooldown = cooldownValue;
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag == "Player") inRange = true;
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.tag == "Player") inRange = false;
    }
    protected virtual void Attack(int f) { }//On frame f do something
    private void LateUpdate()//Cooldown stuff
    {
        if (cooldown > 0)
        {
            cooldown -= Time.deltaTime;
            if (cooldown < 0) cooldown = 0;
        }
    }
}
public class GolemThrow : AttackBase {
    public GameObject lem;
    private Vector2 target;
    protected override void Attack(int f)
    {
        if(f == 10)
        {
            target = (Player.instance.transform.position - transform.Find("Exit").position).normalized * 60;
        }
        if(f == 15)
        {
            
            GameObject thrownLem = Instantiate(lem, transform.Find("Exit").position, new Quaternion(0, 0, 0, 0));
            thrownLem.GetComponent<Rigidbody2D>().velocity = target;
            thrownLem.layer = 10; //Layer 10 is projectile
        }
    }
}
Fight against a Golem

Environment + Puzzle Logic Systems

These logic systems use chains of delegates to update the system when an input has changed.

public class Triggerable : MonoBehaviour
{
    public bool on;
    public delegate void Check();
    public Check check; 
}
public class InteractableAND : Triggerable {
    public GameObject[] required;
    private void Start()
    {
        foreach(GameObject x in required) x.GetComponent<Triggerable>().check += And;
    }
    private void And()
    {
        on = true;
        foreach (GameObject x in required) if (!x.GetComponent<Triggerable>().on) on = false;
        check();
    }
}
public class Door : MonoBehaviour {
    public GameObject activate;
    public Sprite open;
    public Sprite closed;
    private void Start()
    {
        activate.GetComponent<Triggerable>().check += Activate;
    }
    private void Activate()
    {
        if (activate.GetComponent<Triggerable>().on)
        {
            GetComponent<SpriteRenderer>().sprite = open;
            GetComponent<BoxCollider2D>().enabled = false;
        }
        else
        {
            GetComponent<SpriteRenderer>().sprite = closed;
            GetComponent<BoxCollider2D>().enabled = true;
        }
    }
}
public class PressurePlate : Triggerable {
    public Sprite unpressed;
    public Sprite pressed;
    public Collider2D pressure;
    private void OnTriggerStay2D(Collider2D collision)
    {
        if (!pressure)
        {
            on = true;
            pressure = collision;
            GetComponent<SpriteRenderer>().sprite = pressed;
            check();
        }
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (pressure == collision)
        {
            on = false;
            pressure = null;
            GetComponent<SpriteRenderer>().sprite = unpressed;
            check();
        }
    }
}
public class Ledge : MonoBehaviour {
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.GetComponent<Rigidbody2D>())
            transform.parent.GetComponent<BoxCollider2D>().isTrigger = true;
    }
    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.GetComponent<Rigidbody2D>())
        {
            if (collision.tag == "Player" && !Player.instance.stunned) Player.instance.stunned = true;
            collision.GetComponent<Rigidbody2D>().gravityScale = 15;
        }
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.GetComponent<Rigidbody2D>())
        {
            if (collision.tag == "Player") Player.instance.stunned = false;
            collision.GetComponent<Rigidbody2D>().gravityScale = 0;
            transform.parent.GetComponent<BoxCollider2D>().isTrigger = false;
        }
    }
}