Magic FPS/Platformer

GitHub

Overview

This was a project I worked on for 3 months from October 2021 to December 2021.

For this project, I was given the task of creating a game which had an overworld that had to be navigated to reach one or more encounters with examples being given of the map screen and levels present in Mario Bros 3 and the star map and battles presented in FTL: Faster Than Light. With the graphical theming and the rest of the ideas of the game being left up to me. 

At the time I had been playing a lot of Paladins and Valorant and was particularly interested in creating a first-person shooter game but instead of the player using a gun, they would use some sort of psychic magic. I then built a vague story about the player being the next evolution in the human species, who is being experimented on in a secret facility from which they are trying to escape. The final game plays as a sort of hybrid between first-person shooter and platformer.    

The game contains 5 levels in total.

Movement and Abilities

As I chose to do a Mario Bros style of game with the overworld as a level selection map and the encounters as my levels, I began by fleshing out the encounters. I created a first-person character controller which uses traditional WASD for movement in the four cardinal directions as well as spacebar to jump. Taking inspiration from Jett in Valorant, I made it so that if the player holds the spacebar as they fall, their descent is slowed allowing them to mitigate fall damage. The only other special movement present in the game is when the player (or an enemy) touches an ice sheet they have an additional force applied to them which causes their movements to become slippery. 

I then turned my focus to the player's abilities. The primary fire is a spherical projectile which is fired in the direction the player is looking and is unaffected by gravity. I then created a secondary fire option which is a conjured sword. This does close-range melee damage and the player can toggle between the two modes. The conjured sword does more damage to compensate for its shorter range and is necessary for progression through the game as it is used to destroy the magenta cubes placed throughout the levels which often form roadblocks. If the player gets a headshot on an enemy with either weapon a boom particle effect is emitted from the enemy to signify increased damage and when an enemy dies a blood/electrical effect is emitted from the place the enemy was killed after they have disappeared to represent a kill. 

The first of the player's other two abilities is an extensible vine which can be fired from the player at a wall, floor or enemy to pull the player towards that target. This is the ability on Q in the bottom left corner and it was inspired by one of Vora's abilities from Paladins. This is done by manipulating the vertices of a cuboid which form the vine and applying a force to the player towards the point where they aimed. The player is able to tell if they can target a given surface based on if there is a red cross in the middle of the screen (which means untargettable) or a green arrow (which means targettable). The red cross also appears when the ability is on cooldown. 

The last ability launches a targeted enemy straight up into the air and is the ability on E in the bottom corner. This causes enemies to take fall damage when they land. As the enemies in this game use Unity's navigation grid for movement, this made the implementation of this ability quite challenging as I encountered many null pointer errors when I removed the enemy from the mesh. In order to overcome this I had to introduce a bool which would track if the enemy was touching the navigation grid at a given moment and then check against this bool before performing actions. Similarly to the previous ability the player knows that they are able to use this ability when they look at an enemy and see the pink swirling particle effect. This effect does not appear when the ability is on cooldown. 

Player Movement Code

PlayerController.cs

The first thing to not about this function is that I don't create functions like this anymore. This function is too long and has multiple functions. In more recent projects when implementing a similar thing I would have moved the player inputs to a separate function, jumping to a separate function, the falling to a separate function and the resolution of forces to a separate function. But for my first project, this was a decent attempt.

Secondly, it is littered with comments for the sake of having comments. In more recent projects I only comment if I think the code is unclear at a glance.

This function takes user input and converts that to a horizontal movement vector. Then it checks to see if the player wants to jump. If they do a jump force is added to a separate vector. Gravity is then added to the jump vector making this a force vector. Gravity will be reduced if the player is holding space bar while in the air. The distance the player is falling is also cached for a fall damage calculation in a separate function. Air resistance is added to the force vector to have a similar effect to Gravity on horizontal movement. I now know this is a damping force. Like gravity, this would be handled if I were using a RigidBody, however, as I am using a CharacterController I have to implement this manually. Finally, I created a third movement vector for adding slipperyness when the player is in contact with Ice. These three vectors are added together at the end for the movement. They could all be the same vector. Slip velocity is just a multiplier of the initial movement meaning that the initial movement could be multiplied by 1 or 1.015 using a ternary operator and then the jump force, gravity and air resistance could be added afterwards. I would approach slip velocity differently if I were to do it now. I would use the physics material to change the friction between the player and the surface they are walking across. 

Projectile and Melee Abilities Code

PlayerController.cs

Projectile.cs

Sword.cs

The projectile attack object is spawned and given a velocity. When it collides with something it checks if it is an enemy or something else and then damages the enemy if it is an enemy. After it is destroyed. 

The sword attack is an object attached to the player that gets toggled on and off. It has a collision box. When its collision box intersects with an enemy it damages it. When it collides with one of the pink psychic boxes it destroys it.

