Degree project

A Diablo-like RPG

Let me proudly present my own take on the top-down RPG genre, combined with features from my favourite game-series Dark Souls


What started as a small free-time project evolved into four months of dedication, refining gameplay mechanics and mastering the implementation of Diablo-like features. However, this isn't just another mob-grinding RPG. My goal was to create a challenging and punishing experience inspired by Dark Souls, where strategy and precision are key. By incorporating a rolling mechanic, players must carefully plan their moves and dodge enemies to survive, making every encounter a test of skill and reflexes.


The name of the game is temporary, I borrowed it from the band: "King Gizzard and The Lizard Wizard"

Player controls

Point to click

Hold or click the mouse button to move the character to that position.


  • Gets the mouse input position on the NavMesh everytime you click
    • Sets the bool "isMoving" to true when pressing, and sets it to false when releasing
  • Moves player to that position
    • In case mouse raycast hits an interactable object, it runs a HandleInteraction() function instead
    • Gets the center point of that object and moves the player there

Move() runs while isMoving is true

In case mouse hit is Interactable

Buffered input

To make the game more responsive, I have implemented a "buffered input"-system. While doing an attack or performing a roll, the player can hit a key which performs another action during the current one, and when the current action is finished it queues the action the player has pressed.


  • Every action stores 2 floats:
    • A float which represents the time required for the moment he starts the action, to the point when it is ready for another input
    • The other float represents the time for when the buffered input is ready to perform
  • The PlayerInput script includes all functions for handling the different inputs
    • It is structures in a way that it first checks if player can do action at all
    • Then it checks if player is ready for another input
    • If that is false, is then checks if player is already doing an action, and if that is false, it then performs the action that the function is meant for
  • The PlayerMovement script stores three bools that checks if:
    • hasBufferedRoll
    • hasBufferedAttack
    • hasBufferedMovement

Roll mechanic

When pressing space, the player performs a roll.


  • The roll mechanic is divided into four main functions:
    • RollStart()
      • First it clears any previous attacks (if performing)
      • Then it gets the mouse position which will be the direction of the roll
      • Then it disables the NavMesh agent
      • Then triggers the animation
    • RollUpdate()
      • This is used to handle any buffered input during different time marks of the roll
    • RollEnd()
      • Resets the NavMesh and warps the NavMeshAgent Y position to the current Y position
    • OnAnimatorMove()
      • The player position of the roll is handled by the animation itself
      • It sets the player position to the Animator delta position
      • It does a raycast to the ground and lerps the Y-value to that, so that the player can roll up and down stairs
  • It also has a function that checks if the player is colliding with wall
    • In that case, it ends the roll and goes back to idle

RollStart:

RollEnd and OnAnimatorMove:

RollUpdate:

IsCollidingWithWall

Result

Enemy AI

Event handler

For the Enemy AI, I used something called Event Handler. Every event has an OnBegin, OnUpdate and OnEnd function, and a bool that returns if event is done. The enemy has a List (that acts as a stack) where it keeps track off which current event is active, when when the current event is done it removes it from the list and proceeds with the previous event. Some examples of events are:

  • Idle
  • Follow Player
  • Attack
  • Dead


Enemy base script

This is the script that all enemies use as a base. The scripts consists of:

  • OnObjectSpawn function
    • Since I am using Object pooling for this project, everytime an object spawns from the pool it runs this function
    • For the enemy, it resets some values like:
      • Sets the health to max again
      • Enables the NavMeshAgent
      • Clears the Event Stack
      • Disables ragdoll
      • Adds new "Idle event"
  • TakeDamage function
  • EnableRagdoll function
  • Die function
  • Attack function
    • Which simply adds the wanted attack-event
  • Has a list of the enemies drop rate


Enemy Event 

This is the script I have used for the base of all the other event scripts. Every enemy-event like Idle or Attack inherits from this.

  • Includes some important functions that all the other events can use:
    • SetNewDestination
      • Gets a random destination up to 3 times
      • As soon as the destination is valid, it sets the agent's target destination to that position
    • IsValidDestination
      • Returns true if the enemy can create a path to desired destination
    • CheckAnimationInterval
      • Checks every few milliseconds if any attack animations are playing
    • IsAnyAttackAnimationPlaying
      • Is called by CheckAnimationInterval
      • Loops through all the attack animations the enemy has and checks if any is playing
    • IsAnimationPlaying
      • Check if specific animation is playing
    • IsCloseToPlayer(distance)
      • Returns true if distance between enemy and player are less than the desired distance
    • IsTargetedAtPlayer
      • Does a raycast and returns true if it hits player

