VIDEO

Stat Modifier System
Audio Programming
Enemy AI
Unreal Engine & C++

ABOUT

Viscerate was my third game project at FutureGames.

It was the first project where I made a system in such a way that designers could easily create dozens of different upgrades by themselves.

INFO

ROLES: Gameplay/Tools Programmer

ENGINE: Unreal Engine 5.5

DURATION: 7 Weeks

TEAM SIZE: 16

VERSION CONTROL: Perforce

Systems Index

STAT MODIFICATION SYSTEM

A modular stats modification system, used by designers to easily create dozens of modules that change specific stats (health, reload time, etc.) based on editable triggers (enemy hit, player jumped, etc.).

setting a module to modify stats based on a chosen trigger

The system makes use of “Modules” and “StatModifiers” to modify the stats of a given actor. Stat Modifiers do the actual modification, while Modules apply given Stat Modifiers to an actor based on editable triggers. Designers can create and edit modules and stat modifiers to their liking, describing how given stats should be modified.

Anything needing stats modifiable has a “StatHandlerComponent”, holding any “StatModifiableComponents” (has the stats to modify). The Stat Handler is what modules use to push a stat modifier onto a given stat modifiable component and thus apply the modification.

the module from the previous gif in action

StatModifier.cpp: Init() Apply() Revert()
void UStatModifier::Init()
{
	CurrentDuration = Duration;

	if (DoImmediateModificationWhenApplied) CurrentTickInterval = 0;
	else CurrentTickInterval = ModificationFrequency;

	AmountModified = 0;
}

void UStatModifier::ApplyModification(UStatModifiableComponentBase* StatModifiableComponent)
{
	if (StatModifiableComponent)
	{
		float PreValue = StatModifiableComponent->GetValue(StatToModify);
		float PostValue = StatModifiableComponent->Modify(StatToModify, ModificationType, Magnitude);
		AmountModified += (PreValue - PostValue);
	}
}

// Reverts the exact amount that was given/removed.
void UStatModifier::RevertModification(UStatModifiableComponentBase* StatModifiableComponent)
{
	if (StatModifiableComponent && RevertModificationOnRemoval)
		StatModifiableComponent->Modify(StatToModify, Addition, AmountModified);
}
  
StatHandlerComponent.cpp: PushStatModifier()

void UStatHandlerComponent::PushStatModifier(UStatModifier* StatModifierObject)
{
    if (!StatModifiableComponents.Contains(StatModifierObject->GetStatToModify())) return;

    TSubclassOf<UStatModifier> StatModifierClass = StatModifierObject->GetClass();
    EStats StatToModify = StatModifierObject->GetStatToModify();

    TotalStatModifiersMap.FindOrAdd(StatModifierClass);
    TotalStatModifiersMap[StatModifierClass].Array.Push(StatModifierObject);

    if (StatModifierObject->DoesModifyOnce())
    {
        StatModifierObject->ApplyModification(StatModifiableComponents[StatToModify]);

        if (!StatModifierObject->IsDurationInfinite())
        {
            //reorder after time
            OneTimeStatModifiersWithDuration.FindOrAdd(StatToModify);
            OneTimeStatModifiersWithDuration[StatToModify].Array.Push(StatModifierObject);

            //should use a separate function
            OneTimeStatModifiersWithDuration[StatToModify].Array.Sort([](const UStatModifier& ModifierA, const UStatModifier& ModifierB)
            {
                return ModifierA.GetCurrentDuration() > ModifierB.GetCurrentDuration();
            });
        }
        return;
    }

    if (StatModifierObject->IsDurationInfinite())
    {
        TickingStatModifiersWithInfinity.FindOrAdd(StatToModify);
        TickingStatModifiersWithInfinity[StatToModify].Array.Push(StatModifierObject);
    }
    else
    {
        //reorder after time
        TickingStatModifiersWithDuration.FindOrAdd(StatToModify);
        TickingStatModifiersWithDuration[StatToModify].Array.Push(StatModifierObject);

        //should use a separate function
        TickingStatModifiersWithDuration[StatToModify].Array.Sort([](const UStatModifier& ModifierA, const UStatModifier& ModifierB)
        {
            return ModifierA.GetCurrentDuration() > ModifierB.GetCurrentDuration();
        });
    }
}
    
Module.cpp: ActivateModule()
void UModule::ActivateModule(AActor* _ModuleOwner, ARoguelikeGameState* _GameState)
{
	ModuleOwner = _ModuleOwner;
	GameState = _GameState;
	m_StatHandler = _ModuleOwner->GetComponentByClass<UStatHandlerComponent>();

	ExistingConditionTypes.Empty();
	for (auto MyCondition : Conditions) ExistingConditionTypes.Add(MyCondition.ParentCondition);

	AddAudioVisuals();

	if (GameState == nullptr) return;
	
	// Normal Hits
	if (ExistingConditionTypes.Contains(EConditionType::OnTargetHitWithAnyProjectile))
	{
		GameState->GetDelegateData()->GetOnProjectileHitSuccessfulDelegate()->
		AddDynamic(this, &UModule::OnProjectileHitEnemy);
	}
	if (ExistingConditionTypes.Contains(EConditionType::OnTargetHitWithPrimaryProjectile))
	{
		GameState->GetDelegateData()->GetOnTargetHitWithProjectileDelegate()->
		AddDynamic(this, &UModule::OnProjectileHitTargetWithPrimary);
	}
	if (ExistingConditionTypes.Contains(EConditionType::OnTargetHitWithAlternateProjectile))
	{
		GameState->GetDelegateData()->GetOnTargetHitWithProjectileDelegate()->
		AddDynamic(this, &UModule::OnProjectileHitTargetWithAlternate);
	}
	if (ExistingConditionTypes.Contains(EConditionType::OnTargetHitWithMelee))
	{
		GameState->GetDelegateData()->GetOnMeleeKickHit()->
		AddDynamic(this, &UModule::OnTargetHitWithMelee);
	}

	// -- more conditions --
        // ... additional conditions omitted for brevity ... (crits, kills, misc, etc.)
}
  

