top of page

Tribulations of the Mage

Development Info     

Genre: Third-Person MetroidVainia

Development time: N/A

Game Engine: Unreal Engine 5.3

Platform: PC

Team size: 1

Responsibilities

  • Design and developed enemy AI

  • Design and develop systems and mechanics in the game

  • Level Design

Creative Vision

Tribulations of the Mage is a Third-Person MetroidVania game inspired by Tunic and the Legend of Zelda series. In the game, you take control of a mage who has to fight their way through a dungeon to reach the surface.​

Video Breakdowns

Player Mechanics

Player Character

The player character is a mage who wields a staff in one hand and spells in the other.

The player character can sprint, jump, and lock on to targets by pressing the Tab key. 

Stamina Control Code

There are four parts to stamina control.

The DamageStamina event is called when the player's stamina is lowered in a one-time event and not continuously.

These events include blocking attacks, attacking, and rolling.

Different actions will drain the player's stamina in different amounts.

​

The DrainStamina event is called when the player's stamina is drained continuously. This event is called when the player is sprinting.

​

The RecoverStamina event is called when the player needs to recharge their stamina bar. This event occurs when the player uses stamina and goes for two seconds without using stamina, such as when the player sprints and then stops.

​

​The NotEnoughStamina event is called when the player does an action requiring stamina, but they don't have enough stamina to perform the action. If the player tries to perform an action without enough stamina, then a message widget will appear on screen telling them that they don't have enough stamina.

​

​

Sprint Code

The player sprint works as a normal sprint as seen in other games. As the player sprints the player looses stamina.

Roll Code

When the player presses CTRL the will do a roll that can used to doge attacks.

While rolling the player is immune to certain damages.

This roll mechanic is inspired by the roll mechanic as seen in the Dark Souls series and Tunic.

Roll Conditions Code

When the the player rolls the code first checks if they can roll. The player will not be able to roll if they are already rolling, blocking, doesn't have enough stamina, or is drinking a potion. 

Combat Mechanics

Player Attack

Attack Code

The player will perform a melee attack with the staff when they press left click. The player has a four-attack combo they can perform if they press left click four times in quick succession. Each attack in the combo has different animations, and each attack consumes ten stamina. Before the player can attack, the attack input action node will check if the player meets the conditions to perform an attack. If they do, the Blocking Boolean will be set to false, and the Attack function will be called. When the player does numerous attacks in quick succession an integer variable called Attack Count will Increment and different attack animation will play depending on the Attack Count value.

Weapon Attack Stamina Check Code

Inside the Attack Conditions function, the first thing checked is if the player has enough stamina to do an attack. This is checked with another function called Weapon Attack Stamina Check. Inside the function, the Attack Count is checked to see if it's a certain number and is connected to a Branch node. If the Attack Count does equal that number, then the player's stamina is checked to see if its greater than or equal to the amount of stamina required. That Greater Than or Equal to operation node is connected to another Branch node. If true, then the Enough Stamina for Attack Boolean is set to true, and they can attack. If false, then the player can not attack, and the Not Enough Stamina function is called, which puts the Not Enough Stamina message on the player's screen. All of the different attacks are checked like this, but have different required values. For example, if the Attack Count variable is equal to 3, the function checks if the player's stamina is greater than or equal to 20 before they can attack.

Attack Conditions Code

After the attack count and stamina are checked, the Attack Condition function then checks if the player is doing an action that prevents them from attacking. The player will not be able to attack if they are rolling, blocking, stunned, casting a spell, dead, don't have enough stamina, or are drinking a potion. These actions are represented in the Attack Conditions function as Boolean variables, each connected to branch nodes. If one of them is true, then they will be connected to a Can Attack Boolean that is set to false, meaning that the player can not attack. If one of the Booleans is false, then it will go to another branch node to check if another action Boolean is false. If all of the actions represented as Booleans are false, then the Can Attack Boolean is set as true, and the player can attack. When the player attacks the Blocking Boolean is set to false and the Attack function is called

Attack Event Code

A Branch node is connected to the Attack custom event that checks if the Attacking? Boolean variable is true or false. If true, then the Save Attack Boolean variable is set to True, which is used for the Attack Combo custom event. If false, the Attacking? Boolean variable is set to true, and the recover stamina handle timer is cleared. After that, the function goes through a macro called Calculate Weapon Attack.