Some other functions that are inherited from Character script:

  • SetStats
    • At the start of the game, all the stats are affected by which level the enemy is in
    • For every level, it multiplies the stats by 1.5 
    • Stats include:
      • Health
      • Damage
      • Defense
  • CanSeeTarget
    • Has a vision cone in front of enemy
    • Returns true if player is hit by raycast
  • SetFloatRunSpeed
    • Lerps the current speed and target speed
    • Sets the animator float "RunSpeed" to that value
    • This is to avoid a stuttery effect while transitioning to different animations
  • HandleRotation
    • Updates the rotation to face the target
    • Slerps between the current and target rotation to make it smooth


Boss fight

Three different attacks 

For this section, I want to show the three unique attacks that the boss has, since the boss otherwise share the same base behaviour as the other enemies do. The boss attacks consists of a combo attack, a jump attack and a ranged attack that makes it rain rocks from the sky.

Combo attack 

This attack script does all melee enemies share in common. I have made a attack combo-system that is easily expandable upon, where you can just add more animations in queue and the enemy will play them in order unless he is too far from the player. In that case he will go back to its "FollowPlayer"-state.


  • OnBegin
    • First checks if close enough to player
    • Sets currentAttackIndex to 0
    • Triggers first attack animation
  • OnUpdate
    • Runs HandleRotation to face the player
    • Runs HandleAnimationCombo
  • HandleAnimationCombo
    • Returns if enemy is currently playing "Take damage"-animation
    • Checks if the current index of the list "attackAnims" are playing
    • Then checks if a certain amount of time has passed playing that animation
    • Then checks if close enough to player
      • If so, change the currentAttackIndex by 1
      • If index is higher than the attackAnims list, it goes back it the first animation
      • Otherwise it triggers the next animation

Jump attack 

The JumpAttack script works in a similar way that the roll mechanic for the player does. It lets the animation handle the movement. In the previous state/event, it checks if the boss is within a certain distance of the player, and in that case it either performs a jump attack or a ranged attack depending on the RNG


float jumpAttackDistance = 4f;

float comboAttackDistance = 2f;


These two floats are used to decide if the enemy should do a combo attack (the previous one) or a jump/ranged distance.


Now onto the script itself:

  • OnBegin()
    • First checks if close enough to player (uses jumpAttackDistance)
    • Disables NavMeshAgent
    • Trigger jump animation
  • OnUpdate()
    • Checks if it has finished animation
  • OnEnd()
    • Checks if close enough to player to do a melee attack (uses comboAttackDistance)
  • GroundSlam()
    • Spawns a game object from the pool that acts as a ground slam effect
    • Sets the parent of the ground slam object
      • In the spawned ground slam's script, it deals the enemy's damage to the player if it is close enough

Ranged attack 

In this attack, the boss makes it rain rocks from the sky. Before the rocks hit the ground, it shows indicators of where the rocks are going to land, so that the player has some time to dodge them. The first rock will always be above the player, so that the player has to move either way to not get hit.

It has three variables:

  • OnBegin()
    • Stops NavMeshAgent and triggers animation
    • Has a for-loop that goes through the projectileCount
      • If index is 0, it gets the player's position and spawns the first projectile above his head
      • Otherwise, it gets a random position from (X - range to X + range). Same goes with the Z-axis
  • ProjectileCount is how many projectiles the boss is going to launch each attack
  • ProjectileSpawnHeight is the Y-value how high above the boss's Y-value the projectiles are going to spawn
  • Range is how far the boss can shoot projectiles (x-value)

Boss projectile script 

  • OnObjectSpawn()
    • Set the rock mesh to a random rotation
    • Sets the parent of the landing indicator to null
  • RaycastToGround()
    • It shoots a raycast from the rock's position down to where it is going to land
    • The landing indicator's position is set to where the raycast hits, with an offset of 0.1 so it is located a little bit above the ground
  • OnTriggerEnter()
    • Resets the parent of the landing indicator to this projectile transform

Skill tree

Choose your path 

At the skill tree's current state, it only has five different types of passive skills in total. The skill types consists of damage increase, health increase, defense increase, mana increase and attack speed increase. Despite that, I have big plans for the skill tree in the future, and it is going to play a large part in what makes the game enjoyable. There will be a lot more stats that will be affected by different skills, and the player will be able to choose a lot more paths in what way he/she wants to take the character. Here is a list of skills that I plan to add in the future:

Currently added skills:

  • Damage increase
  • Health increase
  • Defense increase
  • Mana increase
  • Attack speed increase