reflection on system

This was my first time making any sort of stat modifier system, which means I learnt a lot from the process. The biggest takeaway was how nice it is to make a system as modular as I did, not having to remake or make certain parts compatible with other systems. The system was simply created in a way that worked on everything that needed stats modified!

If I could start over, I would have stored all stats that needed modification via a stat modifier in the stat handler itself, instead of making each system’s component be “stat modifiable”. It became annoying having to set up each component that needed stats modifiable. I also never accounted for systems that do not use actor components!

So instead, if some system needed to get a stat or even change a stat, I would do it straight through the stat handler instead. This way, whenever something is changed or added, I take all modifiers into account before returning a value or giving a stat a new value.

Revised version of the stat modification system: link. I did many of the things stated above in this one!

MUSIC IMPLEMENTATION

Implementation of the music in the game, done via Wwise. The music transitions seamlessly based on battle status (in battle, outside of battle), what kind of battle (boss fight or regular enemies), and room type.

the music system in action - take a listen!

In Wwise, I set up three state groups together with a switch container to control seamless music transitions.

In UE5, I use a subsystem to control and keep track of which state we are currently in. Whenever the player enters a new sector, I check what sector and set the Wwise state accordingly.

The same goes for the battle state, except I also subscribe to a delegate which broadcasts when an enemy has been killed.

wwise state groups demonstration - also take a listen!!

AudioSubsystem.cpp: SetLocationStateBasedOnSector() SetState()

void UAudioSubsystem::SetLocationStateBasedOnSector()
{
    if (!DungeonManager) return;
    ESectorType SectorType = DungeonManager->GetCurrentSectorType();
    FString StateSetMessage = SetState(SectorType);
    GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, StateSetMessage);
}

FString UAudioSubsystem::SetState(ESectorType State)
{
    const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, TEXT("ESectorType"), true);
    if (!EnumPtr) return "Could not set an Overarching State";

    FString StateGroup = "SectorType";
    FString StateValue = EnumPtr->GetNameStringByValue(static_cast<int64>(State));
    const wchar_t* Group = *StateGroup;
    const wchar_t* Value = *StateValue;
	
    AudioDevice->SetState(Group, Value);
    return "Location State Set: " + FString(Value);
}
    
AudioSubsystem.cpp: OnTargetKilled() SetGameplayStateBasedOnEnemiesLeft()

void UAudioSubsystem::OnTargetKilled(AActor* KilledTarget, AActor* Killer)
{
    SetGameplayStateBasedOnEnemiesLeft();	
}

void UAudioSubsystem::SetGameplayStateBasedOnEnemiesLeft()
{
    TArray<AActor*> Enemies;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), AEnemyCharacterBase::StaticClass(), Enemies);

    if (Enemies.Num() - 1 > 0)
    {
        SetState(EGameplayState::InBattle);
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, "InBattle");
    }
    else
    {
        SetState(EGameplayState::BetweenBattles);
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, "BetweenBattles");
    }
}
    

reflection on system

A smaller issue came up during development of the music system, where figuring out when a battle started and ended was problematic. This was due to me trying to use other programmers’ systems in ways they were not designed to be. As I was using delegates for this, I should have simply made my own system for checking how many enemies were left in a room, instead of fighting the other system. It would have most likely saved on time.

ENEMY AI

The game needed two types of regular enemy AI. One rush-down melee enemy, and one long-ranged projectile-shooting enemy.

ai system in action

To get the AI working, I made use of state trees. The general implementation was quite simple. I broke it down into several subtrees, so that I could reuse behaviours. For instance, I used the same stun tree and the same idle tree for both ranged and melee enemies.

ai behaviour planning from product owner

reflection on system

If I could redo the AI from scratch, I would have implemented some sort of central brain to make sure not all enemies attack at the same time.

I would have still used state trees, but instead of using the plugin that I used, I would have created my own implementation of a state machine and unreal’s behaviour trees. I still would try to create reusable subtrees, but I would have increased the amount I used. Sort of like building blocks for general AI behaviour.

However, the biggest issue was generally just time. At the time of creating the enemy AI, I also had to finish up the stat modification system and do the music implementation. As such, the aggro logic of the AI is slapped together with insanely bad structure. If I am to take on an AI programming role again, I would focus only on AI instead of taking on two other systems as well. I am certain I have the ability to create good behaviour in a structured way, given time.

LIKE WHAT YOU SEE?


let’s connect

MORE PROJECTS

diab.moe();

contact‍ ‍cv

moediabdev@gmail.com
076-100 51 13