Guilherme Oliveira

Guilherme de Oliveira

Gameplay Programmer

Ability System

Project Type: University (Capstone)
Engine: Unreal Engine 4
Languages: C++, Blueprints

During my Master's in Game Design, I'm having the opportunity to be in a Capstone where I can work with a lot of different people to make different types of games. The best thing is that we, the students in the Capstone, can design, pitch and make our own games!

We wanted to make a proof of concept for a Twin Stick shooter that would have some interesting concepts applied on it, my responsability on this one week project was to make an Ability System where player could have from 1 to 4 different ability slots, the ability slots could hold different abilities, or none, and the abilities should be easily implemented in Blueprints, by designers.

What I came up with was an AbilityComponent that holds a pointer to an Ability, the C++ classes would handle all the logic in creating, assigning and using the abilities, as well as keeping references to the player owner, in case the ability needs to talk with the player casting it.

The Ability Component is a class that inherits from Unreal's UActorComponent, the goal is to have components that can be attached directly in the Player's Blueprints and it is activated by an Action Input. An interesting concept is that the after the ability is used and is on a cooldown, it is not time based, but kill based, so a kill counter had to be taken in consideration while creating the class.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Templates/SubclassOf.h"
#include "BaseCharacter.h"
#include "AbilityComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class WEWENTFORTH_API UAbilityComponent : public UActorComponent
{
  GENERATED_BODY()

public:	
  UAbilityComponent();

  UPROPERTY(BlueprintReadOnly)
  ABaseCharacter* pOwner;
  FORCEINLINE ABaseCharacter* GetOwnerCharacter() { return pOwner; }

  /** Hold a pointer to the actual Ability */
  UPROPERTY(BlueprintReadOnly)
  class UAbility* AbilityBeingHold;
  FORCEINLINE void SetAbilityBeingHold(UAbility* Ability) { AbilityBeingHold = Ability; }
  FORCEINLINE UAbility* GetAbilityBeingHold() { return AbilityBeingHold; }

  UPROPERTY(BlueprintReadOnly)
  int CurrentKillCount;

protected:
  virtual void BeginPlay() override;

public:	
  /** Assign Ability */
  UFUNCTION(BlueprintCallable)
  void AssignAbilityToSlot(TSubclassOf Ability);

  /** Use Ability */
  UFUNCTION(BlueprintCallable)
  void UseAbilitySlot();

  UFUNCTION(BlueprintCallable)
  bool IsAbilityReadyToBeUsed();

  UFUNCTION(BlueprintCallable)
  void IncrementKillCount();
};
                                        

#include "AbilityComponent.h"
#include "AbilitySystem/Ability.h"
#include "Engine/Engine.h"

UAbilityComponent::UAbilityComponent() {
    PrimaryComponentTick.bCanEverTick = false;
    AbilityBeingHold = nullptr;
}

void UAbilityComponent::BeginPlay() {
    Super::BeginPlay();
    
    AActor* pOwnerActor = GetOwner();
    if (pOwnerActor != nullptr) {
        pOwner = Cast(pOwnerActor);
    }

    CurrentKillCount = 0;
}

void UAbilityComponent::AssignAbilityToSlot(TSubclassOf Ability) {
    UAbility* AbilityBeingAssigned = NewObject(this, Ability);

    if (AbilityBeingAssigned != nullptr) {
        if (AbilityBeingHold == nullptr) {
            GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Red, TEXT("Ability was Assigned"));
            AbilityBeingHold = AbilityBeingAssigned;
            AbilityBeingHold->SetOwnerComponent(this);
        }
        else if (AbilityBeingHold == AbilityBeingAssigned) {
            /** TODO - Find about to verify that the same Ability is being assigned */
            GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Red, TEXT("AbilityBeingHold == AbilityBeingAssigned"));
        }
        else {
            GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Red, TEXT("I'm in the else right now..."));
        }
    }
}

void UAbilityComponent::UseAbilitySlot() {
    UE_LOG(LogTemp, Warning, TEXT("UAbilityComponent::UseAbilitySlot()"));
    if (AbilityBeingHold != nullptr) {
        AbilityBeingHold->TryUseAbility();
        CurrentKillCount = 0;
    } 
    else {
        GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Red, TEXT("There's no ability assigned!"));
    }
}

bool UAbilityComponent::IsAbilityReadyToBeUsed() {
    if (AbilityBeingHold != nullptr) {
        if (AbilityBeingHold->AbilityStatus == EAbilityStatus::EAS_CanBeUsed) {
            return true;
        }
    }

    return false;
}

void UAbilityComponent::IncrementKillCount() {
    if (AbilityBeingHold != nullptr) {
        GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Red, TEXT("Incrementing Kill Count..."));
        CurrentKillCount++;
        if (CurrentKillCount >= AbilityBeingHold->AmountOfKillsToRefresh) {
            CurrentKillCount = 0;
            AbilityBeingHold->RefreshAbility();
        }
    }
}        
                                            

The Ability class extends Unreal's most basic class, UObject, with some additions so the class can be used to create Blueprints, it can get a reference to the owner character, which is the way the Blueprint can talk to the external world, it also has some properties that can be defined or accessed in Blueprints, so Designers can customize specific ability behavior.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "AbilityComponent.h"
#include "BaseCharacter.h"
#include "Ability.generated.h"

UENUM(BlueprintType)
enum class EAbilityStatus : uint8 {
  EAS_CanBeUsed	UMETA(DisplayName = "CanBeUsed"),
  EAS_OnCooldown	UMETA(DisplayName = "OnCooldown"),
  EAS_Max			UMETA(DisplayName = "DefaultMax")
};

/**
  * 
  */
UCLASS(BlueprintType, Blueprintable)
class WEWENTFORTH_API UAbility : public UObject {
  GENERATED_BODY()

public:
  UAbility();

  UPROPERTY(BlueprintReadOnly)
  UAbilityComponent* pOwnerComponent;
  FORCEINLINE void SetOwnerComponent(UAbilityComponent* Owner) { pOwnerComponent = Owner; }

  UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Abilities")
  int AmountOfKillsToRefresh;

  UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Abilities")
  EAbilityStatus AbilityStatus;
  FORCEINLINE EAbilityStatus GetAbilityStatus() { return AbilityStatus; }
  FORCEINLINE void SetAbilityStatus(EAbilityStatus Status) { AbilityStatus = Status; }

protected:

public:
  /** Try Use Ability is the entry point to be called from the Ability Component */
  void TryUseAbility();

  UFUNCTION(BlueprintImplementableEvent)
  void UseAbility(ABaseCharacter* OwnerCharacter);

  UFUNCTION(BlueprintCallable)
  ABaseCharacter* GetReferenceToCharacter();

  UFUNCTION(BlueprintCallable)
  void RefreshAbility();
};
                                

#include "Ability.h"

UAbility::UAbility() {
  AmountOfKillsToRefresh = 10;
  AbilityStatus = EAbilityStatus::EAS_CanBeUsed;
}

void UAbility::TryUseAbility() {
  ABaseCharacter* OwnerCharacter = GetReferenceToCharacter();

  if (AbilityStatus == EAbilityStatus::EAS_CanBeUsed && IsValid(OwnerCharacter)) {
    AbilityStatus = EAbilityStatus::EAS_OnCooldown;
    UseAbility(OwnerCharacter);
  }
}

void UAbility::RefreshAbility() {
  SetAbilityStatus(EAbilityStatus::EAS_CanBeUsed);
}

ABaseCharacter* UAbility::GetReferenceToCharacter() {
  return pOwnerComponent->GetOwnerCharacter();
}