Future skills:

  • Damage percentage increase
  • Health percentage increase
  • Defense percentage increase
  • Attack cooldown decrease
  • Movement speed increase
  • Health regeneration increase
  • Mana regeneration increase

(Inspiration from Path of Exile)

Get skill points by leveling up 

I have made a level system that applies for all characters in game (both player and enemies). Each time the player levels up he gets a skill point that he can spend to achieve a passive skill. For the enemies it works a little different though. For each level the enemy has, it multiplies all its stats by 1.2. That makes it easily scalable for future levels where the enemies should be harder, and it's easy to reuse already existing enemies and just increase their level to match the level's difficulty which will save a lot of time during development. With that said, I will of course introduce new enemies as the game goes on.

Items & Inventory

ItemSO 

Every single item in the game has a scriptable object called ItemSO as a parent. The ItemSO script stores some important variables that will apply for all children, like a item name, item description, item icon and a bool that is called "isStackable" which determines if you can stack multiple items of a specific type in the same inventory frame.

ActionItemSO

This is a child of the ItemSO that can be equipped in the action slots and used by the player. It stores two floats, one for a cooldown and one for a timer that starts at the moment the player uses the item. It also has an abstract function called "PerformAction", which is called when the player presses the key that matches the number of the action slot the item is in.

AttackTypeSO

This is a child of ActionItemSO, and is a parent to all the different abilities the player can do. It consists of:

  • A damage multiplier that multiplies the player's base damage with itself, and applies the total damage on enemy hit
  • NextAttackDelay is the time it takes for the attack to be finished and the player can do another attack or action
  • BufferedInputDelay is the time it takes from the start of the attack to be ready for another input
  • Mana cost is how much mana will drain when performing the attack
  • GetStatIncrease is a function that returns a string of the total dealt damage of the attack, that will be displayed in the tooltip window 

PotionSO

This is a child of ActionItemSO, and is a parent to all the different potions the player can drink in game. It consists of:

  • An enum of which type of potion it is
  • An overridden function of PerformAction that checks what type of potion it is. It refills health if it is a health potion, and mana if it is a mana potion

EquipmentSO

This is a child of the ItemSO script and is used for equipment and weapons. The player can put them in the Equipment Slots. Each equipment item has an enum for what type it is, and all the equipment slots has its own specific type as well. The type consists of Helmet, Boots, FirstHandWeapon, etc. This is because the player is not going to be able to equip a helmet in the boots slot, or a sword in the helmet slot. The equipment type of the item and the slots has to match for the player to equip it.  

The Stat class consists of:

  • An enum of what type of stat is is
  • StatText is a string that will be displayed in tooltip window, for example "Additional damage:" or "Additional movement speed:"
  • StatImprovement is how much a stat will be improved depending on what type it is
  • MainStat is the most important stat the equipment has. Adds damage from weapons and defense from armours.
  • BonusStats is an array of additional stats an equipment can have. For example, more health regeneration or more maximum mana.
  • UpdateStatDisplay is called from the tooltip window to display the stats from the equipment

__________________________________________________

Inventory screen

The inventory screen consists of 30 inventory slots, 5 equipment slots and 5 action slots. InventorySlot is the parent script of all the other slot types, and is where most of the inventory logic is handled. Each of the inventory slots stores an ItemSO, and the player is limited to place item's in different slot types depending on what item type it is. For example, you can not put an action item in an equipment slot, and you can't put a non-action item in an action slot.


To move around item's, I've implemented a drag and drop-mechanic. The InventorySlot script has three inferfaces included: IBeginDragHandler, IDragHandler and IEndDragHandler. Each of these has a function for when the player is beginning to drag, during drag and end of the drag. To make the item icon follow the mouse when dragging, I have removed the sprite from its parent and placed at the mouse position during drag, and when dropped, it shoots a raycast from the mouse position and depending on what inventory slot it hits, it is placed in that slot by setting the current inventory slot to null, and the targeted inventory slot to this inventory slot. The sprite becomes a child of the previous slot again, but is set to null, and the sprite of the new inventory slot is changed to the new one.


I also have implemented a mechanic to easily switch items with each other. This was the trickiest part in the whole game to get right, in my opinion. There was a thousand stuff to keep track off, since a lot of bugs occured where you could for example switch an item from the equipment slot with an item in the normal inventory slots. That made the player being able to equip a health potion for example, if he switched it with an armour piece. So, I had to set a lot of restrictions to in what moments the switch was going to be successful, or where the item's should go back to their original places. On top of that, I also had to keep track of when the stats of the armour pieces should be applied and when they should be removed.

