Niagara Rats

About

For this project, the goal was twofold: firstly, to craft a user interface (UI) purely with C++, along with making UMG to communicate with C++ ensuring a seamless and responsive design. Secondly, I aimed to replicate the AI system of the rats from "A Plague Tale," utilizing Unreal Engine's Niagara Particles System in combination with C++ for the AI behaviours.

Project Info

  • Role: UI/AI/Gameplay Programmer
  • Team Size: 1
  • Time frame: 1 Month
  • Engine: Unreal Engine 5.3
  • Langauge: C++ | Blueprints

Introduction

For RedRoad project, prominently featured on the Home Page, the game's design centered around managing hordes of zombies, a crucial aspect of the game loop. Given the game's requirement to handle numerous enemies simultaneously, optimizing performance became a significant challenge. Initially, I experimented with leveraging the Niagara System to simulate the zombie horde AI, aiming to achieve both visual and behavioral realism. However, due to time constraints, this approach was ultimately not included in the final iteration

Along with optimizing AI, I always wanted to expand my knowledge on how UMG works under the hood. Being a wrapper over SLATE's framework, I not only managed to update and design UI using pure C++(Slate), but also made UMG designed UI to update and communicate with C++.

So in this project I have two variants for each:

  • Niagara AI behavior using blueprints
  • Niagara AI behavior using C++
  • Pure C++ UI, designed and updating with SLATE
  • UMG designed UI, having core functionality in C++

Niagara Rats

I started my development by first making the Niagara Rats system using the Unreal Engine's Niagara System. To make is communicate with the world, I used the NiagaraCallbackHandlers to update the rats with the players location. Based on the Players location and the bullets impact point I am exporting the data from Niagara System to make the rats either attack the player, or to die if the player is shooting.

Niagara AI System

After creating the rats particle system, I started working on making the Niagara System to talk with the gameplay. For retreving all of the exported particle data, I implemented the INiagaraParticleCallbackHandler interface. This gives access to all of the data that is exported from the Niagara Particle system. Using this data I am triggering the rats to either attack the player or to die if the distance between the bullet impact point and each niagara particle is less than a specified distance.

Because the code was too big, below is the link to the file if you want to check the full code

ANiagaraRatsComponent::ANiagaraRatsComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    PrimaryActorTick.bCanEverTick = true;
    NiagaraParticleRef = CreateDefaultSubobject(TEXT("NiagaraRats"));
    NiagaraParticleRef->SetupAttachment(RootComponent);
    
    // assigns the Niagara system to the component
    static ConstructorHelpers::FObjectFinder NiagaraSystemAssetRef(TEXT("/Script/Niagara.NiagaraSystem'/Game/Niagara/NS_PlagueRats.NS_PlagueRats'"));
    if(NiagaraSystemAssetRef.Succeeded())
    {		
        NiagaraParticleRef->SetAsset(NiagaraSystemAssetRef.Object);
    }
    
    Collider = CreateDefaultSubobject("Collider");
    Collider->SetupAttachment(RootComponent);
    Collider->SetWorldScale3D(FVector3d(30.0f, 30.0f, 1.0f));

    DamageTimer = 50;	
}

void ANiagaraRatsComponent::BeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	InAttackRange = true;
	NiagaraParticleRef->SetVariableFloat(FName("AttackRange"), 1.0f);	
}

void ANiagaraRatsComponent::EndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
	UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	InAttackRange = false;
	NiagaraParticleRef->SetVariableFloat(FName("AttackRange"), 0);	
}

void ANiagaraRatsComponent::GiveDamage()
{	
	UGameplayStatics::ApplyDamage(CharacterRef, 5.0f, GetInstigatorController(), this, UDamageType::StaticClass());			
}

Pure C++ UI

Now that the Niagara AI system is working as intended, I started diving deeper in the UI part. My first iteration was on making a widget having all of the functionality in C++ but desinged using UMG which was easy to do and I got it working.

HealthWidget.cpp

After this my focus shifted to diving deep into how the UMG works. So to design and create a pure C++ UI, I used the RebuildWidget function which constructs the UWidgetTree and its slots.

