Plagued

PLAGUED

What is this?

Plagued is a 2D singleplayer adventure, with a sprawling interconnected world. The focus of the game is its tightly tuned controls to jump, wall-slide and use the recoil of your gun to reach new heights.

 

You play as a plague doctor trying to prevent the spread of the plague, that is on the verge of consuming the world. You use and upgrade your weapon to defeat plague-ridden creatures and make your way to the source of infection. You can also discover treasures and forgotten stories while fighting your way through this adventure.

You can download the game here: https://oliviafollin.itch.io/plagued

  • Engine: Unity
  • Language: C#
  • Platform: PC
  • Development Time: 8 weeks 
  • Team Size: 4

Game Design

My most notable tasks:

Enemies
Upgrade System

Level Design


Miscellaneous task:

The Health System

Audio

Coin System

Collectables  

We wanted to create a game where movement is the key. The main idea behind the movement was that the player should incorporate the recoil of the weapon to reach higher point as well as kill enemies. By having this as our main mechanic I was able to design our levels in different ways; where the player had to use different techniques to reach new heights.

 

We didn’t want the player to feel overwhelmed by too many abilities. Instead of being able to switch between multiple weapons and abilities depending on if the player wants to kill an enemy or reach a platform, we decided to keep it simple by incorporating the two.

 

In the beginning of the level, the player is introduced to smaller jumps and obstacles. We wanted to make sure the player understood how far it reaches from a regular jump.

Since our game is all about movement, we made the decision to add wall jump and wall slide. This is a mechanic that is also introduced in the beginning of the level.


In the beginning of our development we didn’t have this mechanic; instead we had platforms the player had to jump on, to give a “ladder effect” to reach a higher platform.

We felt like it became boring having to always jump from platform to platform. Jumping was already a big part of the game and after some playtesting we felt it became too much. This is the reason why we added the wall jump, it gave the player more variation to its movement.

The wall slide was incorporated later when we realized during playtesting; it was scary to take a leap from a platform while not knowing what was under.

As mentioned before, we didn’t want the player to switch between too many abilities. We wanted the player to always have control over what was happening.

 

We decided to add a shooting mechanic; clicking on the button would eject a small projectile, holding down the same button longer and the projectile would become more powerful.

 

To make the bigger projectile feel more powerful we added recoil; every time the player used it, the player would be thrown backwards.

When the player had learned the jumping mechanic and how to kill enemies, I wanted the player to slowly learn how to use the abilities together. Starting off slowly, the player had to wall jump and then do a regular jump to get to the right platform.

To introduce a way of using the recoil to the players advantage, I created a jump to force the player to use it. It was important that the player learned this mechanic early on; since it was the main mechanic.

The last test was to make the player make a similar jump as it did before, but this time with the recoil of the weapon.

The further the player got into the game, the higher the difficulty become. This was the first time I created a proper level design and it was very challenging to keep it interesting, different and challenging for the player.

 

The deeper into the game the player went the more different types of enemies were introduced. I also added collectables to add a side quest to the players who wanted to learn more about how the plague invaded this world. The collectables were put in challenging places where the player must combine abilities to get to them.

 

This is a game that is design to be difficult for a new player. The more you play the more acquainted should the player become to the controls. While created this game we knew they would be multiple deaths.

They are three different environments in the game; the city, the graveyard and the sewer. We didn’t want to define these sections of the map by only having art representing it. We also wanted to add new obstacles depending on which area of the world the player was on.

Pair Programming

During this project my team and I worked very close together and made sure we always asked for each other’s feedback. To feel comfortable working on all parts of the programming by our self, we started off by pair programming the base of the game together.

The most notable tasks we pair programmed:

Player State Machine

Player Movement

Base Weapon

Base Enemy State Machine

Player Movement

The idea behind the movement was that the player should incorporate the recoil of the weapon to reach higher point as well as kill enemies.

Everything the player and the enemies had in common was built in a script called Brain. Since both the player and the enemies would have different attributes; all the mechanics were divided into states, created in an array in the Brain Class. Each state contained an EnterUpdate and Exit method to make it clear what functionality happens when.

 

This is used to separate the order in which the object is doing specific things during each state. For instance, this is a good way to clearly explain when a specific animation should take place. The State Class is a Scriptable Object whose only task is to keep data.



public abstract class State : ScriptableObject
{
    [HideInInspector] public Brain Brain;
    
    public Transform transform
    {
        get { return Brain.transform; }
    }
    
