The Howling is a first-person horror roguelike about descending into a cave of unnatural horrors with procedurally generated levels, gaining powers as you progress and fighting off monsters to make it to the exit. This was a 4th year group project where I was a programmer and the project manager, alongside another programmer and two designers.
The Howling was the final project I worked on for university, being a 4 month long group project. While I hadn't intended to, I ended up being the project manager as I found myself being the most motivated and experienced member of the team, and was excited by the idea the first two team members had pitched to me - a first person action roguelike with a spooky, supernatural theme, set in a subterranean cavern. To try and make this the best project I could and set us up for success, I started by setting up source control and adding extensions and tools I had found over the years to the base project. I wanted this to be a clean and organised project with good architecture, making use of the knowledge I'd gained over the last few years to take point on programming some of the core elements and to write systems for use by my teammates.
My first port of call on the project was to work on the first-person player, as that could be done in isolation from other systems and was important to get working as soon as possible. The player object uses a CharacterController for its collision and movement, and everything is handled by three scripts - Player, PlayerController, and PlayerViewmodel. The Player script is the top-level reference for the player that everything goes through; it holds references to other components, sounds, effects; useful functions and utilities; and handles things like health and dying, which it inherits from the Entity component that it shares with enemies. It handles essentially anything to do with the player's functioning that can be accessed by other scripts. The PlayerController script is how the player controls the character, responding to inputs and handling movement. Player movement is very simple but has some small things to improve the feel, such as snapping the player to the floor for smooth traversal of ramps, and jump buffering so that the player will still jump if you pressed the button shortly before actually touching the floor. The controller also handles picking up, using, and swapping weapons, and holds an array of the weapons the player has. The PlayerViewmodel script handles everything to do with the player's visuals and animation, including the visible arms and weapons, and the player's view camera with convenience properties for the origin and where the player is looking. The viewmodel script has an array of weapons that is kept in sync with the controller's array, and is used to update visuals for swapping weapons, how the weapon is held, and has several functions for playing different animations for use by other scripts. Through these scripts I was trying to apply OOP principles and making use of encapsulation to separate parts of the player into different components, which I think worked well as the scripts are all fairly straightforward and it's easy to understand their function.
For this player controller I also spent some time figuring out how render the first-person view in front of everything else, so that when up close to the environment the player's model wouldn't clip through it. My initial idea was to just have a second camera that rendered the player's view on top of the world, but I found that this meant lighting information would be incorrect. Shadows from the environment would not cast onto the player, and any lighting from the player would not affect the environment. Unhappy with this, I did some research on possible solutions. One was to use a custom shader for drawing the player view in a way that didn't clip, but this still felt like an unsatisfactory solution, as the shader would have to be applied to every thing moving in and out of the player's view, and it seemed like a hacky approach to what should be a simple need. The solution I eventually found was to use Unity's universal render pipeline, which allowed me to change how the player view was rendered while still keeping all the same lighting information. This kept the rendering to just one camera still, and made it so that changing how something was rendered was as simple as changing its layer.
My next focus was to work on the weapon system and making entities take damage. The player can attack enemies with weapons that they find randomly distributed in the level. They can hold one melee weapon, and one ranged weapon. Every weapon object has a weapon script that inherits from the abstract Weapon class. This class holds information on things like damage, cooldown between uses, the target layer for enemies etc. Every weapon has a model and the colliders that define its shape, and a rigidbody for physics interactions. Weapons have two states - being in the world, and being held. When in the world, they have physics turned on, are on the normal rendering layer, and can be targeted to be picked up. When held, its rigidbody is stopped from doing physics and its colliders are set to ignore the player. The layers of itself and all its child objects are set to the layer for first person rendering, so that they render correctly with the arms. Weapons all have a cooldown which can be adjusted by other scripts. The raw cooldown value is private and unaffected, instead using a CooldownLength property, which uses its 'set' method to adjust a cooldown modifier variable instead, and returns the raw cooldown with the modifier added. This allows the cooldown to easily be reset to its original value, and also enables a CooldownMultiplier property to give the relative speed change, which is useful for adjusting animation speeds.
Melee weapons use the MeleeWeapon script, which has a reference to the collider used for detecting hits. When the weapon is used, the collider is turned on and detects colliders that enter the trigger that are on the correct target layer. To avoid multiple collider hits detecting the same enemy, a hash set of GameObjects is used to store each enemy hit in a single attack, ignoring those already hit. The collider is then turned off at the end of the attack, and the set of hit enemies cleared. Ranged weapons use the GunWeapon and ProjectileGunWeapon scripts, which have an ammo count, and for the projectile weapons, a reference to the projectile prefab that will be spawned. Normal guns when used perform a raycast from the camera centre, check whether the collider hit was on the correct target layer and if so, damages the enemy. Projectile weapons spawn a projectile at the end of the weapon, pointing it from the weapon out towards a very far point in the distance so it shoots forward correctly. The projectile has a ProjectileAttack script which the weapon initialises with attack information and a physics mask for what to hit, it detects if it enters an enemy's trigger collider, and then damages them.
To facilitate the power up system we wanted, I had to think about how to implement damage and attacks, as a naive implementation of directly adjusting damage variables could quickly get messy. For this purpose I made an AttackInfo and HitInfo classes, instances of which could be passed through an event bus of different gameplay events to be adjusted, and the enclosed information used to identify what was occurring. AttackInfo has an entity source (the attacker), a weapon source, a raw damage value, and the ammo cost value. Raw damage is the initial damage value given by the weapon, but this is encapsulated and can't be adjusted directly from outside the class. When changing the damage, for example with an effect that increases damage after a kill, there are two public variables that can be adjusted - DamageAddition, and DamageMultiplier. After adjusting one of these, the actual damage is gotten from a Damage property, that returns the raw damage multiplied by the mulitiplier and with the addition on top. This avoids direct messing with damage and avoids unintended effects of multiplying or adding damage in undefined orders. HitInfo is similiar to AttackInfo, with variables for the target entity, the collider that was hit, the damage, and the weapon source. When an enemy is hit and a HitInfo instance is passed to the event, effects can do things like check with the enemy if the hit collider was a critical collider, and increase the damage of the hit. Having the weapon source is also useful to do things like check whether the hit was from a melee or gun weapon, as melee weapons can't critical hit.
I created the enemy AI system with the goal of making it easy for my teammates to put together behaviour for the enemies they were working on without cluttering up the Enemy script, and could be done within the editor as much as possible. For simplicity's sake, I went with a finite state machine approach, using UnityEvent events to architect the different states' behaviour. Every enemy has an EnemyFSM script to run the state machine, which holds a list of states. Each state is an EnemyState object which has a name, along with events for entering, exiting, and running the state, as well as exit points from that state to other states. The events for the state use a Unity event class that takes a reference to the Enemy component, which the method registered to the event can then use to call the desired functions on the enemy. Using Unity events allows the functions that are run to be assigned in the editor using an AI_Functions script that's also attached, which has all the functions used by the states. Functions to be used are added to that script, and then can be selected in the editor for different events. Every update, the FSM checks whether to exit the current state, and then runs the main logic for the current state. For exiting a state, there is a list of ExitPoint objects, which have a target state and another Unity event that takes the enemy reference, the state machine reference, and a string for the target state. Because events can't return a value, these exit point functions check whether to change states, and then use the references to call on the state machine which state to change to. These exit functions are also assignable in the editor as long as the written function matches the event signature, allowing for specific functions to only be valid to be assigned for the state events or the exit point event. For example, in the editor an enemy can be given a chase state, which runs the chasing logic on its state event. This then has an exit point to the attack state, which has an exit event assigned for checking whether the enemy is close enough to the player, and if true then it calls to change to the attack state. These functions only have to be written once in the script of functions and can be mixed and matched with in the editor to create the desired behaviour. The same functions can even be used agnostic of the intended state, e.g. using a check for whether we've reached the player could be used for different exit points to different states, and not just one state.
This project was a challenge on multiple fronts. Working remotely due to the pandemic without in-person meetings or work sessions made it harder to hold people accountable as project manager on what was or wasn't being done, and the general standards of the team's work were below expectations. I failed to account for the imbalance of members we had and didn't push back on the idea of a game that was very much art-heavy in a team of three programmers, one of which had to take on art responsibilities to make up for the lack of art output otherwise, making a big difference in production.
As far as personal programming, I'm largely happy with what I was able to do in this project. I put to use systems I had learned about, got to get my hands dirty with making more complicated combat and player mechanics than I had been able to in the past, and learnt a lot in the process about what did and didn't work. The enemy AI worked fairly well but was slightly over-architected and had some confusing code paths that made reading the code harder, but I'm happy with the implementation of using it in the editor, and can see easy ways it could be improved, like adding more conditions for whether a state can be moved to or from. I was especially happy with how the player controller turned out, making the first person rendering work and having solid player scripting was very satisfying; but I can still see room for improvement with how things like weapon storage was handled, where it should've been consolidated to one component rather than spread out and needing to be synced when changed.
While this isn't a project I'm the proudest of, it was a deeply invaluable learning experience both for my programming knowledge and experience, as well as my leadership and project management skills. It's something I often reflect on when thinking about current and future projects for whether the scope is sensible, how to distribute work based on available team skills, and the benefit of pre-production planning that could've avoided some of the mistakes made.