Calculate Weapon Attack Code

The Calculate Weapon Attack macro checks and sets the Attack Count variable. The macro has four Exec pins that connect to a different attack.

 Attack Combo Code

The Attack Combo custom event checks if the player should continue the attack combo or if the player has done the last attack in the combo. This is done with a Branch node connected to the custom event that checks if the Save Attack Boolean is true or false. If false, then that means the player did the last attack of the combo, and the combo gets reset with a function called Reset Attack. The reset is done by setting the Save Attack to false, setting the Attack Count to 0, and setting Attacking? to false.

Reset Attack Code

The reset is done by setting the Save Attack to false, setting the Attack Count to 0, and setting Attacking? to false. If true, the Save Attack is set to false and goes through the Last Attack macro.

Last Attack Code

If true, the Save Attack is set to false and goes through the Last Attack macro. In the Last Attack macro, the Attack Count is checked if it's equal to the Number of Attacks. The return value is connected to a NOT value, which is connected to a Branch node. If true, the player continues the weapon attacks and the continue weapon attack exec pin is connected to the Calculate Weapon Attack macro; if false, then after a 0.5-second delay, the player's attacks are reset.

Start Attack Code

When the player attacks, a sphere trace is drawn between two points on the player's staff. This is done with a function called Start Attack found in the I_Character interface used for the player and enemies. The Start Attack event is connected to a Set Timer by Event node that is used to draw sphere traces as the player and enemies swing their weapons. The timer is then cleared in the Stop Attack function. On the Sphere Trace for Objects node, the Out Hit value is used to get a Break Hit Result node. The Hit Actor value on the node is used to see if the actor hit is in an array of actors called Already Damaged Enemy. This is done so that the player only does damage once when they swing their weapon, since multiple sphere traces are drawn with each swing of the weapon. The check for whether an enemy is already damaged is connected to a Branch node. If true, then the attack will do nothing; if false, then the hit actor is added to the Already Damaged Enemy array and takes point damage.

Stop Attack Code

After the player attacks, the Stop Attack function is activated. The Stop Attack function stops sphere traces from being drawn when the player swings their weapon. Then the Already Damaged Enemy array is cleared so that enemies can be damaged again.

Individual Attack Code

There are four attack functions that are connected to the four pins on the Calculate Weapon Attack macro. A Play Anim Montage is connected to each of the attack events. There is an animation variable connected to the Anim Montage of the Play Anim Montage node. I did this so I could change the animation to make different combos. This is also done for the enemies because the enemies are child actors of a parent enemy actor. After the animation montage is played, the Damage Stamina function is called, with stamina reduction being 10. 

Improved Blocking and Targeting

Blocking Code

The player blocks when they click and hold the right mouse button. When activated, the Triggered pin on the Enhanced Input Action node checks if the player's stamina is greater than 0. If false, then the Blocking variable is set to false, meaning that the player can't block. The exec pin of the Blocking variable is also set to another Blocking variable that's set to false, which is attached to the Completed pin of the Enhanced Input Action node. This is done because the player stops blocking either intentionally or because they run out of stamina from blocking attacks. Then the shield particle effects around the player are set to hidden to show that the player is no longer blocking. Then the collision boxes attached to the shields have their collision set to no collision. If the player does have enough stamina for blocking, then the code checks if the player is already attacking or casting a spell. If both are false, then the Blocking variable is set to true. Then, the particle effect shields around the player are made visible with a Set Hidden in Game node set to false. Then, a blocking animation is played and blended with player movement for as long as the player blocks.

Additions to Damage System

Trigger Effect Event PointDamage Code

After working on a couple of spells, I felt the need to add a system for elemental damage and effects. I accomplished this by making different damage types and adding a Status Effect Component to enemies that triggers different effects depending on the damage type. When enemies are damaged, the Damage Type value of the Event Point or Radial Damage event is set as a variable called Damage Type. This is plugged into a function in the Status Effect Component called Trigger Effect. The Trigger Effect function is located in a damage type interface.

Trigger Effect Event  Code

When Trigger Effect is called, it goes through a Switch on Enum node with the enum being a status effect enum with the different status effects listed. The enum will switch depending on the damage type. When a status effect is triggered, it will spawn a status effect actor that's attached to the damaged enemy. 

