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 reusable solution for inventory management across multiple game systems
- Created tools for streamlining the creation process for character assets
Gameplay Trailer
Full Demo Gameplay
Overview
Beings Beyond is a turn-based action game where you tame and fight alongside creatures that will aid you in your journey of restoring a long-abandoned planet.
Combat Design and Implementation
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.
Tap-timing
Elemental Affinities and Reactions
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
Inventory System Integrations
I used my custom Inventory System Plugin to implement the following game systems.
Demonstrations
StateTree Usages
Video
Tools
Below are some editor tools that have greatly increased QoL regarding asset validations, creation, and modifications.
Creature Creator
Skill Event Editor