top of page

Atavysm

Role

Gameplay / Systems Programmer

Description

An energetic hack and slash game centered around an android warrior and her efforts to cleanse a cyberpunk city of corruption that grips the robot populace.​ With a complex and intuitive move set, players are challenged to master their skills and dominate the battlefield in this high-octane, high-tech action game.​

Date

September 2024 - May 2025

Engine

Unreal Engine 5

​

​​

Team Size

21 people

Contributions

Player Combat Overview

I created our game's player combat system using Unreal's Gameplay Ability System (GAS). In our combat system, the player has two weapons, each with three light attacks and one heavy attack. During an attack chain, the player can switch to the other weapon’s heavy attack. This is the core combat mechanic of the game and allows you to use whatever weapon you want whenever you want it, and gives an incentive to switch while still keeping the flow of combat going.

​

The first weapon is the staff, which is meant to be slower, have more AOE, but does not deal as much damage. The other weapon is the swords, which are faster, have less AOE, but do more damage. So, the staff is generally better for crowd control, while the swords are better for attacking a specific target.

​

The combat system went through many iterations, changing as the game changed. At first, each attack was its own gameplay ability (GA). This was useful for giving specific attacks their own functionality. However it became frustrating when we wanted to make changes related to all attacks (which was very often). So, later I changed it so there was just one single attack GA, which kept an internal count of which attack it's on, and if there was functionality needed for a specific weapon, it could be triggered by an animation notify and handled through other means (check out the sword ultimate for an example). Additionally, it cut down on complexity, and if we wanted a new attack in the chain, it would simply be a new enum and animation montage entry, rather than its own GA.

Attacking

The basic attack system heavily utilized the Gameplay Tags and Gameplay Abilities. There are two key "windows" that the system needs to look out for and modify: the combo window and the weapon switch window. The combo window is simply the window to allow a weapon to go to the next attack in the chain. The weapon switch window is the window you can utilize the mechanic to switch to the other weapon's heavy attack mid-combo.

​

I created ComboWindowOpen and WeaponSwitchOpen tags, for which the combo attack GA requires ComboWindowOpen, and the weapon switch GA requires both ComboWindowOpen and WeaponSwitchOpen. Whenever I mention "weapon switch", I will always be referring to the mid-combo chain weapon switch (which is why it also needs the combo window to be open). However, there is also a separate ability that handles a weapon switch not in an attack chain.

So, how do these tags actually get set? That's where animation montages and animation notifies come in. The combat system heavily utilizes these, not just for these tags, but for many other features I will discuss.

​

There are animation notifies for the combo and weapon switch windows, which simply add the tag to the player when hit.​

​

The basic attack is simply handled as an enum with Light1, 2, 3, and Heavy. Since both weapons have the same, both can use this one enum.

image.png

On the attack GA before playing the animation section, if the attack came from a weapon switch, it would jump to the heavy attack. It would then take the enum, convert it to a string, and use that as the start section for playing the montage. After the montage blends out, I increment the enum.

​GAS comes with a very useful ability task: PlayMontageAndWait. However, the Blueprint node itself did not have all the functionality needed; specifcally, I wanted to be able to change the blend in and out arguments. So, I created a custom C++ PlayMontageAndWait ability task. This was mostly just a copy-paste of the original ability task, but added in those arguments and set them before the montage is played. You can see the original task and my task side-by-side.

image.png
image.png
image.png

Montages and Data Tables

I've mentioned the montage a few times now, so let's talk about it more in depth. Montages contain "sections" for each attack. This is extremely useful, because it allows me to manage each individual attack in the entire attack chain in one montage. In the image on the left, the sections are out place, since Unreal unfortunately does not allow you to easily move a section and everything it contains. Thankfully this is fine since it doesn't need to be in any specific order to jump to a section.

​

Now, things get slightly confusing here. Each section I've been referring to are the purple items at the top (Light1, 2, 3, Heavy), which are Unreal animation montage features. However, you may notice that each of those sections also have 3 animation notifies called "Section". In retrospect, I should not have given these the same names as montage sections, since they serve a very different purpose, so I'll call them animation segments here instead.

​

There are 3 segments: windup, swing, and recovery. I made this segment system to be easier on our animators and designers. I didn't want the animators having to worry about how long each main attack part should be in the Maya animation itself, and have to painstakingly change and reimport it when system design inevitably changes (and it did, a lot). Instead, we can just set those segments in the montage. Then, the notify will get which weapon and attack we're currently on, pull the corresponding play rate from the data table, and set the montage play rate to that.

So, what is this data table (DT)? This is the major design data-driven tuning pipeline. It contains all the data for each weapon and each attack to allow easy change and testing to combat. The DT contains two rows, one for each weapon, that have all the information for Light1, 2, 3, Heavy, and some additional weapon-specific data like magnetization and ultimate stuff, which is discussed later.

​

As seen on the right, each segment has a rate and a transition time. The rate is just the play rate I explained above. The transition time is a value that determines how long it takes to transition to that rate. For example, you can see that Light1 windup has a rate of 0.7, but swing has a rate of 1.7. It would look somewhat strange if the player went from a slower attack straight into a faster attack. So, I made this transition time to ease that shift in play rate. This is handled through a separate component that lerps it over time. There's a blend in args and blend out time. This is for the custom ability task I talked about earlier. It also contains data for other things, like hitstop duration, knockback strength, damage, etc.

Input Buffer

The input buffer was a major part of making the combat feel responsive. Its job was to store attack or weapon switch inputs that happened slightly too early, then use them as soon as the next valid combat window opened. Without this, the combat could feel like it was eating your inputs whenever you pressed a button before the combo timing allowed it. In addition to ComboWindowOpen and WeaponSwitchOpen, I also made an InputBufferOpen window. As long as that window was open, the input buffer ability could activate.

​

Since the input buffer was its own Gameplay Ability, it needed to be able to activate from every input I wanted to buffer. GAS normally uses integer input IDs for abilities, so I made a custom solution that let one ability define multiple input IDs, then granted that same ability class for each of those inputs. This let the input buffer ability listen for attacks, weapon switches, and any other bufferable combat inputs, instead of needing separate abilities or separate logic for each one.

​

I also needed a custom C++ to Blueprint bridge to figure out which input had activated it. I needed the FGameplayAbilitySpec to get the InputID, but I could not access that spec directly in the Blueprint. So, I overrode ActivateAbility in C++ and passed the spec into my own ActivateAbilityWithSpec event. Since InputID was still not exposed, I added helper C++ functions to convert the InputID into my custom enum, so the Blueprint could add it to the buffer. When the combo window opened, I took the buffered input and called PressInputID on the Ability System Component to replay it at the first valid time.

Under construction from here. Still need to write about a few other major features like ultimates, magnetization, and finally a retrospective.

PREVIOUS GAME
bottom of page