Fire Damage Type Code

For example, the fire damage type will trigger the burning status effect.

Burning Effect Code

The different status effects are made into actor blueprints. The Burning effect works by having a Set Timer by Event node attached to Event BeginPlay that loops. The event attached to this timer is called Burn, which triggers every second for 3 seconds. The Burn event will deal point damage to the actor to whom the status effect is attached. There is also a chilled status effect that was meant to slow enemies, but there were problems with it, so I changed how enemies get slowed when damaged by ice attacks.

Items

Items

Item Parent Code

All items in the game are child actors of the Item Parent actor. The components of the Item Parent are a blank static mesh that is changed in the child actors and a box collision. From the BeginPlay event, there is a cast to the player character so that variables like health and stamina can be affected depending on what the item is meant to do. I did have the item parent check the save game to see if it's been interacted with to destroy it, but I decided to do that for the child actors because not all items are meant to disappear on use. When the player enters the box collision of an item, a widget will appear on screen that will tell the player to press E to interact with the item. The message will differ depending on the item; for example, spells will say [E] to learn spell. When the player leaves the box collision, the message will disappear. To interact with items, the player has to press the E key. Each item has different interactions and once interacted a save game variable called Used Interactable? will be set to true and will  be added to an array called Interactables Used also found in the save game.

Save Interactable Used Code

Inside the game mode blueprint, there is a function called Save Interactables Used. Inside the function, there is a Load Game from Slot node that is used to cast the save game blueprint. From the cast, I get the Interactables Used array variable and connect it to a For Each Loop node. From the Loop Body, I check if the item is valid. Then I check if the item in the Interactables Used array contains a Used Interactable and set it. Then the game is saved. This goes through all of the items in the level and saves whether they were used or unused when the player saves the game. 

Set Interactable Used Code

When the game is loaded from a save, the Set Interactables Used function is called from the game mode. First, the function loads the game from slot. That node is used to cast to the save game to get the Interactables Used array variable. That variable is used to set the Interactalbes Used array, which plugs into a For Each Loop node. The Array Element is used for an Is Valid node and to cast to the item parent blueprint. The cast is used to get a variable called Destroy Item on Use? This variable is connected to a branch, and if true, the item is destroyed. This is used for items like one-time consumables.

Interaction Code

The Interact function is found in the item interface. When the player presses E, it will call the Interact function in the item blueprint. Inside the player character blueprint, when the player presses E, it will go through a For Each Loop with Break. The array input value is gotten from a Get Overlapping Actors node with the class filter being Actor. The Loop Body will check if the Array Element either implements the item interface or the dialogue interface. If the actor does, then it will message the Interact event or Dialogue Interact event found in the interfaces.

Health & Manna Potion Interaction Code

When the player interacts with the health and manna potion pickups, a sound will play, and integer variables found in the player character will be set. For health potions, the amount that the player has is 3, and for manna potions, it's 5. Then there is a visibility variable for both potions found in the player, which is set to visible. Then, a reference to the character is used to set the potion icons to visible so that the player can see that they have access to potions and can see how many potions they have left when they consume them.

Spells & Spell System

Spell Casting

New Spell System

Spell System

In Tribulations of the Mage, the player will find spells throughout the levels that they can use. There are currently two types of spells in the game. The first type is basic spells that require manna and can be cast multiple times in quick succession. The second type is ultimate spells that have strong effects, like granting the player invincibility for a short time. Ultimate spells don't use manna, and once cast, they take time before the player can cast them again. The player can equip three basic spells and one ultimate spell at a time. The player acquires spells through spell-interacting items. Once the players acquire a spell, they can press the I key to open the Spellbook to equip a spell in one of the slots. To do this, the player clicks the image of the spell in the Spell Book, and once they do, a selector will appear that will let them choose what slot the spell goes to. The player cast spells by pressing the F, G, and H keys for basic spells and the C key for the ultimate spell. I made the system like this because I wanted the player to be able to mix and match what spells they have equipped to make their ultimate combination.

Magic Bolt Interact Example

