Project Bongo

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:

  1. A benchmark for my technical and problem-solving skills.
  2. Explore a genre in which I had yet to create a game for.
  3. 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.

A list of C++-based utility considerations. These can be implemented in both C++ and Blueprints.
A list of C++-based utility considerations. These can be implemented in both C++ and Blueprints.
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.

Seeing how the combat plays out with a very weird spawn placement
Method for generating spawn points

Object Pooling

Damage numbers are recycled to improve performance
Damage numbers are recycled to improve performance

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.

Available nodes for the Object Pooler plugin

Tony Nguyen
Tony Nguyen
Technical Game Designer

Tenacious game developer with an unwavering passion for overcoming game development challenges.