Engage in thrilling and strategic combat alongside your loyal companions, utilizing their unique abilities to overcome formidable adversaries.
Highlights
- Extended Unreal Engine’s Gameplay Ability System to work with turn-based abilities and effects
- Implemented a custom Utility AI plugin for modeling the behavior of creatures in combat
- Developed a custom solution for inventory management
- Integrated pooling system to optimize resource management
Overworld and Combat Gameplay
Overview
Project Bongo is a game I’ve developed which serves several purposes:
- A benchmark for my technical and problem-solving skills.
- Explore a genre in which I had yet to create a game for.
- Further expand my knowledge of Unreal Engine’s toolchain.
Combat Design
The combat mechanics were inspired by a few turn-based games (e.g. Saiyuki: Journey West, Honkai: Star Rail, Shadow Hearts: Covenant, etc.) I have played or researched that had me curious about a combination of their elements.
The challenge in this aspect was trying to combine these elements in a way that could make sense for my envisioned game setting without making the combat feel like a hodgepodge of the aforementioned elements.
Gameplay Ability System (GAS) Implementation
Developing a cohesive and flexible solution for abilities and effects can become unwieldy, which was why I was excited to experiment with the GAS framework.
At the time of development (using UE 5.2 ), I had to create a workaround for turn-based effects since Gameplay Effects would only work well out of the box for realtime gameplay.
I originally tried using the built-in stacks from the GameplayEffect
class to track an effect’s remaining turns, but it did not work well with effects that needed to stack.
So, I created a system that would filter certain effects that have “turn-based” gameplay tags and keep track of handles that belong to those effects.
Filter applied effects for turn-based effects
// This is a callback that receives and filters every GameplayEffect that is applied to the owner ASC for effects that have the required "turn-based" GameplayTags.
void UBongoTurnBasedGlobalAbilitySystem::HandleGameplayEffectAppliedToSelfASC(UAbilitySystemComponent* ASC, const FGameplayEffectSpec& Spec, FActiveGameplayEffectHandle ActiveHandle)
{
Super::HandleGameplayEffectAppliedToSelfASC(ASC, Spec, ActiveHandle);
if (ActiveHandle.IsValid())
{
if (const UGameplayAbility* AbilityInstance = UBongoAbilitySystemBlueprintLibrary::GetGameplayAbilityInstanceFromActiveEffectHandle(ActiveHandle))
{
if (const UBongoTurnBasedGameplayAbility* TurnBasedGameplayAbilityInstance = Cast<UBongoTurnBasedGameplayAbility>(AbilityInstance))
{
FGameplayTagContainer AssetTags;
Spec.GetAllAssetTags(AssetTags);
// Return if it's not a turn-based effect.
if (!AssetTags.HasTag(TurnBasedGameSettings->Tag_TurnBasedEffect)) return;
const bool bTracked = FBongoActiveTurnBasedEffectSpec::GetGlobalMap().Contains(GetTypeHash(ActiveHandle));
const int32 Turns = TurnBasedGameplayAbilityInstance->GetTurnBasedEffectDuration(ActiveHandle);
// Create new effect spec.
const FBongoActiveTurnBasedEffectSpec NewSpec { ActiveHandle, Spec, Turns };
// Skip if continuous, non-stackable, active handle is already tracked
if (bTracked && (NewSpec.IsContinuous() && !NewSpec.IsStackable())) return;
// Prevents ticking effects applied just now. We defer their first tick to the next tick.
if (bTicking)
{
NewEffectSpecsFromTick.Add(NewSpec);
}
else
{
NewSpec.AddToGlobalMap();
}
}
}
}
}
Tick method for turn-based effects
void UBongoTurnBasedGlobalAbilitySystem::TickTurnBasedEffectsForQuery(const FGameplayTagRequirements& TagRequirements)
{
bTicking = true;
for (auto PairIt = FBongoActiveTurnBasedEffectSpec::GetGlobalMap().CreateIterator(); PairIt; ++PairIt)
{
if (!TagRequirements.RequirementsMet(PairIt->Value.GetEffectAssetTags())) continue;
FBongoActiveTurnBasedEffectSpec& EffectSpec = PairIt->Value;
if (EffectSpec.Tick())
{
if (OnTickTurnBasedEffectDelegate.IsBound())
{
OnTickTurnBasedEffectDelegate.Broadcast(EffectSpec.GetActiveGameplayEffectHandle(), EffectSpec.GetTurnsRemaining());
}
}
}
// Tick all effects that were recently applied. Doesn't matter when they tick based on query.
for (auto PairIt = FBongoActiveTurnBasedEffectSpec::GetGlobalMap().CreateIterator(); PairIt; ++PairIt)
{
FBongoActiveTurnBasedEffectSpec& EffectSpec = PairIt->Value;
if (EffectSpec.WasRecentlyApplied())
{
if (EffectSpec.Tick())
{
if (OnTickTurnBasedEffectDelegate.IsBound())
{
OnTickTurnBasedEffectDelegate.Broadcast(EffectSpec.GetActiveGameplayEffectHandle(), EffectSpec.GetTurnsRemaining());
}
}
}
}
bTicking = false;
// Clean global map to prepare for next tick
FBongoActiveTurnBasedEffectSpec::CleanGlobalMap();
// Deferred effect spec ticks can now be added to map
for (FBongoActiveTurnBasedEffectSpec Spec : NewEffectSpecsFromTick)
{
Spec.AddToGlobalMap();
}
NewEffectSpecsFromTick.Reset();
}
Utility AI Implementation
The combat AI was one of the bigger hurdles to overcome. The challenge here was to implement a decision-making AI that can select a sequence of abilities by accounting for various factors:
- Magnitude of the action (e.g. prefer big damage, heal, etc.)
- Target elemental affinity
- Prioritize damage-dealers, healers, buffers, or debuffers
- and many more…
So, I created a custom Utility AI plugin with Unreal Engine.
Implementation for AI selecting actions
StateTree Usages
Team Spawner
Initially, I would manually place spawn points in the world where I wanted to spawn in the creatures. Over time, it became unnecessarily time-consuming having to re-position the spawn points whenever I wanted to make changes. So, I implemented a system that would determine the placements at runtime given a spline.
Method for generating spawn points
Object Pooling
I rolled out my own object pooling system using C++ to reuse commonly-allocated resources like the text popups. My implementation can be viewed here.