Spells can be acquired through interactables called spell pickups that function the same but have a different static mesh and unlock different spells. When the player interacts with one of the pickups, the player character variable is used to set the Spell Information struct. This struct is used to store infromation like whether the spell buttons are enabled in the spellbook and if the spell button is visible in the widget. For example, when the player acquires the Magic Bolt spell, the button for Magic Bolt is enabled, and its visibility is enabled as well. Since I designed Magic Bolt to be the first spell, when I made the other spells, I had to make sure that previous spells would be enabled so that you don't lose spells that you learned. While this wasn't the intended functionality, it does help plan the order of spells that the player acquires as they progress through the game.

Spellbook

In the Spellbook widget, the player can see and select their spells. For Event Contruct, I cast to the player character to get the Spell Information Struct variable. With the struct, I use it to set the visibility of the spell icons and to enable the spell buttons in the spellbook, depending on whether the player has acquired the spells. 

Magic Bolt Button

The spell buttons function the same, with the only difference being the spell and image that the button sets. For this reason, I will go over the Magic Bolt spell as an example. When the player clicks the Magic Bolt spell button, a sound will play, and a spell selector widget will appear on the screen. Inside the Spell Selector widget are two variables called Selected Spell and Spell Image. Selected Spell is an actor class reference that represents the spell that is spawned when the player casts a spell. The Spell Image variable represents the image that appears in whatever slot the player chooses to assign the spell to. Since I selected Magic Bolt, the Selected Spell would be the Magic Bolt blueprint actor, and the Spell Image would be the Magic Bolt image. This is how all the basic spell buttons work. For ultimate spells, instead of an actor class reference, a name variable is set to whatever the name of the ultimate spell is.

Spell Selector Code

The Spell Selector widget consists of 3 buttons called Spell Slot 1, 2, and 3. The Event Construct does a cast to the player character to make a player character reference for later use. When a slot is clicked, it will set a variable in the player character called Spell 1, 2, and 3 with the selected spell chosen from the spellbook. Spell Slot 1 will set Spell 1 in the player, etc. Then the spell image attached to the spell selected from the spellbook will be used to set a variable in the player character called Spell Image. The Spell Image and spell variables in the player are used to cast spells and to be saved in the save game. After that, the Selected Image and Selected Spell variables in the Spell Selector widget are cleared so that the player can pick new spells.

Spell Casting Code

Shoot Spell Notify

Spell Casting Macro

Spell Casting Conditions

Spell casting is handled in the player character blueprint, and the three basic spell slots are connected to the F, G, and H keys. Inside the animation used to cast spells, there is a sound notification that plays when the player starts casting and a notify called Shoot Magic. The Shoot Magic notify is used to call a function called Shoot Spell, which is a part of the I_Character interface and implemented in the player character. The Shoot Spell function uses a Spawn Actor node to spawn a spell represented as the Spawned Spell actor variable. The Spawned Spell variable is set with the spell actor that the player sets with the spell book and selector. When the player presses a spell key, the Spell Index variable is set to 0-2, depending on which key was pressed. Then the Spawned Spell variable is set with the Spell 1, 2, and 3 variables depending on the key. Lastly, the Spell Casting macro is called, which checks if the player can cast a spell, and if they can, the spell will be cast.

Ultimate Spell Experiment

Ultimate Spell System Part 1

Ultimate Spell System Part 2

Rain of Lightning Ultimate Spell Fix

Ultimate Spell Code

Ultimate spells are designed to be spells with strong effects, such as invincibility and high AOE damage. At the moment, ultimate spells do not spawn in actors like regular spells, so they are handled in the player character blueprint and are set with name type variables instead of actor type variables. Ultimate spells use the ultimate bar instead of manna. I'm still deciding how the player replenishes the ultimate bar. Currently, there are two ultimate spells called Rain of Lightning and Iron Skin. Just like with manna and stamina, the ultimate bar has its own set of functions that deal with the reduction and increase of the ultimate bar. 

Ultimate Bar Control Code

Instead of manna, the Ultimate bar uses Ultimate Charge, which is represented by the integer variables Ultimate Charge and Max Ultimate Charge. The functions used to control the ultimate charge function the same way as the functions used for stamina and manna.

Ultimate Spell Selection Code

The Ultimate Spell Selector macro checks the Ultimate Spell name variable to determine what spell to cast. This name variable is set when a player acquires an ultimate spell and chooses one to equip from the spell book.

Rain of Lightning Code

