Project Overview

Astral Plague 0 is the first iteration of a much larger project by the same name (without the 0). The project began in January 2024 as part of my Digital Media and Interaction class as Kennesaw State University. Our team was tasked with choosing a game and adding a twist to it. Our team opted to choose Sekiro: Shadows Die Twice with the twist of demaking it(ie. making it 2D with pixel art).

Technical Game Designer

I was responsible for all of the game's design and implementation. I also acted as the team lead, organizing meetings, sending out deadlines, and ensuring our team had a coherent plan.

  • Authored a bespoke ability system in Unity 2D using C# and a custom gameplay state system allowing for improved enemy and gameplay design.
  • Directing a team of 5 developers, animators, and artists by outlining core game features, narrative, and technical requirements.
  • Championing the design of integral gameplay mechanics and gameplay style by implementing in-engine with C# and custom visual nodes.
  • Writing extensive narrative and story information by planning narrative beats, major locations, and characters.

There is sound, I recommend it primarily for the second clip. The music on the left clip is from Elden Ring, can't take credit for that.

Combat Design


Game Feel in 2D

SoonTM



Getting Technical - The Ability System!

Coming off the back of working in Unreal Engine for the months leading up to this project, I wanted to challenge myself to make a bespoke ability system somewhat similar to Unreal Engine's Gameplay Ability System. There were two main reasons for this challenge:

  1. Challenge myself and improve my technical skills.
  2. Have greater flexibility when designing gameplay features.
With that said let's dive into the ability system's execution flow.

Ability Execution flow

Abilities execute in a linear fashion. Every input has an associated ability defined in their combat class. For example, LMB would active the player's basic attack and a string passed from the enemy BT would mimic this same interaction, activating its basic attack as well.

Once the ability is called we then proceed to enter a series of checks, primarily ensuring the player or enemy's state is valid -- the enemy can't be dead! Before changing the active ability we store the current abilities state if we need to defer back to it at some point (in the case of a combo or failed execution). Those checks are defined in the default ability using enums to list the valid and invalid states for execution.

Assuming all the checks were passed we now look to the ability itself, calling a function on the scriptable object that executes any preliminary processes (i.e. checking location or trajectory). This function then sends any relevant data back to the Combat component as some functionality is restricted on Scriptable objects and can't be executed directly on there.

From there I use existing functions I previously defined, like a damage checker, that takes the input we just grabbed from the ability class. Once all that's done we access the ability on last time to verify there isn't any remaining checks or game feel stuff we need to do -- usually I play some SFX. The structure is surprisingly simple but offers a lot of functionality really quickly.

Detailed Ability Structure

All abilities in Astral Plague: 0 are derived from a Scriptable Objects class called DefaultAbility. This ability holds a few key points of data: collider size, collider position, damage type, and gameplay cue functionality (i.e. sounds or VFXs).

This information is lightweight but versatile, as all abilities could function off this information alone. However, some entities expand this functionality for added effects. One example is the subclassed ability called Auriel_WaveAttack. This ability adds a beam/wave on the end of the attack, effectively spawning a projectile. This functionality is not supported by the default class but can be quickly added with a subclass. This is because of the aforementioned execution flow which always calls the "ExecuteAbility" function on the scriptable object.

Most abilities don't exceed the established functionality and at most will simply include more data for additional SFX, VFX, or some other data. I will point out that I haven't mentioned animation at all. That's because animation is part of the initial ability execution and activates based on the ability name. This means that whatever the ability name is, the system will attempt to play an animation clip of the same name. This, to a degree, decouples the animation system from the ability system and allows for simple string changes for infrastructure changes.

States and why I needed them

One of the big reasons I wanted to do an ability system was for greater control over my entities states, as the Gameplay Tags system from Unreal Engine is brilliant. More soon!

The Mistakes

I would be lying if I said this project wasn't full of hurdles and mistakes, because it was. I want to take a brief moment to point out some mistakes I made and how I would do better next time.


Enemy Variety

Early on when we decided to work on a demake of Sekiro we knew we wouldn't have time for many enemies, in fact we originally only planned to have a boss and a training dummy the player could practice on. This was primarily for scope reasons but also because of time constraints -- we had a lot to make in a short amount of time.

While from a pure developmental perspective I think this worked out, the final product ended up being slightly stale as the player can breeze through the level without fully utilizing all their mechanics. The most obvious example of this is the parry, our central mechanic. Aside from the boss, the other main enemy we developed was a floating eyeball that would periodically attack in a circle around itself. It was simple to implement and gave us some content, but it wasn't fun. Simply put, the player never needed to parry this enemy.

This was exacerbated when we decided to create a second version of this enemy that was ranged... meaning you couldn't parry it at all. Sure you could block its attack, but it wouldn't actually make use of the enemy's posture bar. This meant that the two main enemies both ignored the parry system almost entirely. So how did we end up here, well honestly it was partly my fault but also due to the number of animators we had. Our team was blessed with two talented animators who split the work. Our first animator would work exclusively on the player, doing all the attack, walk, and jumping animation (as well as others). While our second animator would then work on the boss, and when she had time also try and get some enemies done.