__________________________________________________

Tooltip window

The Tooltip window shows up when you hover the mouse over an item or a skill to see more information about it. The information it displays depends on what type of item or skill it is, and the window scales depending on how much text is inside the box

For an armour piece, it displays the title at the top, then below that there is a short description and what the main stat of the item is. Below the description it shows what level is required to equip the item, and at the bottom it shows all the bonus stats (if it has any at all).

Every time you hover over an item, it takes all the information from the ItemSO that item slot holds.

  • The function takes in a Vector3 position, float height and an ItemSO item 
  • It uses the position of the inventory slot and creates an offset of the height / 2 to place the tooltip window right below of the slot
  • It sets the title text and description text to the correct item
  • Activates the lines that are above and below the RequiredLevel text
  • Disables statText if item is not a AttackTypeSO or a PotionSO
  • If the item is a potion, it enables the statText and calls the function "GetStatIncrease" from the item to display the heal amount
  • If item is an equipment it sets statText to true
  • Gets the statText and statImprovement from the Item
  • Returns if it doesn't have any bonus stats
  • Loops through the bonusStat array that consists of TextMeshProGUIs, and enables and sets the text if the item has bonus stats

Abilities

Fire Attack

The Fire Attack is the most straight forward attack I have implemented, you simply shoot a fireball and when it hits an enemy or an object it causes an explosion. The explosion has a sphere collider, and it takes the radius of that sphere and deals damage to all the enemies that are within the distance of that radius.

FireAttackSO

  • Has a attackTrigger hash for the fire attack animation
  • Has a constructor that sets all the values to the correct amount
  • Has a PerformAction function
    • Plays a fire attack sound effect
    • Triggers the fire attack animation trigger
    • Resets cooldown to zero

Explosion

  • Has a reference to an AttackTypeSO
  • In Awake(), it sets the damageRange to the sphereCollider radius + 0.5f
  • In OnObjectSpawn() it checks which enemies are within the damage range
  • Calculates the total damage dealt in the StatCalculator class

__________________________________________________

ProjectileSpawner

  • Has a Vector3 offset for where to spawn the projectile
  • Has two functions that are called from the animation, one with an offset and one without
  • SpawnProjectile() gets called from both of the previous stated functions
  • Spawns the projectile from the object pool
  • Spawn position is different depending on if the bool "offset" is true or false

Lightning Attack

The Lightning Attack causes a chain effect between up to five enemies if they are close enough to each other, and deals electric damage to those enemies. The player also has to be close enough to an enemy for the effect to deal any damage at all, otherwise nothing will happen. 

FindClosestEnemy

  • Makes an array of all enemies
  • Sets closest distance to player range
  • Loops through all enemies
    • Excludes enemies that are already contained in the list or dead
    • Checks distance to current enemy
    • If distance is less than closestDistance, it sets the closestEnemy to current enemy, and the closest distance to current distance
    • If it is checking closest enemy from player's position (firstTime), it makes sure the player is targeted towards the enemy

__________________________________________________

ChainToClosestEnemy

  • Makes a new array of gameobject with a total number of maxChains (5)
  • Gets the closest enemy
  • Adds closest enemy to chainedEnemies list
  • Sets the startPosition to closest enemy position
  • Sets the position count of the lineRenderer to chainedEnemies count + 1 (the player)
  • Sets position of lightRenderer to closestEnemy position
  • Spawns lightning particle effect from object pool
  • Calculates ability damage
  • Has a dictionary of enemies and how many times they have taken damage from the attack
  • Has a damageTimer for every time all enemies within the attack range should take damage
  • In the Update loop, it first checks if the particle effect is done playing, if so, the enemiesHit dictionary clears and the game object despawns
  • Returns if enemies hit is less than 1
  • Checks if current timer is more than damage timer
    • If so, it creates a list of the keys in enemiesHit
    • Iterates through the list and check if they haven't been hit more than 2 times
    • If not, it deals damage to that enemy
    • Changes the key of current enemy in enemiesHit by +1

Earth Shatter Attack

The Earth Shatter attack causes rocks and fire to come out of the floor, and deals great continuous damage to whatever enemy steps inside of it. Even though this attack is overpowered, the mana cost and cooldown is very high. So the player has to consider in what moment it is best to use it.

Rock script

I have made a script for the rocks of the particle effect as well, where it checks if the rocks are colliding with an enemy.

  • OnParticleCollision
    • If enemiesHit dictionary doesn't contain the current enemy:
      • Deals damage to enemy
      • Adds the enemy to the enemiesHit dictionary