    public Vector2 Velocity
    {
        get {return Brain.Velocity; }
        set { Brain.Velocity = value; }
    }
    
    public void InternalInitilize(Brain brain)
    {
        Brain = brain;
    }
    
    public abstract void Initilize(Brain brain);
    
    public abstract void Enter();
    
    public abstract void Update();
    
    public abstract void Exit();
}

    

Since all the objects that inherit from the Brain Class have some sort of movement, we created a simple Get and Set method that flipped the sprite depending on its direction.



public abstract class Brain : MonoBehaviour
{
   public State [] States;
   public State CurrentState;
   
   public Vector2 Velocity;
   pulic float Gravity;
   
   private int _facingDirectionHelper;
   public int FaceingDirection
   {
        get { return _facingDirectionHelper; }
        set
        {
            if( value != 1 && value != -1 )
            {
                Debug.LogWarning("Facing direction set to a value thats not 1 or -1);
                _facingDirectionHelper = 1;
            }
            else 
                _facingDirectionHelper = value;
            
            Vector3 euler = SpriteTransform.rotation.eulerAngles;
            euler.y = value == -1 ? 180 : 0;
            SpriteTransform.rotation = Quaternion.Euler(euler);
                
        }
   }
   
   // ...
}

    

The Brain Class inherited from both a PlayerBrain and an EnemyBrain, with code unique to the two objects. Something they both had in common were states, so we allowed the objects to move from one state to another. The method TransitionToState handled that.



public virtual void TransitionToState < T>()
{
    State tempState = GetState< T>() as State;
    if(!tempState)
        throw new NullReferenceExeption("State is null in " + name + ". Trying to access: " + typeof(T));
        
    CurrentState.Exit();
    CurrentState = tempState;
    CurrentState.Enter();
}

    

By working together on the base, it was easier for us to work on our individual parts. If any of us were to be sick, it wouldn’t be a problem to jump in and help each other. Before starting with the code, we decided on a naming convention to keep the code consistent.

 

The movement of the player were the first thing we made together. It took us several iterations and testing before we liked the movement. I had the pleasure of working with a much more experienced programmer than myself and I learned a lot about character controllers. Before this project I had never made a state machine. Being able to brainstorm and pair program together made me learn a lot! It also made me feel more comfortable to work on my own with my solo tasks.

My Role

The Obstacles

We wanted to make the game more challenging the further into the level you got. We also wanted the player to feel the environment change throughout the game to keep it interesting.

To do so we decided to create four different enemies, two different danger elements, one mini boss and one final boss.


I created the functionality for three of our enemies, all the danger elements and the mini boss.


To give the world some more life I created some obstacles the player must avoid to not lose any health. None of these obstacles can be destroyed.

Spikes

The spikes are simple hit boxes that gives the player damage if touched. To give it some extra feedback, the player will be thrown backwards when damaged by them.

Hands

In the graveyard environment I added hands coming out of the ground (vertical and horizontal) trying to hit the player by grabbing it. Right before the hands tries to grab the player, the give out feedback of that happening by having dust particles appear before the hand.

Pipes

In sewer environment I added pipes. The pipes eject a fluid the player must avoid, preventing getting hit. The pipes have different patterns of when and how far the fluid comes. Just like the hands, the pipes give feedback before ejecting the fluid.

The Enemies

Each enemy has an Enemy Brain, this is where we decide all its data; such as max health, damage and coin worth. This was also an easy way for me to adjust the values while building the level.



public abstract class EnemyBrain : Brain
{
    public static List< EnemyBrain> Enemies;
    
    public float MaxHealth;
    public float CurrentHealth;
    public int CoinWorth = 100;
    public LayerMask CollisionLayers;
    public float DamageFlashTime = 0.1f;
    
    [HideInInspector] public BoxCollider2D Collider;
    [HideInInspector] public Animator Animator;
    [HideInInspector] public Spawner CoinSpawner;
    
    public ParticleSystem DamageParticles;
    