What we needed was another animation, but we also needed better core designs for the two enemies we had. Simply put, the enemies we did make didn't cut it. They were cheaply made and we knew that. However, from a pure design POV I could've done more. For one, the ranged enemy could have a deflection mechanic added. This would've allowed the player to not just parry its attacks, but also deflect them back towards the enemy. While this wouldn't have solved the overall engagement issue, it would've helped to a degree. Second, we should've relied on our existing resources to create more enemies. Practically, this means recoloring the player model (or another's) to make another enemy with our existing animations. WE also could've tried to rely on outsourced animations from a marketplace, but that would've been questionable.

Left depicts the "Training Dummy" while right displays the ranged enemy.

Element Design

Part of the theme of this class was that we would pick a game and add a twist to it and then our professor would do the same mid way through the semester. This is a great idea in concept, but given the already pressing time crunch things got dicey.

For our project we were given the twist "Mega Man", does that make sense to you? Well it didn't to our team until we decided to take it literally... make your game like Mega Man. Thankfully we had a MegaMan modder and expert on our team to help us out. This lead us to the twist of giving the player three elements they could swap between: Fire, Ice, and Lightning. Each element had its own capabilities like fire dealing burn damage, ice dealing posture damage, and lightning adding a ranged attack on the tail end of your combo.

This was actually really cool and somewhat easy to implement as most of the elements worked within the Ability System I designed and were simply recolors of the base model (i.e. a blue sword)


Where we begin to encounter problems is in with the functionality. All three elements worked great implementation wise, but there was a clear winner in terms of strength: fire. The fire element offered significantly higher DPS than the other elements and somewhat trivialized combat. I was able to solve this somewhat with some careful balancing, but the problem remains to an extent. The second issue arose as a result of not having a clear cut manner of activating these elements.

Originally the idea was that the player could parry X amount of times to unlock the element temporarily (i.e. 30s activation), but due to time constraints I was unable to do this. This mean that the player could freely, at any time, toggle between the three elements. Now realize for a moment I said three elements, but there is actually four. The default attack is technically an "element", but it just doesn't have any buffs. This meant that the original attack combo was now completely useless.

The obvious solution to this is the implement the aforementioned system which further emphasizes the parry mechanic, but I think the problem goes further than that. I would probably argue that the ice element is by far the best designed, as it makes creative use of the posture system while not adding any significant overhead. Compare this to the other two, fire and lightning, which arguably require specific scenarios to be useful (even though fire did the most damage). To make proper use of these elements we need a lot more content. But look, I don't want to keep saying "We had no time!" as that doesn't help anything. With what we did have, what could I have done better?

Well for one, we should've only added ONE element and focused on its implementation. Like I mentioned I think ice was the best, so if we focus specifically on how the player can use that element we get a lot more bang for our buck. For example, the player could've had a portion of the level where they needed to freeze a platform to reach a higher point or perhaps they need to slow down an enemy to escape, there is a lot we could do. This also would've made for a more cohesive experience as the player would be forced to engage with our systems to a higher degree, instead of cycling through the elements at random.

Working with constraints, not against them.

I don't like Unity. That probably isn't a shock to hear in 2024 and beyond, but honestly they don't have enough native resources to make me regularly use their engine. It is somewhat strange too considering they are the go-to engine for 2D yet many of their tools don't work there, like NavMesh. This is furthered when you consider they don't have a native behavior tree/Ai architecture system like Unreal has (two actually, BTs and StateTree). I don't want to complain though, so let's jump into the problems I encountered and how I fixed them.

First off, behavior trees. This was one of my biggest challenges as our game revolved around combat. We needed complex and interactive AI otherwise combat would cease to exist. To fix this I immediately launched into developing my own BT system. At first I was planning to do it raw, go full c# with no visual interface, and this worked for a little while. However, as the complex of my AI spiked I needed something better to work with. From there I turned to the glorious work of open source where I found a visual BT system for Unity, but it still needed work. While the system itself was fine, a lot of the nodes didn't work the way I needed them to so I had to do some overhauls to better support 2D NavMesh and attack combos. None of what I did was fancy, but it got the job done. I was however very unsatisfied with how I ended up implementing states (i.e. attacking, dying, running) as they will sometimes break and cause the AI to stop moving.

The second major limitation was not having a NavMesh that natively worked in 2D. While I considered working with pathfinding algorithms like A*, that seemed like it would take time I didn't have. Once again I turned to the world of open source, but like last time I still had to make changes. Namely, the 2D NavMesh system that I found didn't support "flying", or at least not in a way that made sense. In fact the 2D NavMesh I found wasn't even for side scrollers, it was for top down games. Through a series of not so pretty code tweaks and some finagling with game objects, I managed to make a NavMesh that allowed for two distinct layers: ground and air. It might not seem complex... but it was challenging to get working in 2D.