The main thing I would approach differently if I were to do this project today would be the use of co-routines. At the time I preferred using them for tasks which took a deterministic amount of time such as cooldowns. However, I have since developed a preference for using a float which gets decremented by dt each frame in most situations where I would historically have used a co-routine. I now only use co-routines for things that need to repeatedly occur at a set interval and have no effect on anything else taking place on the main thread the main two places I have used this in other projects are for recording position, rotation etc for an object so it could be rewound later and for flickering the colour on a sprite to denote a poison. The main two reasons I stopped using co-routines so liberally were because they were not truly parallel like threads so there was not much point in using them over just using the main thread and trying to access things occurring in the co-routine from other places is an absolute nightmare and can cause all sorts of state inconsistencies. 

Vine Ability Code

PlayerController.cs

In this function, if the player selects the key to use the ability and the cooldown is over then the function is run. A check is made first to determine what the vine would attach to if there is an appropriate surface within the max reach of the vine. There are multiple inefficiencies and redundancies in this code. For example what the vine collides with is already checked in the CheckVineAttachment function so checking it again in the function which calls this function is unecessary. Secondly, all of the branches in the nested if statement found in the CheckVineAttachment function do the same thing except for one. These conditions should be collapsed into a single condition with && placed between and then everything else would just run the else block. This would reduce the likelihood of logical errors occuring due to code duplication. 

After this process, the vertices of a cuboid are created dynamically between the player and the attached surface to simulate the vine. At the time I remember making the choice between a line renderer and a cuboid mesh for this and the reason I chose to use the cuboid mesh in the end was that the cuboid looked more convincingly 3D and helped the game to look better visually. Throughout the duration of this ability, another function is run which updates the player's end of the vine mesh so that the vine shrinks as the player is flung towards their target.

AI

There are two types of enemies present throughout the game: the guards, represented by the women with guns in black and yellow outfits and the small robots. Both types of enemy use the navigation mesh to move through the scene. The guards will stop once they are within a certain range of the player, however, they shoot constantly whereas the small robots come all the way up to the player as the robots only do melee damage and not ranged damage. As the robots are so small and they are placed in the levels in large groups, a boid algorithm is implemented to encourage swarming behaviour when attacking the player without them getting in each other's way. 

UI

UI was a huge part of completing this project. I created all of the assets for the UI, except for the font, myself and this took up more of my time creating the project than I expected. I tried to keep HUD elements to a minimum with a simple set of icons in the bottom left corner which represent each of the player's abilities and a health bar on the right to display the player's health. The abilities have their default key binds placed above to remind players how to select an ability and the health bar changes from green to yellow to red as the player loses more and more health.   There is also a hint in the top left corner, encouraging players to press I. This opens the menu where the player can access the settings, how to play or return to the overworld/main menu depending on if they are in a level or the overworld when they accessed the menu. In addition to this, when the player takes damage, a red vignette is applied around the edge of the screen to highlight to the player that they are taking damage.

One of the most interesting parts of UI design for me was trying to implement a way to get information to players about powerups without putting too much text on the screen, especially considering that this text would not be part of the HUD but instead an in world object with text on it. I decided to not have the text present on a power-up when viewed from a distance but once up close the text would appear, replacing the golden particle effect, and displaying the player a description of the power-up and how to collect it. The canvas on which the text sits is updated to face the player's head (which is where the camera is located) in a similar way to how the enemies rotate their entire bodies to face the player. 

In addition to the HUD and powerup text present in the game I also designed all of the menus though these were far simpler as it just involved creating a collection of canvases with buttons on and linking the enabling and disabling of the canvases to buttons and key presses.

Overworld

The overworld in the game is a lot simpler than the levels. It is a 2D world and the player controls a picture of the disembodied head of the player that is controlled in the levels. The grey regions represent the corridors with the padlocks in the corridor providing a method for the player to enter a level. Levels are unlocked sequentially and before they are unlocked they appear as locked padlocks. Once unlocked they appear as unlocked padlocks. When the player goes near a padlock a similar thing occurs to when a player goes near a power-up. In this case, the entire padlock is replaced with a small box giving the player instructions on how to enter the level or on which level they need to beat to access the level they are trying to access.  When a level has been completed, the padlock turns gold though it remains open, giving the player the opportunity to play a level again. As power-ups are kept between levels, players can replay a level to get more power-ups to overcome challenges in later levels. All power-ups except for the first are randomised though so there is no guarantee about the boost a player may receive.  

Between levels, the player also unlocks bonus levels. These bonus levels display a menu where the player is presented with the monty hall problem. If they are lucky they will recieve a random power-up, however, if they are not they get nothing. These bonus levels are not replayable.