    // ...
}

    

Some enemies are patrolling. To make it easier for me (or anyone else who wanted to help with the level) to put the patrolling enemies into the world, I created gizmos to easier see between what points the enemies would patrol. This also prevented us having to go to play mode to see if the points were correctly placed.

Vermin

The first enemy introduced is the vermin; a patrolling rat that attack the player by jumping on it when it’s in reach.

To create some variation, I created smaller versions of the same rat, but with less max health.

Bat

The next enemy introduced is the bat. It flies around its path, until it spots the player and dive to the player to attack.

Pitcher

The pitcher is a static enemy that throws glowing balls to hit the player. It will start throwing the balls if the player is in range and the player will also take damage if touching the enemy.

Death and Reward

When an enemy die, the Die method is being called. When they die, coins appears and the enemy collider disappears. The reason why we did it that way instead of destroying the whole object, is because we wanted the player to be able to see all their kills. At last, the enemy jumps into DeathState whose only task is to start and quit the animations.



public virtual void Die()
{
    Spawner coinSpawner = Instansiate(_coinSpawner, transform.position, Quaternion.identity);
    coinSpawner.SetUp(CoinsWorth, transform.position);
    AudioManager.Instance.Play("CoinsAppear");
    
    if(Collider != null) Collider.enabled = false;
    TransitionToState< Deathstate>();
}

    

To give the player more feedback when it hit an enemy, we added a method who’s only task was to make the enemy sprite flash red when it got hit.



protected virtual IEnumerator Flash()
{
    TimeManager.Instance.Freeze("EnemyDamage");
    
    if(_spriteMeshes == null) _spriteMeshes = GetComponentsInChildren< SpriteMeshInstance>();
    
    foreach(SpriteMeshInstance spriteMesh in _spriteMesh)
    {
        spriteMesh.color = Color.red;
    }
    
    float t = DamageFlashTime;
    while (t > 0.0f)
    {
        t -= DeltaTime;
        yield return null;
    }
    
    foreach (SpriteMeshInstance spriteMesh in _spriteMeshes)
    {
        spriteMesh.color = Color.white;
    }
}

    

As soon as an enemy die, the player will be rewarded with coins.


I wanted two things when it came to the coins. First, I wanted the player to get many coins when they were collected – I liked the feeling it gave when all coins popped out of the enemy and seeing the number increasing in the UI. I also wanted the coins to have a magnetic effect when the player was near them – instead of having to walk back and forth to make sure you picked up all coins, this feature made it easier to keep a flow in the gameplay.

Almost all methods in EnemyBrain are abstract and can be overridden depending on the enemy type.


The Respawn method make sure all enemy variables are being reset.



public virtual void Respawn()
{
   CurrentHealth = MaxHealth;
   transform.position = StartPosition;
   if(Collider != null) Collider.enabled = true;
   Velocity = Vector2.Zero;
}

    

We added an abstract method all enemies implemented, that would take care of the damage. The method goes through a struct called EnemyHitData and return a bool whether the enemy is dead or not. It also returns a float that takes in how much damage is being done.

 

The method GetHit is being used differently depending on the enemy. The code below is an example from the Vermin Class (the rat enemy).



public abstract EnemyHitData GetHit(float damage, Vector2 projectileDirection, Vector2 force = default(Vector2));

public override EnemyHitData GetHit(float damage, Vector2 projectileDirection, Vector2 force = default(Vector2))
{
    StartCoroutine(HealthbarTimer());
    PlayHitParticles();
    
    float hitDuration = HitDuration.GetLerpValue(Mathf.Clamp01(damage / Maxhealth));
    
    float remainder = CurrentHealth - damage;
    CurrentHealth -= damage;
    _enemyHealthBar.UpdateHealthBar(currentHealth  MaxHealth);
    if(CurrentHealth <= 0) _enemyHealthBar.gameObject.SetActive(false);
    
    CurrentHealth = Mathf.Clamp(CurrentHealth, 0.0f, MaxHealth);
    
    StartCoroutine(Flash());
    TimeManager.Instance.Freeze("EnemyDamaged");
    
    if(remainder > 0.0f)
    {
        if(force.magnitude > 0.0f)
        {
            Velocity = force;
            GetState< HitState>().SetUp(hitDuration);
            TransitionToState< HitState>();
        }
        return new EnemyHitData(false, damage);
    }
    float damageDone = damage + remainder;
    Die();
    
    return new EnemyHitdata(true, damageDone);
}


    

Mini boss

When the player has managed to complete the city and graveyard level, they will encounter a mini boss – right before the drop sown to the sewer to find the big boss. Instead of creating a completely new enemy I decided to take existing enemies and obstacles to create a mini boss.

 

I took a pitcher and made it bigger. I also gave it more health and damage to make it more challenging. When arriving at this point the player would already have overcome some big challenges. I added the hands and bats to raise the difficulty level a bit.


When entering this fight, I added a trigger to make sure the camera would be positioned correctly. I wanted the player to be able to see the whole scene; instead of having the camera following the player.


The Upgrade System

With the coins collected from killing enemies or finding treasures, the player can be upgraded.

The player can win the game without an upgrade, but it will be easier by getting some. I created the upgrade system and divided it in 3 categories; Gun, Grenade and Health.

  

This system is built as a Node Tree. Each Node (represented as an upgrade) has variables to determine its type. I wanted an easy way to change data for each upgrade object; these are the data we could easily change in the inspector.

 

Obtained Color

The color changes if the player has bought the upgrade.

 

Price Tag

An UI element to tell where the price sign should be.

 

Price

The cost of the upgrade.

 

Upgrade type

An enum with all types of upgrades the player can buy.

 

Connections

The children the node has, to make sure the player upgrade a category in the right order (from top to bottom). 

 

Event Trigger

Each upgrade is connected to a text-description, so the player gets an understanding what each upgrade does.



public class TalentObject : MonoBehaviour
{
    #region Colors
    [Header("Colors")]
    public Color ObtainedColor;
    #endRegion
    
