Escape From Dungeon

About

This project marked my inaugural foray into game development with Unreal Engine. After acquiring some foundational skills in Unreal Engine and game development principles, I embarked on this project during my Bachelor's in Computer Science. My primary aim was to deepen my understanding of game programming through hands-on experience.

Project Info

  • Role: UI/Gameplay/AI Programmer
  • Team Size: 1
  • Time frame: 4 Months
  • Engine: Unreal Engine 4.25
  • Langauge: C++

Introduction

Having alread learned some basics of Unreal Engine components, I wanted to get hand on experience into implementing those. The things that I worked on for this project are:
  • Developed a Custom Character Movement Component derived from the Pawn Class, incorporating various movement and stamina states along with collision handling.
  • Implemented Game Systems to update UI logic using C++, while utilizing UMG for the design aspect.
  • Engineered a Save/Load Game feature capable of preserving player information and map positions, including health, weapons, and other data.
  • Crafted intelligent melee enemy AIs with varying damage outputs and various AI states
  • Worked on an array of game elements including pickups, weapons, potions, animation states, and floating platforms to enrich the character's gameplay experience.

Movement Component

The custom movement component is basically a capsule for the player, which adds force in the direction of the input , which makes the character move around. When the character is colliding with something, it slides along the side of that thing.

void UColliderMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if (!PawnOwner || !UpdatedComponent || ShouldSkipUpdate(DeltaTime))
    {
        return;
    }

    FVector DesiredMovementThisFrame = ConsumeInputVector().GetClampedToMaxSize(1.0f);

    if (!DesiredMovementThisFrame.IsNearlyZero())
    {
        FHitResult Hit;
        SafeMoveUpdatedComponent(DesiredMovementThisFrame, UpdatedComponent->GetComponentRotation(), true, Hit);

        //if we bump into something slide along the side of it
        if (Hit.IsValidBlockingHit())
        {
            SlideAlongSurface(DesiredMovementThisFrame, 1.f - Hit.Time, Hit.Normal, Hit);
            UE_LOG(LogTemp, Warning, TEXT("Valid Blocking Hit"));
        }
    }
}                            

UMG Widgets

For the widgets, I designed them in-editor using UMG and let the main logic of it to stay in C++, because it would be easier to access player info from C++ and update the widgets accordingly. I am using the PlayerController for creating the widget and updating using event systems without the Tick.

For the Full code you can checkout the link

void AMainPlayerController::BeginPlay()
{
    Super::BeginPlay();

    if (HUDOverlayAsset)
    {
        HUDOverlay = CreateWidget(this, HUDOverlayAsset);
    }
    HUDOverlay->AddToViewport();
    HUDOverlay->SetVisibility(ESlateVisibility::Visible);

    if (WEnemyHealthBar)
    {
        EnemyHealthBar = CreateWidget(this, WEnemyHealthBar);
        if (EnemyHealthBar)
        {
            EnemyHealthBar->AddToViewport();
            EnemyHealthBar->SetVisibility(ESlateVisibility::Hidden);
        }
        FVector2D Alignment(0.f, 0.f);
        EnemyHealthBar->SetAlignmentInViewport(Alignment);
    }

    if (WPauseMenu)
    {
        PauseMenu = CreateWidget(this, WPauseMenu);
        if (PauseMenu)
        {
            PauseMenu->AddToViewport();
            PauseMenu->SetVisibility(ESlateVisibility::Hidden);
        }			
    }
}

// displays the enemy health abr while the enemy is still alive
void AMainPlayerController::DisplayEnemyHealthBar()
{
    if (EnemyHealthBar)
    {
        bEnemyHealthBarVisible = true;
        EnemyHealthBar->SetVisibility(ESlateVisibility::Visible);
    }
}
void AMainPlayerController::TogglePauseMenu()
{
    if (bPauseMenuVisible)
    {
        RemovePauseMenu();
    }
    else
    {
        DisplayPauseMenu();
    }
}
void AMainPlayerController::GameModeOnly()
{
    FInputModeGameOnly InputModeGameOnly;
    SetInputMode(InputModeGameOnly);
}

Melee Enemy AI

I created basically 2 types of enemies. One is the Critter type and the other is melee type having a sword in its hand. For both the types I made a base enemy class having all of the common logic like the AI states, in the base class, while the type specific logic stays in their own classes. Using this way it was was easier to make multiple enemies having different meshes, animation states by just tweaking the stats values.

Critter Type Enemy
Melee Type Enemy

For the Full code you can checkout the link