When the Rain of Lightning spell is cast, all enemies within a certain range of the player are targeted and struck with bolts of lightning. This is done by getting all enemies that enter a sphere collision attached to the player and adding them to an array variable called AI in Range. The AI in Range variable is plugged into a For Each Loop node. The Array Element pin on the node is used to cast to the parent enemy, which all enemies are the child actor of. Enemies in range have a lightning bolt emitter spawned on them and take damage. The Completed pin of the For Each Loop node is plugged into a Delay node with a duration of 1 second, which is plugged into the Is Valid node before the For Each Loop. This creates the effect of lighting continuously striking enemies until they die.

Iron Skin Code

When the Iron Skin Spell is activated, a variable called Immune to Damage is set to true, and the material used for the player mesh is changed to one that looks like metal. After a 5-second delay, the Immune to Damage variable is set to false, and the player goes back to their original appearance.

Magic Bolt Code

Magic Bolt is the first spell that the player unlocks in the game. Magic Bolt is a spell that sends out a bolt of energy towards an enemy and deals damage. Magic Bolt is meant to be a simple and versatile spell. The spell consists of a particle effect, a projectile movement component, an invisible cylinder, and a capsule collision. When an actor overlaps with the capsule collision, it will be checked to see if it implements the I_Enemy interface, and if it does, it will apply point damage once and spawn a hit particle effect at the enemy's location. Then the spell actor is destroyed.

Earth Spikes Spell

Earth Spikes Code

Magic Bolt is the first spell that the player unlocks in the game. Magic Bolt is a spell that sends out a bolt of energy towards an enemy and deals damage. Magic Bolt is meant to be a simple and versatile spell. The spell actor consists of a particle effect, a projectile movement component, an invisible cylinder, and a capsule collision. When an actor overlaps with the capsule collision, it will be checked to see if it implements the I_Enemy interface, and if it does, it will apply point damage once and spawn a hit particle effect at the enemy's location. Then the spell actor is destroyed.

Ice Shards Code

Ice Shards is a spell that slows enemies when they hit them. The spell actor consists of four static meshes that are icicles attached to a capsule collision and a projectile movement component that controls movements. When an actor overlaps with the capsule collision, it will be checked to see if it implements the I_Enemy interface, and if it does, it will apply point damage once, the static meshes will turn invisible, and spawn a hit particle effect at the enemy's location. Then a variable in the enemy parent actor called Slowed will be set to true. I tried to make it so that when enemies are damaged by ice damage, they are slowed, but I was having problems implementing that in the status effect code. I was able to make the slow work by having a variable in the enemy blueprint called slowed that the enemy AI behavior tree checks.

Fireball Spell

Fire Ball Code

The Fireball spell is a spell that lobs a ball of fire, and once it touches something, it explodes and deals radial fire damage. The spell actor consists of a particle effect attached to a sphere collision and a particle movement component. At Begin Play, the sphere collision is set to ignore the player when moving because I was having problems where the fireball was exploding on the player character. Since the fireball is meant to explode when it hits anything, I used an Event Hit node instead of a Begin Overlap node. When the fireball hits and the hit actor isn't the player, then an explosion effect will appear at the impact point, and radial damage will be applied with a damage radius of 1000.

Fire Storm Code

The Fire Storm spell is a spell that summons a tornado of fire, and enemies within the tornado are burnt every second for 10 seconds. The spell consists of a particle effect attached to a box collision. At Begin Play, there is a Set Timer by Event node that loops every second. The event attached to this timer is a custom event called Damage Tick. The Damage Tick event uses an array called AI to Damage plugged into a For Each loop that applies point damage with the damage type class being fire damage. The AI to Damage variable is set with the box collisions Begin Overlap node. The Begin Overlap node checks if the overlapped actor implements the enemy interface. If they do, then they get added to the AI to Damage Array. When enemies exit the box collision, then they get removed from the AI to Damage Array.

Water Sphere Spell

Water Sphere Code

The Water Sphere spell shoots a ball of water at enemies that knocks them back. The spell actor consists of a particle effect attached to a capsule collision and a particle movement component. At Begin Play, the capsule collision is set to ignore the player when moving because I was having problems where the water sphere was hitting the player character. If the water sphere doesn't hit an enemy, then it will just explode. If it hits an enemy, then the spell will deal damage and the enemy will get launched backwards.

Fixes and Changes

Fixes and Changes

Enemies