So the difference between RebuildWidget and NativeConstruct is that RebuildWidget is about constructing or reconstructing the widget's structure and its children, providing a place to dynamically define the widget tree.

NativeConstruct is for initialization tasks that need to occur after the widget and all its children have been constructed, ideal for setup that depends on the widget hierarchy being fully established.

For the Full code you can checkout the link

// to draw the widget and set the WidgetTree hierarchy 
TSharedRef UHealthWidgetCpp::RebuildWidget()
{
// sets the Canvas panel as the root of this widget
UPanelWidget* RootCanvasPanel = Cast(GetRootWidget());
if(!RootCanvasPanel)
{		
    RootCanvasPanel = WidgetTree->ConstructWidget(UCanvasPanel::StaticClass(), TEXT("RootWidget"));
    if(UCanvasPanelSlot* RootWidgetSlot = Cast(RootCanvasPanel->Slot))		
    {
        RootWidgetSlot->SetAnchors(FAnchors(0.0f,0.0f,1.0f,1.0f));
        RootWidgetSlot->SetOffsets(FMargin(0.f,0.f));			
    }
    WidgetTree->RootWidget = RootCanvasPanel;
}

if(RootCanvasPanel && WidgetTree)
{
    // makes horizontal box as child of Canvas Panel
    UHorizontalBox* HorizontalBox = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("Horizontal Box"));
    RootCanvasPanel->AddChild(HorizontalBox);		
    if(UCanvasPanelSlot* HorizontalBoxSlot = Cast(HorizontalBox->Slot))
    {
        HorizontalBoxSlot->SetAnchors(FAnchors(0.f, 1.f));
        HorizontalBoxSlot->SetPosition(FVector2D(200.0f, -120.0f));
        HorizontalBoxSlot->SetAutoSize(true);			
    }

    // makes a size box and adds as the child of the horizontal box
    USizeBox* SizeBox = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("SizeBox"));
    HorizontalBox->AddChildToHorizontalBox(SizeBox);
    
    // the size box slot 
    if(UHorizontalBoxSlot* SizeBoxSlot = Cast(SizeBox->Slot))
    {
        SizeBoxSlot->SetHorizontalAlignment(HAlign_Fill);
        SizeBoxSlot->SetVerticalAlignment(VAlign_Fill);
        SizeBox->SetHeightOverride(100.0f);
        SizeBox->SetWidthOverride(150.0f);
    }

    // creates the text slot as the child of the size box
    TXT_HealthText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("TextBox"));
    SizeBox->AddChild(TXT_HealthText);
    if(USizeBoxSlot* TextBoxSlot = Cast(TXT_HealthText->Slot))		
    {
        TextBoxSlot->SetHorizontalAlignment(HAlign_Center);
        TextBoxSlot->SetVerticalAlignment(VAlign_Center);					
    }

    // Size box for progress bar
    USizeBox* BarSizeBox = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("ProgressBarSizeBox"));
    HorizontalBox->AddChildToHorizontalBox(BarSizeBox);

    if(UHorizontalBoxSlot* BarSizeBoxSlot = Cast(BarSizeBox->Slot))
    {
        BarSizeBoxSlot->SetHorizontalAlignment(HAlign_Fill);
        BarSizeBoxSlot->SetVerticalAlignment(VAlign_Center);
        BarSizeBox->SetHeightOverride(30.0f);
        BarSizeBox->SetWidthOverride(550.0f);
    }

    // creates the health bar as the child of the Horizontal box
    BAR_HealthBar = WidgetTree->ConstructWidget(UProgressBar::StaticClass(), TEXT("ProgressBar"));
    BarSizeBox->AddChild(BAR_HealthBar);
    
    if(USizeBoxSlot* HealthBarSlot = Cast(BAR_HealthBar->Slot))
    {
        HealthBarSlot->SetHorizontalAlignment(HAlign_Fill);
        HealthBarSlot->SetVerticalAlignment(VAlign_Fill);			
        BAR_HealthBar->SetFillColorAndOpacity(FLinearColor::Red);
        BAR_HealthBar->SetBarFillType(EProgressBarFillType::LeftToRight);
        BAR_HealthBar->SetBarFillStyle(EProgressBarFillStyle::Scale);
    }		
}	
return Super::RebuildWidget();
}