void AEnemy::AgroSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
    if (OtherActor)
    {
        AMain* Main = Cast(OtherActor);
        if (Main)
        {
            bHasValidTarget = false;
            if (Main->CombatTarget == this)
            {
                Main->SetCombatTarget(nullptr);
            }
            Main->SetHasCombatTarget(false);
            Main->UpdateCombatTarget();
            SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Idle);		
            if (AIController)
            {
                AIController->StopMovement();
            }
        }
    }
}
void AEnemy::CombatSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor && Alive())
    {
        AMain* Main = Cast(OtherActor);
        if (Main)
        {
            bHasValidTarget = true;
            Main->SetCombatTarget(this);
            Main->SetHasCombatTarget(true);
            Main->UpdateCombatTarget();

            CombatTarget = Main;
            bOverlappingCombatSphere = true;
            
            float AttackTime = FMath::FRandRange(AttackMinTime, AttackMaxTime);
            GetWorldTimerManager().SetTimer(AttackTimer, this, &AEnemy::Attack, AttackTime);
        }
    }
}
float AEnemy::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)
{
    if (Health - DamageAmount <= 0.f)
    {
        Health -= DamageAmount;
        Die(DamageCauser);
    }
    else
    {
        Health -= DamageAmount;
    }
    return DamageAmount;
}

void AEnemy::Die(AActor* Causer)
{
    UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
    if (AnimInstance)
    {
        AnimInstance->Montage_Play(CombatMontage, 1.75f);
        AnimInstance->Montage_JumpToSection(FName("Death"), CombatMontage);
    }
    SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Dead);

    CombatCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    AgroSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    CombatSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

    bAttacking = false;

    AMain* Main = Cast(Causer);
    if (Main)
    {
        Main->UpdateCombatTarget();
    }
}

Save/Load Game

For Saving and loading the game from any point of the game, I am inheriting from the USaveGame Object. Then I am storing all of the values like the player health, weapon, map location etc and then using the SaveGameToSlot to actually save the game on the player's local machine.

Similarly for loading the game, I have used the LoadGameFromClass inside the UGameplayStatics to loading all of the values if any saved slot exists. If no slot exists than I am using the default values creating a new game.

For the Full code you can checkout the link

void AMain::SaveGame()
{
    UFirstSaveGame* SaveGameInstance = Cast(UGameplayStatics::CreateSaveGameObject(UFirstSaveGame::StaticClass()));

    SaveGameInstance->CharacterStats.Health = Health;
    SaveGameInstance->CharacterStats.MaxHealth = MaxHealth;
    SaveGameInstance->CharacterStats.Stamina = Stamina;
    SaveGameInstance->CharacterStats.MaxStamina = MaxStamina;
    SaveGameInstance->CharacterStats.Coins = coins;

    FString MapName = GetWorld()->GetMapName();
    MapName.RemoveFromStart(GetWorld()->StreamingLevelsPrefix);
    
    SaveGameInstance->CharacterStats.LevelName = MapName;

    if (EquippedWeapon)
    {
        SaveGameInstance->CharacterStats.WeaponName = EquippedWeapon->Name;
    }

    SaveGameInstance->CharacterStats.Location = GetActorLocation();
    SaveGameInstance->CharacterStats.Rotation = GetActorRotation();

    UGameplayStatics::SaveGameToSlot(SaveGameInstance, SaveGameInstance->PlayerName, SaveGameInstance->UserIndex);
}

void AMain::LoadGame(bool SetPosition)
{
    UFirstSaveGame* LoadGameInstance = Cast(UGameplayStatics::CreateSaveGameObject(UFirstSaveGame::StaticClass()));

    LoadGameInstance = Cast(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->PlayerName, LoadGameInstance->UserIndex));

    Health = LoadGameInstance->CharacterStats.Health;
    MaxHealth = LoadGameInstance->CharacterStats.MaxHealth;
    Stamina = LoadGameInstance->CharacterStats.Stamina;
    MaxStamina = LoadGameInstance->CharacterStats.MaxStamina;
    coins = LoadGameInstance->CharacterStats.Coins;

    if (WeaponStorage)
    {
        AItemStorage* Weapons = GetWorld()->SpawnActor(WeaponStorage);
        if (Weapons)
        {
            FString WeaponName = LoadGameInstance->CharacterStats.WeaponName;

            if (Weapons->WeaponMap.Contains(WeaponName))
            {
                AWeapon* WeaponToEquip = GetWorld()->SpawnActor(Weapons->WeaponMap[WeaponName]);
                WeaponToEquip->Equip(this);
            }			
        }
    }
    if (SetPosition)
    {
        SetActorLocation(LoadGameInstance->CharacterStats.Location);
        SetActorRotation(LoadGameInstance->CharacterStats.Rotation);
    }
    SetMovementStatus(EMovementStatus::EMS_Normal);
    GetMesh()->bPauseAnims = false;
    GetMesh()->bNoSkeletonUpdate = false;

    if (LoadGameInstance->CharacterStats.LevelName != TEXT(""))
    {
        FName LevelName(*LoadGameInstance->CharacterStats.LevelName);

        SwitchLevel(LevelName);
    }
}