    #region Prizes
    [Header("Prizes")]
    [SerializeField] private TextMeshProUGUI _prizeTag;
    [SerializeField] private int _prize = 100;
    #endRegion
    
    #region Weapon Type
    [Header("Upgrade type")]
    [SerializeField] private UpgradeType _upgradeType;
    #endRegion
    
    #region Branch Connections
    [Header("Connections")]
    [SerializeField] 
    private List< TalentObject> _children = new List< TalentObject>();
    [SerializeField] private bool _isRoot;
    #endRegion
    
    private TalentObject _parent;
    private bool _isBought;
    private image _image;
    
    //...
}


    

Miscellaneous

Healing

I added a healing function where the player had to sacrifice coins to heal itself.

Collectables

The main goal of the player is to get to the boss in the sewer; to kill the source of the plague. We wanted to add lost journals, with a short text to describe what happened to this world. This isn’t something the player must collect to win the game, but a fun quest for the player who’s curious.

I didn’t want all the journals to be on the path of the player. I created some challenging areas, where the player had to use all its abilities to get to the journal. The obstacles to overcome was designed to be a bit more difficult than the regular path.

 

When a player found a journal it also got some coins out of it. The player could also re-read the found journals in the menu.

Player Death

When the player dies, we didn’t want the player to lose all its coins but instead to be able to pick them up from the same spot. The problem was having many coins laying around. When the player died, I stored all coins in a big coin the player could pick up.

Doing this removed a lot of lag. If the player were to die a second time before getting to the coins, all the coins would disappear. I added that feature to make the game a little bit riskier; we didn’t want the player not to care about a death.

Checkpoints

I also created a checkpoint system; since the probability would be high, especially for a new player. We decided the place to get an upgrade would be the same place the checkpoint was activated.


When the player passed an alchemy machine to upgrade the tools it would light up to indicate that the checkpoint has been activated.

The Process

It took us a while to come up with a game idea. We wanted to make a game where the weapon is a big part of the player movement.  As soon as we had an idea, our artist started creating concept and me and the other programmer made a UML diagram to get an overview of how we would create the classes. Then, as I mentioned before, we pair programmed the base of the state machine and the base of the weapon abilities.

 

For me it’s important with communication, we all worked very close to each other and kept giving each other feedback on art, gameplay and layout. I created a scrum board and made sure we all updated it, so everyone was on board on what was happening and what was done. Every time someone worked from home, it was important to communicate what each person was working on and in what scene we where working to avoid merge conflicts.

Challenges

Our group worked really well as a team. The biggest challenge was to be able to have a finished product for our deadline. There were of course a lot of challenges code wise; but with research, testing and “rubber-ducking” it all turned alright! Two things I found challenging was the level design and the sound. Nearing the end, our artists had a lot to do with all the assets and us programmers where almost done; with only some minor bug fixes and “juice” left. We didn’t have any levels, so I took the role as a level designer. Since our game was all about movement it was a big challenge to make a level that felt good.

 

One thing that was challenging (mostly because we didn’t think of it) was the sound. We had an audio system but no sound. Originally, we had plans to record our own sounds but always postponed it. It was challenging to find sounds that matched the game in short notice!

Iteration

As I said before, it was a challenge to make the level feel like they had a meaning and was fun. I asked everyone to always playtest and took notes to see where I should iterate or not. During the playtest I also took notes to see what bugs we had programming wise.

 

I learned a lot from this project, I developed as a programmer a lot by working with others! One thing I took with me were the importance of giving time to playtest and create the level. Just because you have one level doesn’t mean it’s the final one, iteration on the game is key to get something fun.