All Enemies in the game are child actors of the parent enemy actor. They also incorporate the enemy and character interface. The enemies in the game are designed to be simple but varied in how they attack. The AI

Enemy Parent Point Damage Handling Code

When enemies are damaged, a status effect is activated depending on the type of damage. Then their health bar is made visible above their heads. Their attack combos are reset, and if they were attacked before they've seen the player, then the Activate AI function is called, which makes the player aware of them and gets them into combat mode. Then the damage is checked again to see if the damage is just normal damage. If it's just normal and the enemy is blocking while facing the player, then the damage is blocked. If the enemy isn't blocking or the damage is elemental, then the enemy takes damage. If damage doesn't kill the enemy, then they will be stunned for a short time. The enemy AI also has stamina the same as the player, and when the AI blocks attacks, their stamina will lower, and they can get staggered if they run out.

Enemy Parent Controller

All enemy controllers function the same, with the only difference being what behavior tree they use and the range set for their attacks. They rely on sight and damage sense for their perception. When the AI senses the player, it goes into combat mode. A reference to the AI is created, which is used in the behavior tree.

Enemy Behavior Tree

Since this project was based on a template that I made tweaks to, I'm just going to talk more about the things I added or heavily tweaked. The behavior tree starts with a selector node with a service attached called BTS_SettingAIBehavior that determines the AI's behavior. The AI's behavior includes patrolling, attacking, chasing, sending the AI to the player's position or last known location, strafing, being stunned, and being idle. To fix the problem of the chilled status effect not properly slowing enemies, I made it so that ice damage sets a variable in the enemy called slow to true. Then, in the sequences of the behavior tree that require the AI to move, a selector and a blackboard decorator ask if the AI is slowed to determine movement speed.

Setting Behavior Code

In the behavior setting service, I added a Remove Slow event that sets the slow variable in the AI to false. This event is called at the end of the receive tick, and it updates the AI's behavior every 0.15 seconds.

Health Check For Block

Inside the service that determines the chance that AI starts blocking, I added a section that checks the AI's health. If the AI is below half, then their chance of blocking increases.

First Enemy AI

Improved First Enemy AI

The first enemy I designed for the game was the Fishman with a wooden shield and a dinky spear. The Fishman can patrol and, in combat, it stabs with its spear, blocks with its shield, and strafes. The Fishman was inspired by the Murlok for World of Warcraft. The Fishman is meant to be a weak enemy that can be beaten without magic and could teach the player about basic combat. I had plans on making stronger versions of the Fishman that had better equipment and were bigger to show that it was stronger. The Fishman and its planned variants would show up throughout the game. This was before I went into the design direction of making levels with different themes and enemies that matched.

Fishman Attacks

Just like the player, the Fishman has four different attacks.

Fishman Attacks

Inside the construction script, I made an array of the different Fishman meshes that get randomly set so that the Fishmen could have a variety of appearances in the level they were placed in.

Second Enemy AI

The second enemy I designed was the Slime. The Slime was meant to be an early game enemy that would be harder than the Fishman. The Slime couldn't block and only had one attack, but it could spit slime balls at the player.

Slime Ball Code

Slime Ranged Behavior

I made a copy of the behavior setting service, and inside of it, I had it so that if the player is out of the slime's melee range, it would either shout a slime ball or run towards the player.

 The Lizardman enemy is a tough and aggressive enemy with high health and damage. The Lizardman has a bite attack that deals high damage. They could come in different versions, and at the moment, I made one that deals fire damage. The Lizardman is an enemy that the player should avoid getting close to and try to take down from range. In the beginning stages of thinking of the story and layout of the game, I envisioned the Lizardmen to be like the Elites in Halo in the sense that they would be the strong warrior-type enemies that could be the backbone of an army.

Lizardman Attack Code

The Lizardman's attack combo consists of a sword slash and two bite attacks. Bite attacks deal twice the damage of the sword slash, making the Lizardman dangerous in close quarters.

Lizardman Bite Attack Code

When the bite attack is called, it plays the Lizardman's bite attack animation. Inside the animation montage is a notify that is used to call Event Launch Bite. Event Launch Bite will draw a sphere trace from the Lizardman's head that will deal damage if it hits the player.

Knight Enemy

©2021 by Malcolm Smith. Proudly created with Wix.com

bottom of page